3、Modbus RTU协议:帧结构、功能码详解、CRC校验计算、主从站实现
好,咱们今天来啃一块硬骨头——Modbus RTU。说实话,这协议在工控圈子里,就跟英语里的ABC一样基础。但越是基础的东西,越容易出幺蛾子。我见过太多项目,最后排查下来,问题就出在RTU的帧结构或者CRC算错了。
咱们今天不讲虚的,直接上干货。从帧结构到功能码,再到CRC校验和主从站实现,一条龙捋清楚。
3.1 帧结构:别小看这串字节
Modbus RTU的帧结构,说白了就是一组字节的排列规则。你想想看,两个设备要聊天,总得有个规矩吧?谁先说话,说多长,怎么结尾,都得定好。
RTU的帧结构长这样:
| 地址码 | 功能码 | 数据区 | CRC校验 |
| 1字节 | 1字节 | N字节 | 2字节 |
嗯,就这么简单。但简单不代表容易。我当年刚入行时,就犯过一个低级错误——把地址码和功能码的顺序搞反了。结果设备死活不响应,查了三天才发现。
地址码:从站地址,范围1-247。0是广播地址,所有从站都得听,但不回复。
功能码:告诉从站要干啥。读还是写,读线圈还是读寄存器。
数据区:具体参数。比如从哪个地址开始读,读多少个。
CRC校验:两个字节,用来检查数据在传输过程中有没有被干扰。
重要提醒:RTU帧没有起始位和停止位标识。它是靠总线空闲时间来判断帧边界的。一般要求3.5个字符时间的静默,才算一帧结束。这个时间跟波特率有关,我建议你算好再配置。
3.2 功能码详解:常用的就这几个
Modbus功能码很多,但实际项目中常用的,一只手数得过来。我个人习惯,先把这五个吃透:
| 功能码 | 名称 | 作用 | 我常用的场景 |
|---|---|---|---|
| 01 | 读线圈 | 读取DO状态 | 读取继电器输出 |
| 02 | 读离散输入 | 读取DI状态 | 读取按钮、限位开关 |
| 03 | 读保持寄存器 | 读取模拟量输出值 | 读取变频器频率设定值 |
| 04 | 读输入寄存器 | 读取模拟量输入值 | 读取温度、压力传感器 |
| 06 | 写单个寄存器 | 写入一个保持寄存器 | 设定PID目标值 |
举个例子,功能码03的请求帧:
从站地址: 01
功能码: 03
起始地址: 00 01 (高字节在前)
寄存器数量: 00 02 (读2个寄存器)
CRC校验: 95 CB
响应帧呢?从站会回复:
从站地址: 01
功能码: 03
字节数: 04 (2个寄存器×2字节)
数据: 00 0A 00 14 (两个寄存器的值)
CRC校验: F8 7D
这里有个坑——地址是从0开始的。比如PLC里地址是40001,那Modbus地址就是0000。我见过有人直接拿40001去发,结果读回来的全是错的。
我的小技巧:写代码时,把地址映射关系单独做个表。40001对应0x0000,40002对应0x0001,这样不容易乱。
3.3 CRC校验计算:别偷懒,自己算一遍
CRC校验,很多人直接调库。但我建议你至少手算一次,理解原理。为什么?因为调试时,CRC算不对是最常见的故障原因之一。
Modbus RTU用的是CRC-16,多项式是0x8005。计算步骤:
- 初始化CRC寄存器为0xFFFF
- 取第一个字节与CRC低8位异或
- 右移一位,如果移出的位是1,则与0xA001异或
- 重复8次
- 处理下一个字节
- 最后得到的CRC,低字节在前,高字节在后
来个C语言实现,我当年在STM32上就是这么写的:
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算出来总是差一个字节。排查了半天,发现是大小端搞反了。Modbus RTU的CRC是低字节在前,高字节在后,跟很多人的习惯不一样。
避坑指南:我曾经遇到过一台国产仪表,它的CRC顺序跟标准是反的。后来我加了个配置项,让用户自己选CRC顺序。嗯,有时候兼容性比标准更重要。
3.4 主从站实现:谁先说话?
Modbus RTU是典型的主从架构。主机先发请求,从机收到后回复。从机不能主动发数据,只能等主机问。
主站实现要点:
- 轮询所有从站,每个从站发完请求后,等待响应
- 设置超时时间,我一般设500ms,够用了
- 收到响应后,检查地址码、功能码、CRC
- 如果超时或CRC错误,重试3次,再不行就报错
从站实现要点:
- 监听总线,收到完整一帧后,检查地址是否匹配
- 解析功能码,执行对应操作
- 组装响应帧,计算CRC,发回总线
- 如果功能码不支持,返回异常码
异常码长这样:
从站地址: 01
功能码: 83 (03 + 0x80,表示异常)
异常码: 02 (非法数据地址)
CRC校验: C0 F1
常见的异常码:
- 01:非法功能码
- 02:非法数据地址
- 03:非法数据值
- 04:从站设备故障
你想想看,如果从站回复了异常码,主站该怎么处理?我建议是记录日志,然后跳过这个从站,继续轮询下一个。别卡死在那里。
实战经验:我在一个污水处理项目中,用了32个从站。一开始轮询一圈要3秒,后来优化成只读变化的数据,轮询时间降到了0.5秒。说白了,Modbus RTU的效率瓶颈往往不在协议本身,而在你的轮询策略。
嗯,今天就先聊到这儿。Modbus RTU看着简单,但真正用好,还是得靠实践。下一章咱们聊聊Modbus TCP,看看它跟RTU有什么不同。