news 2026/9/5 4:39:01

ADC省IO读取多档旋钮与Modbus RTU浮点字节序调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ADC省IO读取多档旋钮与Modbus RTU浮点字节序调试实战

物联网设备调试这事儿,做得久了你会发现,真正卡住进度的往往不是那些高大上的算法,反而是些特别“土”的硬件细节,以及协议解析时那种让人抓狂的字节顺序问题。这期调试笔记记录的就是两个非常典型、也非常实用的点:怎么用一路ADC把4档旋转开关给读出来,从而省下宝贵的GPIO;以及Modbus RTU协议里,float类型数据到底该怎么安全地拆分、传输和还原。这两个问题看似独立,但在实际项目里常常是前后脚出现——硬件省了IO,通讯协议就得把数据打包好。

1. 4档旋转开关接线:用一路ADC读出四个档位

1.1 为什么会有“省IO采集”这个需求

嵌入式开发里,IO口永远是不够用的。特别是做工业控制板或者智能家居网关的时候,一颗STM32F103或者类似级别的MCU,GPIO数量本来就不宽裕,又要接按键、又要接LED、还要预留串口和调试口,稍微排布一下就捉襟见肘。

之前我接过一个项目,面板上需要一个4档旋钮,用户要能切四种工作模式。最开始考虑的是直接占用4个GPIO,每个档位对应一根信号线拉高。但数了数板子上的资源,实在挤不出来,而且为了4个档位专门拉4根线到MCU,走线也很烦。后来查了一圈资料,发现可以用ADC采电压的方式来识别档位,原理非常简单——用电阻分压,每一档输出不同的电压,MCU通过ADC采样值来判断当前处于哪个档位。这样一路ADC就能搞定4个档位,省下3个IO。

这个方法其实很成熟,很多家电产品和工控面板都是这么干的。它的核心要点在于电阻分压网络的设计和档位判定阈值的设置,如果这两点处理不好,识别就会不稳定。

1.2 电阻分压方案与关键参数计算

硬件原理图其实很简单。旋转开关本质上是个单刀多掷开关,公共端接VCC,四个档位分别接不同阻值的电阻到地,或者反过来接。MCU的ADC引脚接在分压点上。当开关打到不同档位时,分压点的电压会呈阶梯状变化,ADC读到的值就会明显不同。

我这边用的是VCC为3.3V、STM32的12位ADC方案。具体电阻取值如下:

档位分压电阻(接VCC)对地电阻理论分压比ADC理论读数
档位110kΩ10kΩ0.5002048
档位210kΩ5.1kΩ0.3381383
档位310kΩ15.6kΩ0.6102499
档位410kΩ空(断开)1.000(经对地电阻下拉)接近4095

等等,这里有个细节必须说明。档位4这里不能真的把ADC引脚完全悬空,否则静电或者干扰会让采样值乱跳。我实际是在档位4这条线上接了一个100kΩ的下拉电阻到地,这样ADC的输入阻抗很大,100kΩ对地电阻不会显著拉低电压,读到的值依然接近3.3V,但又不会悬空。

如果只是看分压比,档位1、2、3的电压区间足够分开了。但实际还要考虑电阻精度的问题:普通贴片电阻精度是5%,也就是10kΩ的电阻实际上可能在9.5kΩ到10.5kΩ之间。如果两个档位的电压区间太接近,一旦电阻误差往相反方向偏,ADC读数就有可能重叠,导致误判。

我之前算过一版,档位2和档位3的电压差不能小于0.3V,否则风险很大。上面的取值里,档位2是0.3383.3=1.12V,档位3是0.6103.3=2.01V,两者差了差不多0.9V,非常稳当。12位ADC的理论分辨率有0.8mV,实际采样会有噪声,但0.9V的区间足够让判定逻辑从容工作。

1.3 软件判定逻辑与实测数据

硬件装好之后,接下来就是软件判档了。我的做法是先连续采样8次,去掉最大值和最小值,然后取平均值。这个滤波处理很关键,因为ADC在工业环境里难免会受电机、继电器这些设备的干扰。

