搞嵌入式的,早晚都会碰MODBUS。工业现场、设备联网、上位机对接,这个有四十多年历史的老协议至今还是事实标准。我的调试笔记写到了第七篇,这期就把MODBUS从帧格式、CRC计算到实际抓包排错完整梳理一遍。如果你是刚接触,可以直接照着后面的步骤做;如果你已经调过,看看有没有踩过跟我一样的坑。
MODBUS听起来老,但它简单、透明、不依赖特定硬件,串口、网口、无线都能跑,所以变频器、PLC、电表、温控仪、传感器,基本都带这个口。只要你负责的是嵌入式设备与外部系统通信的活,就绕不开它。这篇笔记我会拿实际调试帧来拆解,把RTU、ASCII、TCP三种模式,主站从站的数据交互,以及CRC校验这些细节都说透;同时把我在现场踩过的坑——波特率不匹配、共地问题、寄存器偏移、方向切换时序——一并整理出来,方便你少走弯路。
1. 这篇笔记想解决什么问题
1.1 一次现场调试把我撂倒的经历
去年做一套环境监测终端,STM32采集温湿度、气压、风速,数据要上送到现场触摸屏。硬件工程师画板子时留了RS485口,触摸屏那边也只出MODBUS RTU协议。我当时觉得挺简单:把传感器数据塞进寄存器,等屏来读就行。结果联调时,触摸屏一直报“通信超时”,偶尔闪出来一个数又很快卡死。
一开始怀疑是485芯片坏了,换了一片没解决;后来怀疑接线太长,把波特率降到9600也不行;折腾一下午后发现,问题出在我把CRC校验字节高低位放反了。从站收到请求帧后校验不通过,直接静默丢弃,主站那边自然一直超时。这种问题用示波器、逻辑分析仪根本看不出来,因为你发的波形是“正确”的,只有报文内容对不上。从那以后我就养成一个习惯:凡是调MODBUS,先把帧结构、CRC、字节序,一行一行对照协议原文核实,再上电联调。
这篇笔记我想把MODBUS相关的核心知识一次性讲透,尤其是帧格式和调试工具怎么配合使用。因为很多人在学校学过MODBUS,但一到工程现场就露怯:不知道寄存器地址怎么映射,不知道异常码怎么处理,不知道RTU和TCP该选哪个。这些都是实际干活最需要的东西,也是面试官最爱问的“八股”。
1.2 MODBUS能干掉哪些通信麻烦
MODBUS之所以还活着,本质上是因为它把“通信规则”定得非常简单。主站发一条请求,从站回一条响应,一问一答,不会出现两边同时乱说的情况。对嵌入式开发来说,这种确定性意味着代码容易写、问题容易查。你不需要处理复杂的协议栈,一个串口中断加一个状态机,基本就能跑起来。
它能解决的典型问题包括:不同厂商设备之间的互联互通,比如用PLC去读第三方仪表的数值;上位机组态软件通过标准协议抓取嵌入式设备的运行状态;多台设备挂同一条485总线,靠地址区分彼此。相比I2C、SPI这些板级总线,MODBUS的优势在于传输距离远、抗干扰强、设备可扩展性好;相比CAN,它对MCU的要求更低,普通串口就能实现。
但它的“简单”也带来了一些限制。比如没有主动上报机制,从站不能自己说话;没有时间同步和错误重传机制,主站得自己管理超时和重试;数据带宽也不算高,不适合传大量实时数据。理解这些边界,选型的时候才不会选错。
2. 协议模型:一张表和三个模式
2.1 数据模型:四类对象一张表搞定
MODBUS的核心逻辑是:设备内部数据被抽象成四张表,主站通过功能码去读写这些表。理解这个模型,后面所有报文都能看懂了。
| 数据模型 | 对象类型 | 访问权限 | 对应功能码 |
|---|---|---|---|
| 离散输入(Discrete Input) | 1位 | 只读 | 0x02 |
| 线圈(Coil) | 1位 | 读写 | 0x01、0x05、0x0F |
| 输入寄存器(Input Register) | 16位 | 只读 | 0x04 |
| 保持寄存器(Holding Register) | 16位 | 读写 | 0x03、0x06、0x10 |
举个例子:一台温控仪,当前温度是模拟量输入,对应“输入寄存器”;用户设定的目标温度可以被上位机修改,对应“保持寄存器”;设备报警输出状态,对应“线圈”;门磁开关信号,对应“离散输入”。
这里有个工程上非常容易踩的坑:MODBUS的“寄存器地址”和“协议报文里的地址”通常差1。很多设备手册写的保持寄存器地址是40001,对应协议报文里的数据地址0x0000。你把40001填到报文里,从站要么回异常码0x02(非法数据地址),要么干脆没反应。所以开发前一定要先搞清楚设备手册用的是“协议地址”还是“PLC风格地址”,这个细节能救你一命。
2.2 功能码:通信的业务指令
功能码是报文的“动词”,告诉从站要做什么。实际项目里最常用的其实就是三四个,不用记全部,但常用的得烂熟于心。
| 功能码 | 功能名称 | 典型用途 |
|---|---|---|
| 0x01 | 读线圈 | 读取DO输出状态 |
| 0x02 | 读离散输入 | 读取DI输入状态 |
| 0x03 | 读保持寄存器 | 读取运行参数、设定值 |
| 0x04 | 读输入寄存器 | 读取采集的实时数据 |
| 0x05 | 写单个线圈 | 控制单路DO |
| 0x06 | 写单个寄存器 | 修改单个设定值 |
| 0x0F | 写多个线圈 | 批量控制DO |
| 0x10 | 写多个寄存器 | 批量写入设定参数 |
从站处理不了请求时,会回异常响应。异常响应有两个特征:功能码最高位置1(比如0x03变成0x83),数据域带一个异常码。常见的异常码就几个:01非法功能码、02非法数据地址、03非法数据值、04从站设备故障。调试时看到这些码,基本就能定位到问题方向。
2.3 RTU、ASCII、TCP三种模式怎么选
MODBUS分好几种传输模式,名字经常把人绕晕。其实按物理层分就两条路:串口链路和以太网链路。
串口链路上有两种编码方式:RTU模式用二进制传输,效率高,绝大多数设备都支持;ASCII模式用十六进制字符传输,一个字节拆成两个ASCII字符,效率低一半,但好处是对时序要求没那么苛刻,以前在慢速或噪声大的线路上见过有人用,现在基本被RTU取代了。
以太网链路上就是MODBUS TCP,报文里没有CRC校验,因为TCP/IP协议栈已经保证可靠性了;同时加了一个MBAP头,用来标识事务和长度。选型建议很简单:设备就在板子旁边、用串口,就用RTU;设备要跨交换机、走网络,就选TCP。485总线和MODBUS不是一回事,485只是物理层,MODBUS是应用层协议,两者可以自由搭配。
3. 帧格式逐字节拆解
3.1 RTU帧长什么样
RTU模式下,一帧报文由四部分组成:从站地址(1字节)、功能码(1字节)、数据域(N字节)、CRC校验(2字节)。以读保持寄存器为例,主站发请求:
| 字段 | 值 | 说明 |
|---|---|---|
| 从站地址 | 0x01 | 目标从站,范围1~247 |
| 功能码 | 0x03 | 读保持寄存器 |
| 起始地址 | 0x0000 | 从0号寄存器开始 |
| 寄存器数量 | 0x0002 | 连续读2个寄存器 |
| CRC低字节 | 0xC4 | CRC16低字节在前 |
| CRC高字节 | 0x0B | CRC16高字节在后 |
整帧十六进制就是:01 03 00 00 00 02 C4 0B。从站正确收到后,响应帧是:01 03 04 00 00 00 02 CRC低 CRC高。其中04是数据字节数,后面4个字节是两个寄存器的值。
RTU还有一个帧间隔要求:报文内字节与字节之间,空闲时间不能超过3.5个字符时间;超过1.5个字符时间,从站就认为上一帧不完整,直接丢弃。波特率9600下,一个字符大概是1ms多一点,所以帧间停顿至少得留4ms以上。好多人在高波特率下丢帧,就是因为帧间隔控制得不对,主站把请求分成两截发,从站只收到了半截。
3.2 CRC16计算:边算边讲原理
CRC校验是RTU模式最容易写错的地方,也是最值得花十分钟搞清楚的地方。MODBUS RTU用的是CRC16,多项式是0x8005,初始值是0xFFFF,结果在发送时低字节在前。
计算流程是:CRC寄存器先预装0xFFFF;然后对每个数据字节,先和CRC低字节异或,再右移8次,每次遇到最低位为1,就和固定值0xA001异或。
C语言实现一段标准的位运算法:
#include <stdint.h> uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; }实际项目里要求性能的话,可以用查表法,把256个CRC低字节结果预计算好,每个字节只需要一次查表和三次异或,速度快很多。但原理一样,调试时用上面的位运算法就足够。
我调试时一般会拿已知报文验证CRC函数对不对。比如01 03 00 00 00 02这6个字节,算出来CRC应该是0x0BC4,发送顺序就是C4 0B。如果算出来是0xC40B,说明字节序给弄反了。这个验证习惯强烈建议保留,能在调试早期就把最常见的坑排除掉。
3.3 MBAP头与TCP帧格式
MODBUS TCP的帧结构和RTU不一样,没有CRC,多了MBAP头。MBAP头共7字节:事务处理标识符(2字节)、协议标识符(2字节,固定0x0000)、长度(2字节)、单元标识符(1字节)。
事务标识符由主站生成,每次请求递增,用来匹配请求和响应;协议标识符固定为0表示MODBUS协议;长度字段表示从单元标识符开始到帧尾的字节数。单元标识符在网关场景下用来寻址串口后面的从站,相当于RTU里的从站地址。
举个例子,读保持寄存器的TCP请求帧:
| 字段 | 值 |
|---|---|
| 事务标识符 | 0x0001 |
| 协议标识符 | 0x0000 |
| 长度 | 0x0006(后面从单元标识符开始共6字节) |
| 单元标识符 | 0x01 |
| 功能码 | 0x03 |
| 起始地址 | 0x0000 |
| 寄存器数量 | 0x0002 |
十六进制就是:00 01 00 00 00 06 01 03 00 00 00 02。在网络调试助手里,只要发送这些字节,就能收到从站或模拟器的响应。TCP模式的调试思路通常比RTU更顺手,因为不需要考虑CRC,报文直接在Wireshark里都能抓到。
3.4 字节序、寄存器编号与数据映射
MODBUS协议本身规定:多字节数据在报文里按大端顺序传输,也就是高字节在前。但这只是“线上顺序”,不代表设备里存储的变量就是大端。
最常遇到的坑是32位数据。比如一个浮点温度值,占用两个16位寄存器,不同厂商对不同寄存器顺序的定义可能完全相反。有的设备高16位在低地址寄存器、低16位在高地址寄存器;有的反过来。所以读取时,要先把原始字节拼好,再根据设备手册确定交换顺序。我见过有人调了一天,始终觉得数据“差不多但不对”,最后发现就是32位字序没调。
另外,有些设备习惯把寄存器编号从0开始,有些从1开始。一个保持寄存器读地址如果总是差一位,很多情况下不是功能码的问题,而是“基址偏移”没处理。写程序时,应该把这类差异统一放到驱动层的宏定义或配置表里,而不是散落在业务代码里,否则后期维护会很痛苦。
4. 调试前准备:硬件连接与工具选型
4.1 RS485接线与共地问题
RS485是差分信号,用A、B两根线传数据。标准接法是A接A、B接B,但不同厂商对A/B的正负定义有时候相反,接到一起之后数据全是乱码或完全没反应,所以遇到问题先交叉试一下。
RS485总线两端需要接120欧终端电阻,用来消除信号反射。短距离(几十米内)不接一般也能工作,但距离一长、速率一高,终端电阻就必须加上。另外,RS485是半双工,同一时刻只能一个设备发数据。MCU用GPIO控制收发芯片方向时,从发送状态切换到接收状态、或者反过来,都需要预留一点延时,否则第一帧数据可能被吃掉。
共地问题在RS485调试中特别容易忽略。有些现场设备通过开关电源隔离供电,如果你和它之间没有共同参考地,即使A/B接对了也可能通信不稳定,表现为偶尔通、偶尔超时。解决办法是在某一端把信号地(GND)连起来,或者用带隔离的RS485收发芯片。
4.2 工具链:串口助手、模拟器与逻辑分析仪
调试MODBUS,工具选对了效率翻倍。我自己常用的组合是:
- 串口调试助手(SSCOM或类似的):直接看十六进制报文,适合主站和从站单独验证。
- Modbus Poll(主站模拟):配合Modbus Slave(从站模拟),可以在没有真实设备的情况下把协议逻辑跑通。
- 逻辑分析仪:抓RS485线上波形,看帧间隔、电平是否正常,查物理层问题。
- Wireshark:调试MODBUS TCP时必备,抓包能看到完整的MBAP头和数据域。
串口调试助手怎么用呢?最简单的方式:如果你是做从站,先用串口助手把主站要发的请求帧原样发出去,看你的从站能不能正确回帧;如果你是做主站,先用串口助手模拟从站,把固定响应帧发给你写的主机程序,看它能不能正确解析。这种方式绕开了物理层和时序问题,能快速验证协议栈逻辑。
4.3 串口参数和常见波特率
MODBUS RTU在串口上的默认配置一般是8数据位、无校验、1停止位(8N1),波特率常见9600、19200、115200。设备若支持校验位(如偶校验),参数就是8E1,通信双方必须完全一致。
波特率不匹配的症状很典型:收到的全是乱码,或者从站完全没有响应。但是有一点容易被忽略:有些设备支持自动波特率检测,上电初期会有一段学习过程,如果这期间你在发报文,它可能会忽略前面若干帧,导致你误判为“通信失败”。
判断波特率是否匹配,最直接的办法是把从站回的数据或主站发的数据用逻辑分析仪抓下来,看波形上每个字节的时间宽度,和理论波特率对照。这个方法在没有任何文档的情况下特别有用,能直接“猜”出设备的真实波特率。
5. 手写RTU主机:代码实现与抓包验证
5.1 最小工程框架
为了把协议逻辑讲清楚,我在这里用C语言写一个最简的RTU主站框架,运行在Linux环境下,通过USB转485适配器访问从站。如果你用的是STM32,移植思路完全一样:把读写串口的函数替换成你的UART驱动,把延时函数换成硬件定时器。
#include <stdio.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> static int serial_fd; // 打开串口并设置8N1 static int serial_open(const char *dev, int baud) { serial_fd = open(dev, O_RDWR | O_NOCTTY); if (serial_fd < 0) return -1; struct termios tty; memset(&tty, 0, sizeof(tty)); cfsetospeed(&tty, baud); cfsetispeed(&tty, baud); tty.c_cflag = CS8 | CLOCAL | CREAD; tty.c_iflag = 0; tty.c_oflag = 0; tty.c_lflag = 0; tcsetattr(serial_fd, TCSANOW, &tty); return 0; } static int serial_write(const uint8_t *buf, int len) { return write(serial_fd, buf, len); } static int serial_read(uint8_t *buf, int len, int timeout_ms) { // 实际工程中用select或poll实现超时读取 // 这里简化为直接read return read(serial_fd, buf, len); }这个框架里,串口配置是8N1、无流控;实际使用中需要根据设备手册改成8E1或8O1,记得把tty.c_cflag里的校验位设置同步修改。超时读取我用了注释占位,因为select/poll的代码量稍大,但原理不复杂:设置一个超时时间,等待文件描述符可读,超时则返回0。
5.2 发一帧读请求:从构建到CRC
核心函数就是构建请求帧并发送。以读取从站1的保持寄存器0x0000和0x0001为例:
uint16_t modbus_crc16(const uint8_t *data, uint16_t len); int modbus_read_holding_registers(uint8_t slave_addr, uint16_t start_addr, uint16_t quantity) { uint8_t frame[8]; frame[0] = slave_addr; frame[1] = 0x03; // 功能码:读保持寄存器 frame[2] = (start_addr >> 8) & 0xFF; frame[3] = start_addr & 0xFF; frame[4] = (quantity >> 8) & 0xFF; frame[5] = quantity & 0xFF; uint16_t crc = modbus_crc16(frame, 6); frame[6] = crc & 0xFF; // CRC低字节在前 frame[7] = (crc >> 8) & 0xFF; return serial_write(frame, 8); }这一段代码里最容易写错的是CRC字节序。很多人直接把modbus_crc16返回的uint16_t强转成两个字节发出去,结果高低位颠倒。记住:MODBUS RTU的CRC传输顺序是低字节在前,所以frame[6] = crc & 0xFF; frame[7] = crc >> 8;,这个顺序一旦搞反,整个帧都会被从站丢弃。
5.3 用串口助手对拍抓包
代码写完先别急着接真从站,先用串口助手做一次“协议对拍”,确认自己发出的帧是协议标准要求的。操作步骤如下:
- 把USB转485适配器插到电脑,打开串口调试助手,选择对应串口,波特率设9600,8N1。
- 打开十六进制显示,然后手动发送:
01 03 00 00 00 02 C4 0B。 - 如果对面有一个正常的从站,它应该回一帧以
01 03 04开头的数据;如果没有从站,串口助手里没有任何回复,但你自己发的数据应该原样显示在接收区(前提是开启了“本地回显”)。 - 再运行自己写的程序,发送同样的报文,用逻辑分析仪或者把485转成TTL接串口助手的RX,确认程序真的发出了
01 03 00 00 00 02 C4 0B这8个字节。
为什么这步很重要?因为它把“程序逻辑正确性”和“物理链路正常性”分开验证。如果程序发的帧在串口助手里显示完全正确,再接到从站上还不行,那问题就在物理链路、从站配置或时序上,排查范围一下缩小了很多。
5.4 解析从站响应
从站响应帧格式是:地址(1字节)+ 功能码(1字节)+ 字节数(1字节)+ 数据(N字节)+ CRC(2字节)。比如响应是:01 03 04 00 00 00 02 79 C6,其中字节数04表示后面有4个数据字节,也就是两个寄存器的值:0x0000和0x0002。
解析代码需要做三件事:校验从站地址没被别人占用;对收到的帧重新计算CRC并与帧尾CRC比对;检查功能码最高位是否为1,若是1则读异常码。
int modbus_parse_response(uint8_t *buf, int len, uint8_t *data_out) { if (len < 5) return -1; // 至少 地址+功能码+长度+CRC低+CRC高 uint16_t crc_rx = buf[len - 1] << 8 | buf[len - 2]; uint16_t crc_calc = modbus_crc16(buf, len - 2); if (crc_rx != crc_calc) return -2; // CRC错误 if (buf[1] & 0x80) return -3; // 异常响应 int data_len = buf[2]; if (data_len + 5 != len) return -4; // 长度不匹配 memcpy(data_out, &buf[3], data_len); return data_len; }这里有个细节:CRC校验要覆盖“地址+功能码+数据域”,不含CRC本身,所以计算时传入长度是len - 2。代码里我把接收帧的CRC拼接成buf[len-1] << 8 | buf[len-2],因为收到时低字节在前、高字节在后,拼回uint16_t时要恢复成大端形式。
6. 调试实战:三种典型故障排查实录
6.1 完全无数据:先查电平和接线
现象是主站发请求后,从站毫无反应,逻辑分析仪上看不到任何波形。这种时候优先查物理层,而不是协议层。
第一步检查接线:A/B是否接反、GND是否共地。第二步检查供电:很多RS485收发芯片的VCC引脚需要3.3V或5V电源,如果设备由电池供电且电压跌落,收发器可能进入不确定状态。第三步查终端电阻:总线两端是否各有一个120欧电阻,没有的话在长距离传输时信号反射会非常严重。第四步用万用表量A/B之间的电压:空闲状态下,A相对B应为正,大约2V到5V;如果测出来是负的,说明A/B定义接反了。
有时候还会遇到设备地址不对导致的不响应。比如主机发地址01,但是从站地址设成了02,这属于配置问题,物理层完全正常,但主站永远等不到应答。所以排查无数据问题时,地址、波特率、校验位这些配置项也要一并过一遍。
6.2 有请求无响应:超时问题的排查链
现象是主站明明发了数据,从站却没有任何应答。这类问题的排查顺序我总结成一个链条:物理层→配置→帧内容→时序。
物理层确认正常后,先看波特率:把逻辑分析仪抓到的波形展开,数一个数据位的宽度,和理论波特率对比。再看从站地址:请求帧第一个字节必须和从站地址一致。然后查功能码:有些从站只实现了03和04,你用01去读,它会回异常码,或者因为没实现该功能码直接丢弃。最后看CRC和帧间隔:用串口助手把请求帧原样发给从站,看它有没有响应;如果原样发送有响应,而程序发没有,多半是程序发送时字节间间隔太长,超过了RTU的3.5字符帧间隔要求。
帧间隔这个坑特别隐蔽。我遇到过用DMA发送时一切正常,但改用中断逐字节发送后,从站偶尔不应答。排查发现中断发送时字节间间隔在负载高时被拉长到了几毫秒,从站把一帧拆成了多帧,自然不响应。解决办法是用DMA或把整帧放在一个串口环形缓冲区里连续发送。
6.3 数据对不上:大小端与寄存器偏移
能通、但数据不对,这种问题在逻辑分析仪上是最难发现的,因为报文格式完全正确,只是数据值超出预期。
我遇到过的典型案例:读温控仪的当前温度,返回寄存器值是0x00 0x64,十进制100,但仪表显示25摄氏度。查手册发现,该仪表的温度数据是用有符号数表示的,而且分辨率是0.1℃,实际值应该是100 * 0.1 = 10.0℃;再仔细看发现寄存器编号还差了一位,我读的是0x0000,但手册里温度从0x0001开始。两个问题叠加,才导致读数看起来“差了很多”。
处理这种问题要养成的习惯是:先把原始寄存器值用十六进制打印出来,再做数据类型转换。因为转换之前的原始值最接近协议真相,一旦提前做了乘除、偏移,反推就很难了。我的做法是在驱动层解析出原始值,业务层才做工程量和物理量的换算,这样每层出问题都能快速定位。
6.4 用虚拟串口做联调
没有真实设备时,可以用虚拟串口工具配合从站模拟器做联调。原理是:虚拟串口工具创建一对虚拟串口,比如COM5和COM6,这两个串口被背靠背连接起来;你的主站程序打开COM5,Modbus Slave模拟器监听COM6,数据就在虚拟串口之间传输。
我的联调步骤是:
- 创建虚拟串口对,比如COM5和COM6。
- 打开Modbus Slave,选择从站地址1,配置保持寄存器区域,设好起始地址和数量。
- 用串口调试助手打开COM6,手动发
01 03 00 00 00 02 C4 0B,正常情况下Modbus Slave会回数据。 - 把主站程序的串口指向COM5,运行后看Modbus Slave里的收发统计是否增加。
- 如果主站程序解析到的数据不对,用串口调试助手同时挂在COM5上抓包,看主站发出和收到的原始帧。
这套方法的好处是不需要任何硬件,纯软件联调就能把协议栈逻辑跑通。等真实设备到了,再把串口切换到物理口,剩下的就只是物理层验证了。
7. 常见问题速查表与避坑心得
7.1 常见问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无响应 | A/B接反、没共地、从站地址错误 | 万用表量A/B电压,检查地址配置 |
| 收到乱码 | 波特率不匹配、串口参数不一致 | 逻辑分析仪掐波形,数位宽 |
| 偶发超时 | 帧间隔过长、没接终端电阻、噪声干扰 | 检查发送时序,加终端电阻 |
| 一直报异常码02 | 寄存器地址越界或编号偏移 | 读设备手册,确认起始地址 |
| 数据值明显不对 | 字节序、数据类型、比例因子弄错 | 先打印原始寄存器值再换算 |
| 主站能通但从站间互斥 | 没有做RS485方向切换延时 | 收发方向切换后加延时 |
这里想特别强调:“偶发超时”很多时候不是协议层问题,而是电源或干扰问题。有次我把两条485线跟动力线绑在一个线槽里,设备总是几分钟掉一次线,换成屏蔽双绞线并单端接地后,故障就消失了。工业现场布线规范真的不是玄学。
7.2 几条实测避坑经验
第一,调MODBUS之前,先把设备手册的“寄存器地址映射表”完整看一遍,重点看起始地址、数据类型、大小端、比例因子。很多人连设备支持哪些功能码都没确认,上来就按通用方式读,结果折腾半天。
第二,所有报文验证都从“原样发送确认帧”开始。主站代码里第一步就是发一帧标准请求,验证发出内容和协议完全一致;从站代码里第一步就是接收一帧标准请求,验证能正确回帧。这两个基准点对了,后续问题都好查。
第三,异常码是很有用的调试入口。从站回0x83 0x02,说明它收到了你的请求而且地址正确,只是寄存器地址你给错了;回0x83 0x01,说明功能码它不支持。见到异常码别慌,它是在告诉你“我已经收到你了,但我不接受这个请求”。
第四,保存一份“标准报文集”。把常用操作的标准帧(读03、读04、写06、写10)和对应响应帧记录下来,放在工程doc目录下。每次换设备、换平台,先拿这套标准帧验证新环境,能在一分钟内判断出是协议栈问题还是设备问题。
最后再分享一个小技巧:做主站时,每次请求都打印原始收发帧,包括CRC。别怕打印内容多,调试阶段信息越多越好。等系统稳定了再关掉打印,也不迟。调试MODBUS跟破案一样,现场信息留存得越完整,你越容易找到真凶。