3. Modbus RTU帧结构:报文格式详解
好,咱们今天来聊聊Modbus RTU最核心的东西——报文格式。说白了,就是设备之间到底怎么“说话”的。我刚开始接触这个协议的时候,看着那一串十六进制数,说实话有点懵。但后来摸透了规律,发现其实特别简单。
3.1 报文的基本骨架
一个完整的Modbus RTU报文,就像一封信。它由四个部分组成:
- 地址码(1字节)—— 这信是写给谁的
- 功能码(1字节)—— 你要对方干什么
- 数据域(N字节)—— 具体的内容或参数
- CRC校验(2字节)—— 确保信没被篡改
你看,就这么简单。我当年在调试一个污水处理项目时,发现PLC和仪表怎么都对不上话。折腾了半天,最后发现是地址码写错了。嗯,这种低级错误,谁还没犯过呢?
3.2 地址码:设备的“身份证”
地址码占1个字节,取值范围是1到247。0是广播地址,248到255保留不用。
关键点:每个从站设备必须有唯一的地址。主站发报文时,地址码告诉总线:“我在找谁”。从站收到后,一看地址不是自己的,直接忽略。
我曾经在一个项目中,现场有30多个仪表,地址从1排到30。结果有个同事图省事,把两个仪表都设成了5。你想想看,主站发指令给5号设备,两个设备同时响应,总线直接乱套。所以我的建议是:设备地址一定要做好台账,最好用标签贴在设备上。
3.3 功能码:告诉设备做什么
功能码也是1个字节。常用的就那么几个:
| 功能码 | 含义 | 我常用的场景 |
|---|---|---|
| 0x01 | 读取线圈状态 | 控制继电器、指示灯 |
| 0x02 | 读取离散输入 | 读取按钮、限位开关 |
| 0x03 | 读取保持寄存器 | 读取传感器数值、设定参数 |
| 0x04 | 读取输入寄存器 | 读取模拟量输入(如4-20mA) |
| 0x05 | 写单个线圈 | 控制单个开关 |
| 0x06 | 写单个寄存器 | 修改单个参数 |
| 0x0F | 写多个线圈 | 批量控制输出 |
| 0x10 | 写多个寄存器 | 批量修改参数 |
这里有个小技巧:如果从站返回的功能码最高位是1(比如0x83),说明出错了。这是Modbus的异常响应机制。我刚开始做项目时,看到返回0x83还以为是设备坏了,后来才知道是功能码不支持。
3.4 数据域:真正的“干货”
数据域的长度是可变的,从0字节到252字节不等。它具体包含什么,取决于功能码。
举个例子,读取保持寄存器(功能码0x03):
- 主站发送:地址码 + 0x03 + 起始地址(2字节)+ 寄存器数量(2字节)+ CRC
- 从站响应:地址码 + 0x03 + 数据字节数(1字节)+ 数据(N字节)+ CRC
我习惯用实际报文来说明。假设我们要读取1号从站,从地址0x0000开始,读2个寄存器:
主站发送:01 03 00 00 00 02 C4 0B
从站响应:01 03 04 00 0A 00 14 3A 86
你看,从站返回了4个字节的数据(0x000A和0x0014),也就是十进制的10和20。是不是很直观?
我的经验:调试时最好用串口助手抓一下报文。很多问题一看原始数据就明白了。比如数据全是0xFF,那多半是通信线接反了。
3.5 CRC校验:通信的“安全锁”
CRC校验占2个字节,放在报文的最后。它是对地址码、功能码、数据域进行循环冗余校验的结果。
计算CRC的步骤:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC寄存器的低8位异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复步骤3共8次
- 处理完所有字节后,得到最终的CRC值
说实话,我从来不自己手算CRC。都是直接用现成的函数库。但你要知道它的原理——CRC校验能检测出99.99%以上的传输错误。
注意:CRC在报文中的字节序是低字节在前,高字节在后。比如CRC值是0xC40B,发送时先发0x0B,再发0xC4。这个顺序搞反了,通信肯定失败。我曾经因为这个坑,排查了整整一个下午。
3.6 字节序与传输规则
Modbus RTU的字节序遵循“小端模式”。什么意思呢?就是多字节数据,低字节在前,高字节在后。
举个例子,寄存器值0x1234,在报文中的顺序是:
先发 0x34,再发 0x12
而不是我们习惯的0x12、0x34。这一点特别容易搞混。我刚开始做Modbus开发时,就因为这个字节序问题,读出来的数据全是错的。
另外,传输规则还有几点要注意:
- 每个字节的格式:1个起始位 + 8个数据位 + 无校验/奇校验/偶校验 + 1个停止位
- 波特率:9600、19200、38400等,主从站必须一致
- 报文间隔:两个报文之间至少要有3.5个字符的静默时间
为什么要有3.5个字符的间隔?说白了,就是让从站知道“上一个报文结束了,下一个报文要开始了”。如果没有这个间隔,从站可能把两个报文当成一个来处理,那数据就全乱了。
3.7 一个完整的报文示例
咱们来看一个完整的读写例子。假设我们要把1号从站的寄存器0x0001的值改为0x00FF:
主站发送:01 06 00 01 00 FF D8 1A
从站响应:01 06 00 01 00 FF D8 1A
注意看,写单个寄存器的响应报文,就是把发送报文原样返回。这是Modbus协议规定的。如果返回的不一样,说明出错了。
我遇到过一种情况:从站返回了异常码0x86(功能码0x06的最高位置1),表示“非法数据值”。后来查手册才知道,那个寄存器是只读的,不能写。所以,拿到设备手册后,一定要先看清楚哪些寄存器可读可写,哪些只能读。
3.8 避坑指南
最后,我总结几个常见的坑:
- 地址重复:同一总线上不能有两个相同地址的设备
- CRC错误:计算CRC时别忘了包含地址码和功能码
- 字节序搞反:多字节数据一定是低字节在前
- 波特率不匹配:主从站的波特率、校验位、停止位必须完全一致
- 报文间隔太短:两个报文之间至少间隔3.5个字符时间
嗯,这一章的内容就到这里。报文格式是Modbus RTU的基础,搞懂了它,后面的通信调试就会顺畅很多。下一章咱们聊聊实际项目中怎么用代码实现Modbus RTU通信。