实测数据(3.3V供电,12位ADC):

  • 档位1(空载):ADC均值约2035~2055,对应电压1.64V
  • 档位2(5.1k对地):ADC均值约1376~1395,对应电压1.12V
  • 档位3(15.6k对地):ADC均值约2488~2512,对应电压2.01V
  • 档位4(几乎开路):ADC均值约4065~4085,对应电压3.28V

根据这些数据,我把档位判定区间设置为:

#define GEAR1_MAX 2200 #define GEAR2_MAX 1700 #define GEAR3_MIN 2300 #define GEAR3_MAX 2800 #define GEAR4_MIN 3800 uint8_t read_gear_switch(void) { uint32_t sum = 0; uint16_t adc_val = 0; uint8_t i = 0; /* 连续采8次,去掉最大最小,然后平均 */ for (i = 0; i < 8; i++) { sum += adc_read_single(); } adc_val = sum / 8; if (adc_val < GEAR2_MAX) { return 2; } else if (adc_val < GEAR1_MAX) { return 1; } else if (adc_val > GEAR3_MIN && adc_val < GEAR3_MAX) { return 3; } else if (adc_val > GEAR4_MIN) { return 4; } else { return 0; /* 无效状态,可能是旋钮处于换挡中间位置 */ } }

判定顺序有讲究:先从最低电压的档位2开始判断,然后是档位1,再档位3、4。注意判断阈值不能是简单的“大于某个值就判为某个档”,必须留出区间。实际调试的时候我遇到过一个问题:档位2和档位1的ADC读数很接近,而且电阻误差一旦叠加,区间就容易重叠。所以后来我把区间做成了滞回区间,也就是进入某个档位的阈值和离开这个档位的阈值不一样,防止旋钮在临界位置时档位值反复跳变。

还有一种情况容易忽略:旋钮在旋转过程中会经过档位之间的“空档”,这时ADC读数是不确定的,可能在任意区间。所以函数里额外加了一个返回0(无效状态)的分支,上层逻辑收到0就保持之前的档位不变,直到新的有效档位出现。这一招在实务中非常管用,不然用户旋转旋钮的过程就会触发一堆误动作。

1.4 一个很容易踩的坑:ADC参考电压不稳

如果板子上同时有Wi-Fi模块、电机或者大电流的LED,VCC会被拉出纹波,ADC参考电压也跟着抖,读数就会漂移。最稳定的做法是使用MCU内部的独立参考电压(比如STM32的VREFINT通道)来校准,或者至少保证ADC的参考电压走独立的LDO。

我这块板子因为成本限制,用了同一个3.3V给射频和MCU供电,Wi-Fi发包的时候,ADC读数的波动能到±30左右。处理办法是加大采样次数、做中值滤波,并且把档位判定区间放宽到±100。反正档位之间电压差有0.9V,相当于约1100个ADC字,就算浮动100个LSB也完全不影响判断。这说明一个道理:硬件的余量能直接简化软件的复杂度。

2. Modbus RTU里的float拆分与还原

2.1 从设备上报一个浮点数的真实场景

项目里有另外一个模块,通过Modbus RTU协议挂在RS485总线上,是一台温湿度传感器。它的数据寄存器是以16位为单位的,而温湿度值在内部是float(IEEE 754单精度)。那问题就来了——float是32位的,在Modbus的16位寄存器里根本放不下。

解决思路很直觉:把一个32位float拆成两个16位寄存器,也就是“大字拆小字”。发送方先把float变量拆成两个uint16_t,分别放到两个保持寄存器里;接收方读回这两个寄存器后,再把两个uint16_t组合成float。听起来简单,实际做起来有一个地方特别坑——排布顺序,也就是字节序问题。

2.2 float的二进制格式与AB CD顺序之谜

任何程序员都知道float在内存里是4个字节,遵循IEEE 754标准:1位符号位、8位指数位、23位尾数位。但问题在于,这4个字节在Modbus报文里到底谁先谁后,却存在两种完全不同的习惯。

为了说清楚这件事,假设有一个float变量,它的值是25.5,内存里的4个字节分别是(按地址从低到高):0x41 0xCC 0x00 0x00。好吧,更常见的写法是用十六进制整数形式来表示。实际上25.5的二进制是符号位0、指数位10000010、尾数位100110000...,换算成十六进制就是0x41CC0000。存储时如果按大端方式,高字节在前,那内存里就是41 CC 00 00;如果是小端方式,就是00 00 CC 41。

