3. Modbus RTU帧结构:地址码、功能码、数据区、CRC校验详解
咱们做现场总线的人,天天跟Modbus RTU打交道。说白了,RTU就是一串字节在线上跑,但每个字节都有它的使命。今天我就把这四个部分——地址码、功能码、数据区、CRC校验——掰开了揉碎了讲清楚。
3.1 地址码:谁在说话?
地址码是帧的第一个字节。取值范围从1到247,0是广播地址,248到255保留。嗯,这里要注意:广播地址(0x00)所有从站都会接收,但从站不会回复。
我个人的习惯:在项目初期规划地址时,我会留出10%的余量。比如现场有30个设备,我不会从1排到30,而是从1排到33,中间跳几个号。为什么?因为后期加设备时,你不想重新烧录所有从站的地址吧?
地址码关键点:
- 单播地址:1~247,每个从站唯一
- 广播地址:0,所有从站执行但不回复
- 地址冲突是现场最常见的故障之一
我在项目中遇到过一件事:一个水处理项目,30多个PLC走Modbus RTU,结果有两台设备地址都设成了5。你想想看,主站发个读指令,两个从站同时回复,总线直接乱套。查了我整整一个下午,最后拿万用表量地址拨码开关才发现。从那以后,我每个项目都会在调试前用扫描工具扫一遍所有在线设备的地址。
3.2 功能码:要干什么?
功能码是帧的第二个字节。它告诉从站:你要读什么?写什么?做什么操作?
常用的功能码其实就那几个,我列个表给你看:
| 功能码 | 含义 | 我常用的场景 |
|---|---|---|
| 0x01 | 读线圈状态 | 读取继电器输出、阀门状态 |
| 0x02 | 读离散输入 | 读取按钮、限位开关 |
| 0x03 | 读保持寄存器 | 读取传感器数值、设定参数 |
| 0x04 | 读输入寄存器 | 读取模拟量输入(AI) |
| 0x05 | 写单个线圈 | 控制单个继电器 |
| 0x06 | 写单个寄存器 | 修改单个设定值 |
| 0x0F | 写多个线圈 | 批量控制输出 |
| 0x10 | 写多个寄存器 | 批量修改参数 |
你可能会问:为什么功能码不能乱用?举个例子,你用0x03去读线圈,从站会返回异常码0x01(非法功能码)。我见过新手工程师把功能码写错,然后死活调不通,最后发现是代码里把0x01写成了0x02。这种低级错误,我早期也犯过。
我的小技巧:调试时先用串口助手抓一帧完整的报文,看功能码对不对。别一上来就怀疑硬件。
3.3 数据区:具体内容
数据区是帧的第三部分,长度可变。它跟在功能码后面,包含起始地址、寄存器数量、要写入的数据等。
举个例子,读保持寄存器的请求帧:
从站地址: 0x01
功能码: 0x03
起始地址: 0x00 0x00 (高字节在前)
寄存器数: 0x00 0x0A (读10个寄存器)
CRC校验: 0x?? 0x??
回复帧的数据区就是这10个寄存器的值,每个寄存器2个字节,共20个字节。
这里有个坑:Modbus的数据是大端模式(Big-Endian),也就是高字节在前。比如寄存器值是0x1234,发送顺序是0x12、0x34。有些国产设备默认是小端,你得在配置里改过来。我曾经在项目里没注意这个,读出来的温度值直接翻了好几倍,吓得我以为传感器坏了。
避坑指南:我曾经在调试变频器时,发现写入的频率值总是对不上。查了半天,原来是数据区的字节顺序搞反了。从那以后,我每次写多字节数据前,都会先写一个已知值,读回来验证一下字节顺序。
数据区的长度取决于功能码和操作数量。写多个寄存器时,数据区还包括字节数(Byte Count)字段。比如写5个寄存器,数据区结构是:
起始地址: 0x00 0x00
寄存器数: 0x00 0x05
字节数: 0x0A (5个寄存器 × 2字节 = 10字节)
数据: 0x?? 0x?? ... (10个字节)
3.4 CRC校验:数据对不对?
CRC校验是帧的最后两个字节。它用来检测数据在传输过程中有没有出错。Modbus RTU用的是CRC-16,多项式是0x8005。
说白了,CRC就是一个数学运算。发送方计算整个帧(地址码+功能码+数据区)的CRC值,放在帧尾。接收方用同样的算法重新计算,如果结果一致,说明数据没被篡改。
我手写一个CRC计算的C代码片段,你感受一下:
uint16_t crc16_modbus(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 = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
注意:CRC校验是低字节在前。比如计算出的CRC是0x1234,发送顺序是0x34、0x12。这个跟数据区的大端模式正好相反,别搞混了。
CRC校验的重要性:
- 没有CRC校验,你无法知道数据是否被干扰
- 现场环境恶劣(电机启停、变频器干扰),CRC能帮你过滤掉大部分错误帧
- 我见过有人为了省CPU资源,把CRC校验给关了。结果呢?数据偶尔跳变,查了两个月没找到原因。
3.5 完整帧结构一览
把上面四个部分串起来,一个完整的Modbus RTU帧长这样:
| 地址码(1B) | 功能码(1B) | 数据区(NB) | CRC低字节(1B) | CRC高字节(1B) |
举个例子,读取地址为1的从站,从寄存器0开始读5个寄存器:
请求帧: 01 03 00 00 00 05 85 C9
回复帧: 01 03 0A 00 01 00 02 00 03 00 04 00 05 3A 1F
你看,请求帧里数据区是起始地址和数量,回复帧里数据区是实际的寄存器值。CRC校验在最后,保证整帧数据的完整性。
调试建议:我习惯用Modbus Poll或ModScan这类工具,先发一帧标准指令,看从站怎么回复。如果回复帧的CRC不对,要么是硬件干扰,要么是从站程序有bug。别急着改代码,先抓波形看看。
3.6 知识体系图
下面这张图把Modbus RTU帧结构的核心逻辑串起来了,你一看就明白:
这张图把帧结构的四个部分和它们的关键属性都标出来了。你调试的时候,对着这个图一帧一帧地拆,很快就能定位问题。
公众号:蓝海资料掘金营,微信deep3321