4、Modbus RTU帧结构详解:地址域、功能码、数据域、CRC校验、报文示例与手动解析
好,咱们今天来啃一块硬骨头——Modbus RTU的帧结构。
说实话,我刚开始接触Modbus那会儿,看着一串十六进制数,脑袋都是懵的。后来在项目现场,拿着示波器一点一点抓波形,才真正搞明白每个字节到底在干什么。今天我就把这点经验掰开了揉碎了讲给你听。
4.1 整体帧结构:一个完整的报文长什么样?
一个标准的Modbus RTU帧,说白了就是四段数据拼在一起。我习惯叫它「四段式」结构:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址域 | 1字节 | 从站地址,范围1-247 |
| 功能码 | 1字节 | 告诉从站要干什么 |
| 数据域 | N字节 | 具体参数或数据内容 |
| CRC校验 | 2字节 | 低字节在前,高字节在后 |
你想想看,这就像寄快递:地址域是「收件人」,功能码是「要办的事」,数据域是「包裹内容」,CRC就是「保价单」——确保东西没坏。
4.2 地址域:谁在说话?
地址域只有1个字节,取值范围0-255。但实际能用的只有1-247。
- 0:广播地址,所有从站都要接收,但不回复
- 1-247:单播地址,每个从站唯一
- 248-255:保留,别乱用
我的经验:我在一个水处理项目里,有人把从站地址设成了250,结果怎么都通信不上。查了半天手册才发现,有些从站芯片压根不支持248以上的地址。所以啊,老老实实用1-247,别给自己找麻烦。
4.3 功能码:到底要干什么?
功能码也是1个字节。常用的就那么几个,我列个表给你看:
| 功能码 | 名称 | 作用 |
|---|---|---|
| 0x01 | 读线圈 | 读取DO状态 |
| 0x02 | 读离散输入 | 读取DI状态 |
| 0x03 | 读保持寄存器 | 读取AO/寄存器值 |
| 0x04 | 读输入寄存器 | 读取AI值 |
| 0x05 | 写单线圈 | 控制单个DO |
| 0x06 | 写单寄存器 | 写入单个寄存器 |
| 0x0F | 写多线圈 | 批量控制DO |
| 0x10 | 写多寄存器 | 批量写入寄存器 |
嗯,这里要注意:如果功能码的最高位(bit7)被置1,说明从站返回了异常响应。比如你发了0x03,从站回了0x83,那就是出错了。
4.4 数据域:真正的干货
数据域的长度是可变的,取决于功能码和具体操作。我拿最常见的0x03功能码举例:
主站请求(读保持寄存器):
从站地址: 0x01
功能码: 0x03
起始地址: 0x00 0x6B (即107)
寄存器数量: 0x00 0x03 (读3个)
CRC校验: 0x?? 0??
从站响应:
从站地址: 0x01
功能码: 0x03
字节数: 0x06 (3个寄存器 × 2字节)
数据: 0x02 0x2B 0x00 0x00 0x00 0x64
CRC校验: 0x?? 0x??
关键点:Modbus的数据都是大端模式(Big-Endian),高字节在前。比如0x02 0x2B,实际值是0x022B = 555。我见过有人把这个搞反了,读出来的数据完全不对。
4.5 CRC校验:最后的守护神
CRC校验是Modbus RTU的命根子。它用的是CRC-16(Modbus),多项式是0x8005。
计算步骤其实不复杂:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC寄存器的低8位异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复8次,处理完所有字节
- 最终结果取反,得到CRC值
我直接给你一个C语言实现,现场调试时我经常用这个:
unsigned int crc16_modbus(unsigned char *buf, int len) {
unsigned int crc = 0xFFFF;
for (int i = 0; i < len; i++) {
crc ^= buf[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
避坑指南:我曾经在一个项目里,CRC怎么算都不对。折腾了半天才发现,我忘了把CRC寄存器初始化为0xFFFF。还有一次,我把高低字节顺序搞反了——Modbus规定低字节在前,高字节在后。这两个坑,你记住了。
4.6 报文示例与手动解析
光说不练假把式。咱们来手动解析一个真实报文。
原始报文(十六进制):
01 03 00 6B 00 03 76 87
手动解析步骤:
- 地址域:0x01 → 从站地址1
- 功能码:0x03 → 读保持寄存器
- 起始地址:0x00 0x6B → 十进制107
- 寄存器数量:0x00 0x03 → 读3个
- CRC校验:0x76 0x87 → 低字节0x76,高字节0x87
咱们来验证一下CRC:
- 计算对象:01 03 00 6B 00 03(共5字节)
- CRC结果应为:0x8776
- 报文中的CRC:0x76 0x87 → 组合起来就是0x8776
完全正确!
我的小技巧:现场调试时,我习惯用串口助手抓原始数据,然后拿计算器手动算一遍CRC。虽然慢,但能帮你快速定位问题。等熟练了,再用工具自动算。
4.7 常见问题与排查思路
最后,我总结几个现场常见的坑:
- CRC不对:先检查是不是把CRC字节顺序搞反了,再检查计算范围是否包含了所有字节
- 地址不对:确认从站地址是否在1-247范围内,有没有重复
- 数据不对:检查字节序(大端/小端),特别是多字节数据
- 超时无响应:看看是不是广播地址(0),或者从站根本没收到
好了,Modbus RTU帧结构就讲到这里。说白了就是四个部分,每个部分都有它的规矩。你只要把地址、功能码、数据、CRC这四个东西搞明白了,Modbus通信就通了八成。
下一节咱们聊Modbus TCP,那个更简单,因为CRC被TCP/IP协议栈包办了。但RTU的基础打牢了,TCP就是小菜一碟。