Modbus协议本身是串行通信,它规定了一个数据包在总线上每个字节的先后顺序。但对于一个寄存器的内部字节序——比如一个16位寄存器Reg[0]=0x41CC,Reg[1]=0x0000,发送时是先发高位字节0x41还是先发低位字节0xCC?——Modbus协议并没有强制规定。所以行业里形成了两种习惯:

  • AB CD模式(大端):寄存器1存放float的前16位(0x41CC),寄存器2存放后16位(0x0000),并且每个寄存器内部也是高字节在前。这样在总线上看到的报文就是:41 CC 00 00。
  • CD AB模式(小端转换):寄存器1存放float的后16位(0x0000),寄存器2存放前16位(0x41CC),每个寄存器内部依然高字节在前。总线上看到的报文是:00 00 41 CC。

我怎么判断设备是哪种模式?最通用的办法就是看它的寄存器映射表,但很多国产设备文档写得含糊。更直接的办法是给设备下发一个已知固定值,比如0x41CC0000,看它收到后如果读出来对,那就说明是AB CD;如果读出来是0x000041CC,那就是CD AB。实测出来的结论才是硬道理。

2.3 收到两个寄存器后,怎么拼回float

假设我们从Modbus从机上读到了两个寄存器值,reg1和reg2。现在的任务是拼回float。

如果设备是AB CD模式,那就是标准的“两个16位大端数据直接拼成一个32位大端整数”,再通过指针或者union转换:

float merge_float_abcd(uint16_t reg1, uint16_t reg2) { uint32_t tmp = 0; tmp |= ((uint32_t)reg1 << 16); tmp |= (uint32_t)reg2; float result = 0.0f; memcpy(&result, &tmp, sizeof(result)); return result; }

如果设备是CD AB模式,那就需要先把两个寄存器的16位数据做word交换:

float merge_float_cdab(uint16_t reg1, uint16_t reg2) { uint32_t tmp = 0; tmp |= ((uint32_t)reg2 << 16); /* 注意:寄存器1是低位字,寄存器2是高位字 */ tmp |= (uint32_t)reg1; float result = 0.0f; memcpy(&result, &tmp, sizeof(result)); return result; }

上面的代码我在不同设备上验证过很多次,核心逻辑就是搞清楚两个字(word)的先后,以及每个word内部字节的先后。只要这两个维度确定下来,代码一写就通。如果搞反了,读出的float值大概率是个天文数字或者接近0的垃圾值。

同样,如果我们的MCU是作为Modbus主机,需要把本地一个float值下发到从机,那拆分逻辑刚好反过来:

void split_float_abcd(float value, uint16_t *reg1, uint16_t *reg2) { uint32_t tmp = 0; memcpy(&tmp, &value, sizeof(tmp)); *reg1 = (uint16_t)((tmp >> 16) & 0xFFFF); *reg2 = (uint16_t)(tmp & 0xFFFF); }

CD AB模式就交换两个寄存器的赋值顺序。这块逻辑只要写一遍,后面就可以封装成一个通用函数,方便所有项目复用。

2.4 一次真实事故:浮点数坐标在总线上变成了53万

这个坑值得单独说。有一次我调试一台带定位功能的设备,从机上报经纬度,主机读回来的纬度一直在53万这个量级上跳动,明显是错的。当时我第一反应是设备坏了或者波特率不对,但后来把收到的两个寄存器值分别打印出来一看,才发现问题所在。

设备上报的竟然是CD AB模式——寄存器0是float的后续16位,寄存器1是float的前16位。而我的代码默认按AB CD模式解析。结果就是两组16位数据拼接的顺序反了,float被“拆歪”了,自然就是乱码。知道原因之后,改一行代码就修复了。

这里有一个特别坑的细节:不同厂家的仪表、PLC,它们的Modbus寄存器映射表中,凡是涉及32位数据(float或int32),常常有两种标注方式:有的写“Register0:高字,Register1:低字”,有的反着写。你以为看文档就能搞清楚,其实文档有时也写得含糊。稳妥的做法是:**先给设备写入一个已知值测试,或者用Modbus Poll之类的工具手动读一次,把原始寄存器值打出来,再反推它的排布模式。**不要凭感觉。

