1. 为什么我建议调试前先把MODBUS的帧结构背下来搞嵌入式调试的迟早都会撞上MODBUS。上周帮同事排查一台温控表上位机一直报超时从机指示灯明明在闪查了一下午发现是RTU请求帧里CRC两个字节放反了。报文发出去看似完整实际每帧都过不了从机的校验。这个坑我记到今天因此这篇《嵌入式调试笔记7》把MODBUS从帧结构、存储区、串口工具链到从站代码按调试顺序完整过一遍。适合正在用串口助手调传感器、变频器、温控表或者准备自己写MODBUS从站固件的朋友。其实MODBUS本身并不复杂复杂的是大家在调试时习惯性跳过基础概念直接拿工具去点。工具能发出报文不代表发出的报文是正确的。很多设备手册写得也不规范寄存器地址一个“40001”能坑掉半个工作日。所以我把最容易被忽略的几个点拆开讲帧结构、数据表、功能码、CRC顺序这些对齐了后面的调试才有意义。1.1 主从模型不是所有设备都该主动说话MODBUS在串行链路上是严格的主从结构。主机发起请求从机响应从机不能自己主动往总线上发包。这句话听起来像废话但实际项目里真有人把从机做成“主动上报”模式结果两个设备互相抢总线整个链路全是乱码。常见的组网是PC或PLC做主机变频器、温控表、传感器做从机。每个从机必须有唯一地址合法范围是1到247地址0是广播地址。广播时从机执行命令但不回复比如广播写多个寄存器可以实现同步启停但调试阶段尽量不要用广播否则你根本分不清报文到底有没有被执行。调试时要先确认一件事你手上的工具是“主机侧工具”还是“从机侧工具”。串口调试助手是裸的主机因为它只会按你的字节流往外发Modbus Poll是标准主机模拟器Modbus Slave是标准从机模拟器。拿Modbus Slave当从机再用另外一个串口工具发报文是验证设备侧协议最简单的方式。这个后面专门讲。1.2 RTU帧结构地址、功能码、数据和CRC字节顺序别想当然工程里90%以上是MODBUS RTU帧结构非常固定字段长度说明从机地址1字节目标从机地址0为广播功能码1字节执行什么操作数据N字节寄存器地址、数量、写值等CRC162字节低字节在前高字节在后以读取从机1的保持寄存器为例请求帧是01 03 00 00 00 01 84 0A对应的含义是地址01功能码03起始寄存器地址0x0000寄存器数量0x0001CRC校验值0x0A84。关键点在这里CRC计算出来是0x0A84但发送时必须先发低字节0x84再发高字节0x0A。很多新手直接按大端方式发成0A 84结果设备收到后校验失败根本不会响应。我之前那次调试就是栽在这个顺序上。MODBUS还有一种ASCII模式帧以冒号开头、以回车换行结尾校验用LRC而不是CRC。ASCII模式在老旧设备上还能见到调试频率已经很低。新手如果看设备手册写着“ASCII模式”第一件事不是急着发报文而是先把字符转换规则确认好否则很容易把十六进制字符和纯文本搞混。1.3 四张数据表与地址偏移40001到底对应报文里的哪个地址MODBUS把数据分成四张表线圈、离散输入、输入寄存器、保持寄存器。线圈和离散输入都是bit级数据输入寄存器和保持寄存器都是16位寄存器。区别只有一个线圈和保持寄存器可读可写离散输入和输入寄存器只读。数据表位宽功能常见PLC地址对应功能码线圈1 bit读写00001起0x01, 0x05, 0x0F离散输入1 bit只读10001起0x02输入寄存器16 bit只读30001起0x04保持寄存器16 bit读写40001起0x03, 0x06, 0x10最大的坑就在地址偏移上。协议报文里的寄存器地址是0开始的偏移量而设备手册经常用PLC风格的“40001”来表示“第一个保持寄存器”。也就是说手册写40001报文里起始地址应该填0x0000手册写40002报文里才填0x0001。我见过不少人看到手册写“温度寄存器40001”然后往报文里填00 01结果读到的是40002也就是第二个寄存器数据看起来像一个乱码排查半天还以为是大小端问题。其实只是地址错了1。这个“1”的陷阱在之后从站代码实现里同样存在我专门放在第4章再讲。1.4 常用功能码速查表功能码不需要全背但下面这几类必须形成条件反射功能码名称典型用途0x01读线圈读继电器输出状态0x02读离散输入读开关量输入0x03读保持寄存器读温湿度、压力、频率等参数0x04读输入寄存器读只读测量值0x05写单个线圈单路继电器开/关0x06写单个保持寄存器修改单个设定值0x0F写多个线圈同时控制多路输出0x10写多个保持寄存器批量修改参数调试中0x03、0x06、0x10用的最多。0x03读保持寄存器能覆盖大多数参数读取需求0x06适合改一个设定值0x10适合批量下发参数。0x04读输入寄存器也常遇到但整体思路和0x03一样只是数据来源不同。2. 把调试链路通起来串口参数、接线和工具选型很多MODBUS调试问题根本不是协议问题而是物理链路和工具配置问题。从“电脑发一个字节”到“设备正确收到”中间隔着USB转串口、电平转换、线缆、串口参数好几层。任何一层不对都会表现为无响应或乱码。因此环境准备是调试中最枯燥但最关键的一步。2.1 物理层先搞对RS232/RS485/TTL以及A/B线MODBUS RTU可以跑在RS232、RS485、TTL上但工业现场基本是RS485。RS485是差分信号抗干扰强支持一主多从。RS232只能点对点TTL一般只在板级调试时用不能接长线。接RS485时最常见的就是A/B接反。A和B在不少设备上的丝印并不统一有些标A/B有些标D/D-还有些标485/485-。接反后最典型的现象是完全没有响应或者偶尔能收到乱码。不要纠结理论上的A是正还是负直接对调A/B试一次通常马上见分晓。真正的工业总线还要注意屏蔽层单端接地但短距离调试时先把A/B接对更重要。如果直接调试MCU板子上的TTL串口要注意电平是3.3V还是5V。USB转TTL模块和MCU之间必须共地否则通信会时好时坏。很多STM32板子用USB口供电又用另一个USB转TTL插到PC这时候两个USB的地如果电位不一致就容易出奇怪问题。调试时尽量让设备、转换器、PC共用一个电源系统地。2.2 串口参数为什么RTU几乎一定是8N1MODBUS RTU是二进制协议每个字节必须完整传输8个数据位所以数据位必须是8。校验位可选无、奇、偶停止位可选1或2。最常见的组合是9600 8N1也就是波特率9600、8个数据位、无校验、1个停止位。但这不是绝对的很多设备出厂是9600 8N1也有19200、38400甚至115200的必须看设备手册。假如你连续发了合法报文但设备不应答先怀疑波特率别一上来就分析协议。这里有一个细节串口调试助手显示的是“HEX”和“文本”实际发送的是字节流。有些助手在文本模式下会自动给你加回车换行如果你用文本模式发送十六进制字符串设备会多收到0D 0A或者空格字符帧直接废掉。所以调MODBUS必须用HEX模式发送时确认没有额外的不可见字符。2.3 工具组合裸报文用SSCOM联调用Modbus Poll/Slave我调试MODBUS一般备三样工具SSCOM串口调试助手、Modbus Poll、Modbus Slave。SSCOM用来发原始字节流适合验证CRC、帧格式和协议细节Modbus Poll用来做标准主机调试Modbus Slave用来模拟从机测试自己的主机程序。SSCOM这类串口助手是“数据盲发工具”它不管MODBUS帧格式只负责把一串字节发出去。所以用它调MODBUS你必须自己能构造完整的RTU帧CRC也得自己算。不会算没关系网上很多CRC16/MODBUS在线计算器或者直接用后面的C代码自己写一个。重点是把“请求帧长什么样”掌握住否则连设备为什么沉默都猜不出来。Modbus Poll和Modbus Slave则不需要手工算CRC。Modbus Poll设置从机地址、功能码、起始地址、数量软件自动组帧Modbus Slave则模拟一个从机你可以在里面填寄存器数值然后观察主机请求。这两个工具配合能在不接真实设备的情况下完成很多联调验证特别适合写主机程序但硬件还没到位的场景。2.4 先做回环测试确认串口链路是干净的每次插上新的USB转串口设备我做的第一件事不是接MODBUS而是回环测试。对USB转TTL和RS232设备把TX和RX短接用串口助手随便发一串HEX数据如果能在接收区原样看到说明电脑到转换器的收发通路是通的。对RS485设备回环稍微麻烦一点。因为RS485是半双工差分总线自发自收不一定像TTL那样直接回显。最简单的办法是拿两个USB转RS485模块对连A接A、B接B一个口发数据另一个口收数据。如果两边的助手都能正确收发说明链路基本没问题这时候再接真实设备去调MODBUS就能把“电脑串口问题”排除掉。3. 实战读一个保持寄存器从请求帧到异常码的完整复盘基础说完了下面做一个完整的调试实例。这个场景我经常遇到也是很多人的第一个MODBUS实战读取温控表当前温度。假设从机地址是1波特率96008N1温度值存放在第一个保持寄存器设备手册上写的是40001。3.1 场景与寄存器说明设备手册写“40001”时该怎么发地址先说地址怎么换算。设备手册写40001表示第一个保持寄存器。在MODBUS PDU报文里寄存器地址是从0开始的偏移量所以40001对应报文地址0x0000。如果手册写40002报文里才是0x0001。写多个寄存器时也是同样的规律从哪个寄存器开始读就先把手册地址减1再转成十六进制这就是我在前面说的“1”的陷阱。这个场景里起始地址就是0x0000。数量读1个寄存器就够了对应一个16位温度值。如果你的温度是32位浮点数或者带符号数很可能要连续读两个寄存器后面再细说。3.2 手工构造RTU请求帧01 03 00 00 00 01 84 0A读取保持寄存器的功能码是0x03。请求帧的组成如下从机地址0x01功能码0x03起始地址高字节0x00起始地址低字节0x00寄存器数量高字节0x00寄存器数量低字节0x01CRC低字节由CRC算法算出后低字节在前CRC高字节后发高字节对前面的6个字节01 03 00 00 00 01做CRC16/MODBUS计算得到CRC值0x0A84发送时低字节0x84在前高字节0x0A在后。所以完整帧是01 03 00 00 00 01 84 0A在SSCOM里选择HEX模式波特率选9600数据位8、校验位None、停止位1把这个字符串填进发送区点发送。如果设备正常应该会在接收区看到类似这样的响应01 03 02 00 64 B9 AF解释一下0x01是从机地址0x03是功能码0x02是后面数据的字节数0x00 0x64是温度寄存器的高低位B9 AF是CRC。0x0064换算成十进制是100相当于当前温度100具体温度单位看设备手册。请注意MODBUS寄存器默认是大端传输也就是高字节在前低字节在后解析的时候要先把高字节左移8位再和低字节做或运算。3.3 响应帧解析正常响应、异常响应与常见故障码如果设备返回的帧以0x83开头而不是0x03说明设备返回了异常响应。0x83等于0x80加上功能码0x03最高位置1表示这是一个异常响应。后面紧跟一个异常码常见的有异常码含义常见原因0x01非法功能码设备不支持该功能0x02非法数据地址寄存器地址越界或偏移算错0x03非法数据值写入值超出范围0x04从机设备故障内部错误比如你收到01 83 02 ...功能码0x83、异常码0x02典型原因是寄存器地址发错了。如果手册写40001你填了0x0001设备认为你在读第二个寄存器而这个寄存器可能不存在于是回异常码0x02。这时候回去检查起始地址把地址减1再试。如果异常码是0x03常见于写寄存器时数值超范围或者写入长度不对。3.4 一次温度值“负了”的排查大小端和数据字节序MODBUS协议规定16位寄存器数据发送时高字节在前、低字节在后但这不代表每个设备都严格遵守。尤其是一些国产仪表可能给出“高低字交换”或“低字节在前”的非标实现。实际解析时不要只看协议要看设备手册里对数据格式的说明。有个典型问题温度是带符号数比如-25度用16位有符号数表示是0xFFE7。按大端收到的是FF E7按有符号int16解析就是-25。如果你忽略了符号位直接当作无符号数会得到65511。很多调友看到负数温度变成大正数第一反应是“寄存器地址不对”其实只是缺少符号类型转换。C语言里直接用int16_t强转即可但前提是你知道这个数据是有符号的。另一个坑是有的设备会把16位数据拆成两个寄存器保存高16位在一个寄存器、低16位在另一个寄存器这种32位数据还需要确认字顺序是“高字在前”还是“低字在前”不同厂家差别很大。调试这类数据时不要猜先给设备写入一个确定值再看接收到的原始字节用实测数据确定字节序。4. 嵌入式从站实现不难难在把状态机写稳如果你不是用现成协议栈而是要在MCU上自己写一个MODBUS从站前面说的帧结构理解就派上用场了。从站代码的关键三件事CRC计算、UART接收状态机、地址映射。任何一件没做稳都会表现为“时好时坏”或“忽然死掉”。4.1 CRC16-MODBUS的C实现与验证方法CRC计算是MODBUS从站最容易出低级错误的地方。CRC16/MODBUS的初始值是0xFFFF多项式是0x8005的反射形式0xA001。我惯用的实现如下uint16_t modbus_crc16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *buf; for (uint8_t i 0; i 8; i) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }验证方法很简单对请求帧01 03 00 00 00 01调用这个函数返回值必须是0x0A84。收到完整RTU帧后需要比较的是“收到的CRC”和“对帧中除CRC外所有字节重新计算的CRC”。注意收到的CRC在帧里是低字节在前处理时要把第一个CRC字节作为低8位第二个CRC字节作为高8位否则又会被字节序坑一遍。uint16_t recv_crc rx_buf[len - 2] | (rx_buf[len - 1] 8); uint16_t calc_crc modbus_crc16(rx_buf, len - 2); if (recv_crc ! calc_crc) { /* 丢弃该帧 */ }4.2 UART接收状态机帧间隔3.5字符时间和分包处理MODBUS RTU没有显式的帧头帧尾靠的是“静默时间”来切分帧。字符与字符之间间隔不能超过1.5个字符时间帧结束要求有3.5个字符时间的静默。如果MCU用普通串口接收中断不能在每个字节到达后立刻解析否则会把一帧拆成好几段。常用做法是串口每收到一个字节就存入缓冲区同时让一个定时器重新计时当定时器超时达到3.5个字符时间就认为一帧接收完毕再去解析缓冲区里的整帧数据。3.5字符时间怎么算8N1格式下一个字符占10个bit9600波特率下就是3.5 * 10 / 9600 ≈ 3.646 ms我一般留一定余量9600下用5ms115200下用1ms左右。如果你用DMA串口空闲中断思路是一样的收到空闲中断时DMA已经攒了一帧再进解析函数。这里最忌讳的是在串口中断里做CRC计算和复杂的地址映射开销太大容易造成后续字节丢失。正确做法是快速把数据存入环形缓冲区等整帧超时后在主循环里处理。4.3 地址映射的“1”陷阱寄存器索引别直接拿报文地址用从站固件里寄存器通常存在一个数组里比如holding_regs[100]。外部请求的报文地址是0x0000对应的数组索引是0也就是“第一个保持寄存器”。如果你在代码里直接拿reg_addr当数组下标那报文地址0x0001会访问到holding_regs[1]对应的是第二个保持寄存器。这个行为在外面看来就是“寄存器地址整体偏移了1位”。更麻烦的是有些用户在设备手册里用40001这样的PLC地址到了配置界面里又填了一个40001工具端可能自动减1也可能不减。不同软件处理方式还不一样。我的建议是从站固件内部统一用协议地址0开始的偏移量对外文档里明确写“40001对应协议地址0x0000”。收到报文后先把请求中的寄存器地址转换成你数组里的物理索引越界检查也基于这个索引做。这样能避免大多数地址错位问题。4.4 用第二路串口打日志比任何仿真器都实在MCU上没有操作系统很多协议问题你没法直接看到“收到了什么”。我强烈建议从站固件预留一个调试串口把接收到的原始字节、解析后的功能码、CRC校验结果、异常码全部print出来。这样设备在现场出问题时只要接上调试串口就能看到完整过程。比如你看到日志里CRC校验一直失败但总线波形看着正常那大概率是收到的字节本身被干扰了或者是波特率不匹配导致字节错位。如果日志里显示CRC正确但功能码被解析成0x00那就要查你的帧超时时间是不是太短把一帧拆成了两段。第二路串口的代价很小但排查效率提升非常大。5. 总线级调试多从机、干扰、终端电阻与抓包单设备调通只是开始上了现场总线才是真正的试金石。RS485组网后经常出现“单独测每个设备都正常多接几个就乱套”的情况。这种问题的定位思路和时间顺序很有讲究。5.1 RS485组网的拓扑与终端电阻RS485总线要求手拉手菊花链拓扑也就是从主机到每个从机都是短接在主线上尽量避免星形分支更不要用很长的T型支线。很多现场为了接线方便把每个设备用很长的线并联到主线上结果信号反射严重通信速率一高就出错。终端电阻是另一个高频话题。按规范RS485总线的物理终端两端各接一个120欧匹配电阻目的是消除反射。但是在短距离几米内调试时不接终端电阻往往也能工作。真正的问题是只在一端接了120欧或者中间某个设备上把终端电阻拨上了导致总线负载异常。判断方式很简单拔掉所有从机只留主机和一个设备如果通信正常接上多个设备后出现不定时CRC错误先检查终端电阻和总线拓扑。5.2 干扰导致CRC漫天飞时的排查顺序CRC错误是最让人头疼的现象因为报文看着“大概对”但就是校验不过。遇到这种情况我按下面的顺序排查先降波特率比如从115200降到9600如果错误减少大概率是线缆质量或拓扑问题。检查A/B是否接反接反时不只是无响应还有可能数据错位导致CRC失败。检查屏蔽层和接地。屏蔽层只能在单点接地不要在多个设备上重复接地否则屏蔽层变成地环路反而引入干扰。把RS485转换器换成带隔离的型号工业现场经常因为设备间地电位差把转换器打坏或者通信异常。实在不行把同一包数据抓出来对照看是字节错位还是数据被改。这能帮助区分是物理干扰还是协议栈解析问题。5.3 用逻辑分析仪和串口助手协同抓包当主机侧工具和从机侧都看不到问题但确实现场就是不通时物理层抓包是最有力的手段。拿一个逻辑分析仪接在RS485转换器后的UART信号端或者直接跨接总线差分信号解码观察每一帧的实际电平。串口助手能看到的是“我发出去的数据”逻辑分析仪能看到的是“总线上真实传输的数据”。我最常用的是把逻辑分析仪的采样率设高一些解码UART时选择和你波特率一致的参数然后对比总线上抓到的帧和主机工具发出的帧。如果总线上抓到的请求帧已经少了字节或CRC错了说明问题在主机侧发送链路如果请求帧完全正确但设备没有响应问题就在设备侧接收或执行环节。这个“切一刀”的思路能帮你快速缩小范围省掉很多无效猜测。5.4 多从机地址冲突与固件升级后不回帧的实例多从机组网还会遇到一种隐蔽问题两个从机地址被设成了同一个。比如设备出厂默认地址都是1现场同时接了两台主机读地址1时两台设备都认为自己被点名同时往总线上发响应于是主机收到一堆乱码或者CRC错误。排查方法很简单只保留一个设备在总线上修改成不同地址再逐个接上测试。最好在项目调试开始时直接把每台设备的地址和波特率登记成表。还有一次现场调试某个从机固件升级后主机读其他设备都正常只有这台设备偶尔不回帧。后来发现是这台设备升级后上电初始化时间变长主机在设备启动完成前就开始发请求自然得不到响应。处理方式是给主机配置更大的上电延时或者在从机上电初始化完成后拉高一个IO口用示波器确认“从机已经准备好”再开始通信。这类问题没法靠协议本身解决只能靠现场日志和时间参数一点点抠。最后再分享一个我自己的习惯不管用哪款串口助手发送数据时都先用16进制逐字节核对一遍再点发送收到的数据也一律以HEX模式保存。MODBUS调试本质上是在对着字节说话你越早接受“协议就是一堆有格式的字节”就越少被玄学问题困住。希望这篇笔记能让你少踩几个CRC和地址偏移的坑。