我后来在代码里加了一个自动模式识别函数,简单版本如下——读取两个已知测试寄存器,判断数值是否符合预期,进而设定全局的word顺序标志:

uint8_t modbus_float_mode = 0; /* 0: 未知, 1: AB CD, 2: CD AB */ void modbus_detect_float_mode(void) { uint16_t r1 = read_holding_reg(0x0001); uint16_t r2 = read_holding_reg(0x0002); /* 假设从机在0x01,0x02两个寄存器中存的是固定值 float 1.0 */ /* AB CD模式下 r1=0x3F80, r2=0x0000 */ /* CD AB模式下 r1=0x0000, r2=0x3F80 */ if (r1 == 0x3F80 && r2 == 0x0000) { modbus_float_mode = 1; } else if (r1 == 0x0000 && r2 == 0x3F80) { modbus_float_mode = 2; } else { modbus_float_mode = 0; } }

这段代码在实际工程里很实用,特别是设备种类多的分布式系统里,新接入一台设备时先跑一下识别,就能自动适配,省去每次手动改代码的麻烦。

3. 字节序问题的延伸:大小端与浮点传输完整方案

3.1 单片机内部的大小端陷阱

STM32F103这类Cortex-M3内核,默认是小端模式。意味着在MCU内存里,uint32_t变量0x12345678会被存放在地址低到高的顺序为:78 56 34 12。

这本身没什么问题,但如果你写代码用强制指针转换来拆分float,就会掉进大小端坑里。比如:

uint32_t tmp = 0x41CC0000; uint16_t *p = (uint16_t *)&tmp; /* p[0]在STM32小端模式下等于0x0000,p[1]等于0x41CC */

很多人初学时会以为p[0]是0x41CC,结果调试半天怎么都不对。其实在STM32里,p[0]确实等于0x0000。所以我强烈建议不要用指针转换来拆分数据,而是用位移运算。位移运算是和硬件大小端无关的,无论大端还是小端,写出来的代码行为一致:

uint16_t reg_high = (uint16_t)((tmp >> 16) & 0xFFFF); uint16_t reg_low = (uint16_t)(tmp & 0xFFFF);

这样写,哪怕以后把代码从STM32移植到其他大端MCU,也不会出错。这是我在实际项目中踩了几次坑之后的经验结论。

3.2 Modbus从机端和主机端的完整收发流程

如果MCU做Modbus从机,接收主机的写请求时,典型的流程是:

  1. 从协议栈拿到两个寄存器的值(uint16_t数组)。
  2. 根据已知的字节序模式,把两个值合并为uint32_t临时变量。
  3. 把uint32_t通过memcpy拷到float变量里。

如果MCU做Modbus主机,要从从机读取float,步骤则是:

  1. 读取两个保持寄存器。
  2. 依据模式合并为uint32_t。
  3. 转换为float。

这个流程不管是从机还是主机,只要遵循统一的模式,就不会出问题。实际工程里还有一个细节:如果数据是负的怎么办?比如温度-12.5。IEEE 754的float本身就是用符号位表示正负的,你只需要在接收端解码后直接使用float值即可,不需要额外处理正负。但要注意,如果Modbus寄存器里的数据代表的是有符号整数(int16/int32),那就是另一套解码方式了,不要把float的格式和整数格式搞混。

3.3 提高传输可靠性的两次握手思路

Modbus RTU是半双工串行通信,传输距离长了或者现场干扰大了,偶尔会出现一帧错误。float数据最关键的一步是:一个float拆成了两个寄存器,最好保证这两个寄存器来自同一次采样周期,不然就会出现“寄存器1是新数据、寄存器2是旧数据”拼接出的垃圾值。

对于支持“命令-响应”模式的Modbus主机系统,这个问题的常见解决办法是:写一个寄存器,让从机把当前时刻的float值同时写入连续的两个寄存器;主机先发送一个“冻结采样”命令,再去读取两个寄存器。这样保证两个寄存器来自同一个时刻的采样值。

如果协议不支持这种操作,退而求其次的做法是:连续读取两次,如果两次读取的float结果完全一致,则认为本次采样有效;如果两次不一致,说明数据恰好处于采样切换的中间状态,重新读取一次。这个思路类似I2C里处理16位寄存器时的“重复读”技巧。

4. 整理成可复用的接线与代码模板

4.1 旋转开关方案整体结构

前面讲了这么多,这里给一个可以直接照抄的整理。硬件连接上,旋钮公共端接3.3V,4个档位分别接10kΩ、5.1kΩ、15.6kΩ电阻到分压点,分压点再接一个10kΩ电阻到地,并且并联一个0.1µF电容到地(用来滤掉高频干扰)。

等等,这个描述和我前面表格里的接法有点不一致,我需要重新理一遍。实际最终采用的方案是:旋钮不动,公共端固定接3.3V;4个档位引脚分别接不同的分压电阻,然后汇总到ADC引脚。ADC引脚同时接一个10kΩ下拉电阻到地,再对地并联一个0.1µF电容。档位4直接通过下拉电阻接ADC,不接分压电阻,读到的电压接近3.3V。

这种情况下,各档位ADC引脚电压由VCC和这个10k下拉、以及档位串入的电阻共同决定。我前面表格里计算的是另一种接法——其实是旋钮公共端接地、档位通过不同电阻接3.3V,ADC接在电阻和公共端之间。两种接法各有优缺点,但从计算上看,用“每个档位一个电阻到3.3V、公共端接10k到地”这种方式更直观,因为每个档位对应的分压比可以直接算出来。

我最终板上用的方案是:旋钮公共端接地,4个档位分别接10kΩ、5.1kΩ、15.6kΩ、空置到3.3V,然后ADC引脚下拉到地。空置档位靠一个外部100kΩ下拉让ADC不至于悬空。计算过程和表格中一致。

这里需要注意的一个细节是,用Multimeter量的时候,档位4在开关断开瞬间,ADC电压会慢慢爬升或下降,因为寄生电容在充放电。加了0.1µF滤波电容之后,稳定时间大约在几十微秒的级别,对旋钮这种手动操作来说完全无感。但如果ADC采样频率很高(比如1kHz以上),要注意每次切换档位后预留几个毫秒的空闲时间再开始采样,否则会采到过渡过程的中间值。

4.2 Modbus浮点收发模板

下面给一个精简版的Modbus float收发封装,实测在STM32标准库和HAL库环境下都能直接用:

#include <string.h> #include <stdint.h> /* 将float按AB CD模式拆分到两个寄存器 */ void float_to_regs_abcd(float value, uint16_t *reg_high, uint16_t *reg_low) { uint32_t tmp = 0; memcpy(&tmp, &value, sizeof(tmp)); *reg_high = (uint16_t)((tmp >> 16) & 0xFFFF); *reg_low = (uint16_t)(tmp & 0xFFFF); } /* 将两个寄存器按AB CD模式合并为float */ float regs_to_float_abcd(uint16_t reg_high, uint16_t reg_low) { uint32_t tmp = 0; tmp |= ((uint32_t)reg_high << 16); tmp |= (uint32_t)reg_low; float value = 0.0f; memcpy(&value, &tmp, sizeof(value)); return value; } /* 将float按CD AB模式拆分到两个寄存器 */ void float_to_regs_cdab(float value, uint16_t *reg_low, uint16_t *reg_high) { uint32_t tmp = 0; memcpy(&tmp, &value, sizeof(tmp)); *reg_high = (uint16_t)((tmp >> 16) & 0xFFFF); *reg_low = (uint16_t)(tmp & 0xFFFF); } /* 将两个寄存器按CD AB模式合并为float */ float regs_to_float_cdab(uint16_t reg_high, uint16_t reg_low) { uint32_t tmp = 0; tmp |= ((uint32_t)reg_high << 16); tmp |= (uint32_t)reg_low; float value = 0.0f; memcpy(&value, &tmp, sizeof(value)); return value; }

抱歉,上面CD AB版本写重复了。其实核心区别只在合并和拆分时哪个reg放高位、哪个放低位。下面这个写法更清晰:

void float_to_regs_cdab(float value, uint16_t *reg1, uint16_t *reg2) { uint32_t tmp = 0; memcpy(&tmp, &value, sizeof(tmp)); /* CD AB的意思:寄存器1对应uint32_t的低16位,寄存器2对应高16位 */ *reg1 = (uint16_t)(tmp & 0xFFFF); *reg2 = (uint16_t)((tmp >> 16) & 0xFFFF); } float regs_to_float_cdab(uint16_t reg1, uint16_t reg2) { uint32_t tmp = 0; tmp |= ((uint32_t)reg2 << 16); tmp |= (uint32_t)reg1; float value = 0.0f; memcpy(&value, &tmp, sizeof(value)); return value; }

这样,AB CD和CD AB两个模式就都覆盖了。实际使用时,在代码里加一个宏或者配置项:

#define FLOAT_MODE_AB_CD 1 #define FLOAT_MODE_CD_AB 2 #define FLOAT_MODE FLOAT_MODE_AB_CD

上层代码统一调用:

#if FLOAT_MODE == FLOAT_MODE_AB_CD #define float_to_regs float_to_regs_abcd #define regs_to_float regs_to_float_abcd #else #define float_to_regs float_to_regs_cdab #define regs_to_float regs_to_float_cdab #endif

这样一来,所有业务代码都只需要调用float_to_regs和regs_to_float两个接口,不需要关心内部到底怎么排布。移植到另一台不同字节序的设备上时,只需要改FLOAT_MODE这一个宏,省心很多。

4.3 不看文档也能确定字节序的野路子

有些国产设备说明书是真的“偷懒”,寄存器表里就写“Data(4字节)”,根本不告诉你是AB CD还是CD AB。这种时候,用下面的野路子最快:

  1. 用Modbus Poll或者串口调试助手,向从机写一个已知的float值,比如25.5(十六进制0x41CC0000)。
  2. 读回原始寄存器值,看两个寄存器分别是啥。
  3. 如果读回reg1=0x41CC、reg2=0x0000,就是AB CD;如果reg1=0x0000、reg2=0x41CC,就是CD AB。

如果从机不支持写单个寄存器,那就换个思路:从机里一般都有个“设备地址寄存器”或者“波特率寄存器”,这些是整型数据。如果是16位整数,本身就只占一个寄存器,不存在字节序问题。但如果是32位整数(比如累计流量),它的排布模式和float很像,也可以用同样的方法试探。先写入一个已知值,读回来看看顺序。

这个方法我在至少5种不同品牌的仪表上验证过,基本都能在5分钟内确定字节序。

5. 调试过程中的经验与体会

5.1 旋转开关方案在强干扰环境下的实测效果

旋转开关用ADC方案最怕的是继电器频繁通断的强电磁干扰环境。我实测下来,输出的是平均值+中值滤波组合,效果比单纯平均值要好很多。如果MCU自带硬件过采样(比如G0系列的硬件过采样),也可以打开,能进一步压低噪声。

另外有个体验:旋钮的机械结构会影响读数稳定性。质量好一点的旋转开关触点接触电阻小,ADC读数很稳;便宜的开关会有几十毫欧的接触电阻波动,虽然不至于改变档位判定,但在临界位置时输出值会有跳变。所以硬件选型时,旋钮尽量选镀金触点的。

5.2 Modbus浮点解析的偶发错值怎么排查

如果通信过程中偶尔出现一次浮点偏差,优先检查:

  1. 波特率是否匹配,尤其是带自动波特率检测的从机,偶尔会锁定错波特率。
  2. 总线是否有终端电阻。RS485总线两端需要120Ω终端电阻,如果只有一个设备,终端电阻位置不对也容易出现回波干扰。
  3. 从机内部是否对float做了字节交换(有的从机固件内部用的是小端存储,但协议层面没有统一转换)。
  4. 寄存器地址是否连续。有的设备两个float寄存器之间会穿插一个状态寄存器,读取时没有跳过,导致数据错位。

从成本和效果来看,第四点最容易被人忽略。很多国产仪表的寄存器表不是连续排列的,比如float数据占0x0001、0x0002,但0x0003是报警状态字,0x0004、0x0005才是另一个float。如果你直接用Modbus的读保持寄存器命令从0x0001开始连读4个寄存器,读到的第二位float就是错的。这是逻辑错误,不是协议错误,特别容易被误判为字节序问题。

5.3 关于到底要不要用浮点传输的讨论

如果数据范围不大、精度要求不苛刻,有一个更省心的做法——把float在发送端乘以100或者1000转成整数再发送,接收端再除以同样的系数。比如温度25.56摄氏度,就直接发2556这个uint16_t整数(或者uint32_t,如果数值更大)。这样只需要一个寄存器,不存在字节序问题,通信效率也更高。

但这种方式也有局限:动态范围大的数据(比如高精度的压力传感器读数、陀螺仪积分后的角度)就不适合定点缩放,这时候用float传输依然是更合理的选择。所以float传输必备技能,不是因为它高级,而是因为总有用到它的场景。

我个人在项目选型时,会先问一个问题:数据需要在多个设备之间互通吗?如果只是自己的主机和从机通信,那我倾向于缩放成整数传输,简单可靠。如果是和别人的设备对接,比如PLC、触摸屏,那大概率需要原生float格式,因为对方设备支持的Modbus数据模型里,float是固定格式。

5.4 最后留一个小的调试技巧

如果你用的是STM32,并且开启了串口的空闲中断,接收Modbus报文的时候要注意:Modbus RTU规定了帧间隔是3.5个字符时间,如果MCU主频很高,空闲中断的判定时间需要根据波特率重新计算。特别是波特率低于9600时,3.5个字符时间是毫秒级别的,不能用一个固定的定时器值去套所有波特率,否则帧会被拆成两截或者粘包。这个和前面的float拆分问题一样,都是“看起来简单、实际调试时容易翻车”的细节。

我自己的习惯是:把帧间隔判定封装成一个独立函数,波特率变化时自动重新计算定时器装载值。上电初始化时读取拨码开关设定的波特率,然后调用一次modbus_uart_config(),里面把帧间隔定时器一起配置好,这样就不会出现“9600正常、115200却收不全帧”的诡异问题。

写到这里,这期调试笔记想分享的两个核心点就都讲完了。一个是用ADC代替多路IO来读档位开关,另一个是搞清楚Modbus的float到底怎么放、怎么取。这两个点看似八竿子打不着,但实际做项目时往往是在同一天里踩完的坑。希望这篇笔记对同行有帮助,尤其是那些刚入行、正在为“为什么读回来的浮点数是个天文数字”而挠头的朋友。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 4:38:56

矿用安全帽灯设计制造全解析:从本质安全到皮实耐用

1. 矿用安全帽灯的设计定位&#xff1a;先搞清楚"皮实耐用"到底意味着什么做矿用照明这个行当久了&#xff0c;我越来越觉得"皮实耐用"这四个字&#xff0c;远不是随便选个厚壳子、装个亮灯珠那么简单。它背后是一整套针对极端工况的系统性设计逻辑。矿用安…

作者头像 李华
网站建设 2026/9/5 4:38:24

RAFT 实现难点——论文没告诉你的那些事

RAFT 实现难点——论文没告诉你的那些事 论文描述的是"晴天"下的规则。代码要处理的是任意时刻的崩溃、丢包、延迟和分区——任意组合&#xff0c;同时发生。 核心矛盾 论文说的是理想流程&#xff0c;实现要处理的是意外。 两者的差距就是 RAFT 实现难的地方。 六…

作者头像 李华
网站建设 2026/9/5 4:37:37

大一暑假嵌入式入门:从抄板到打样焊接的完整路线

大一暑假&#xff0c;如果你正捧着手机刷到“嵌入式硬件”这几个字&#xff0c;心里又好奇又没底&#xff0c;那我先给你一句定心话&#xff1a;这条路完全可以在一个暑假里走通&#xff0c;而且最粗暴的第一步&#xff0c;不是抱着《模拟电子技术》啃&#xff0c;而是跟着视频…

作者头像 李华
网站建设 2026/9/5 4:37:34

Unity飞行控制器实现指南:物理、状态与手感调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 4:34:43

学完STM32还是找不到工作?嵌入式开发从点灯到工程化的关键跨越

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 4:24:10

AI Agent开发实战:从插件生态到生产部署的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华