3、Modbus RTU帧结构:地址码、功能码、数据区、CRC校验的详细拆解,手算CRC-16的笨办法
各位同学,咱们今天来啃一块硬骨头——Modbus RTU的帧结构。
说实话,很多搞了两年物联网开发的工程师,对CRC校验还是一知半解。他们只知道调库,真到了现场调试,报文发出去对方没反应,抓瞎了。我当年也吃过这个亏,所以今天咱们把这层窗户纸捅破。
3.1 帧结构长什么样?
Modbus RTU的报文,说白了就是一串字节。但这一串字节有严格的规矩。我习惯把它拆成四个部分:
- 地址码(1字节)—— 你要跟谁说话?
- 功能码(1字节)—— 你想干什么?
- 数据区(N字节)—— 具体说啥事?
- CRC校验(2字节)—— 确保没传错。
你看,就这么简单。但实际项目中,很多人栽在细节上。
一个典型的RTU帧(读保持寄存器):
01 03 00 00 00 02 C4 0B
拆开看:
- 01 —— 地址码,表示1号传感器
- 03 —— 功能码,读保持寄存器
- 00 00 —— 起始地址,从0号寄存器开始
- 00 02 —— 读取数量,读2个寄存器
- C4 0B —— CRC校验,低字节在前
3.2 地址码:谁在说话?
地址码就一个字节,范围0x01到0xF7(1~247)。0x00是广播地址,所有设备都得听,但不回复。
嗯,这里要注意:地址码不能重复。我在一个温室大棚项目里遇到过,两个土壤湿度传感器都设成了0x01。结果主站发命令,两个同时回复,总线直接乱套了。后来我挨个拆下来重新配置,折腾了一下午。
所以我的建议是:给每个传感器贴标签,记下地址。别偷懒,现场几十个设备,你根本记不住谁是谁。
3.3 功能码:你想干啥?
功能码告诉从机:你要读数据还是写数据?读什么类型的数据?
农业传感器里最常用的就这几个:
| 功能码 | 含义 | 农业场景 |
|---|---|---|
| 0x03 | 读保持寄存器 | 读取温度、湿度、光照值 |
| 0x04 | 读输入寄存器 | 读取传感器原始AD值 |
| 0x06 | 写单个寄存器 | 设置传感器阈值、校准参数 |
| 0x10 | 写多个寄存器 | 批量配置传感器参数 |
你想想看,如果主站发了个0x03,从机回复的也是0x03。但如果出错了,从机会回复0x83(把最高位置1)。这个细节我当年调试时没注意,一直以为从机没反应,后来才发现是错误码。
3.4 数据区:具体说啥?
数据区最灵活,长度可变。它跟在功能码后面,具体内容取决于功能码。
举个例子,读保持寄存器时:
- 主站发:起始地址(2字节)+ 寄存器数量(2字节)
- 从机回:字节数(1字节)+ 寄存器数据(N字节)
这里有个坑:Modbus的数据是大端模式。比如寄存器里存了0x1234,先发0x12,再发0x34。我见过有人搞反了,读出来的温度值直接翻了好几倍。
我的小技巧:调试时用串口助手抓原始数据,对照手册逐字节看。别信上位机软件显示的数值,它可能帮你做了大小端转换,反而掩盖了问题。
3.5 CRC校验:怎么保证没错?
CRC-16是Modbus RTU的保命符。它占2个字节,放在帧的最后。
计算规则其实不复杂:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC低字节异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复8次(处理完一个字节)
- 所有字节处理完后,得到CRC值
- 发送时低字节在前,高字节在后
你看,规则就这么几条。但手算起来确实麻烦。
3.6 手算CRC-16的笨办法
我当年在实验室没电脑,只能手算。现在想想挺傻的,但确实让我彻底理解了CRC。
咱们拿前面的例子来算:01 03 00 00 00 02 这6个字节的CRC。
手算步骤(一步一步来):
初始值: 0xFFFF
处理第1字节 0x01:
CRC = 0xFFFF ^ 0x01 = 0xFFFE
右移8次,每次判断最低位:
第1次: 0xFFFE >> 1 = 0x7FFF, 移出0, 不异或
第2次: 0x7FFF >> 1 = 0x3FFF, 移出1, 异或0xA001 -> 0x3FFF ^ 0xA001 = 0x9FFE
第3次: 0x9FFE >> 1 = 0x4FFF, 移出0, 不异或
第4次: 0x4FFF >> 1 = 0x27FF, 移出1, 异或0xA001 -> 0x27FF ^ 0xA001 = 0x87FE
第5次: 0x87FE >> 1 = 0x43FF, 移出0, 不异或
第6次: 0x43FF >> 1 = 0x21FF, 移出1, 异或0xA001 -> 0x21FF ^ 0xA001 = 0x81FE
第7次: 0x81FE >> 1 = 0x40FF, 移出0, 不异或
第8次: 0x40FF >> 1 = 0x207F, 移出1, 异或0xA001 -> 0x207F ^ 0xA001 = 0x807E
处理完0x01后, CRC = 0x807E
处理第2字节 0x03:
CRC = 0x807E ^ 0x03 = 0x807D
右移8次...
(篇幅原因,中间省略)
处理完0x03后, CRC = 0x7C0B
... 继续处理剩余字节
最终得到 CRC = 0x0BC4
发送顺序: 低字节在前 -> 0xC4 0x0B
你看,算出来是0x0BC4,跟前面例子里的C4 0B完全一致。
曾经踩过的坑:我曾经手算时把异或顺序搞反了,先移位移了8次再异或,结果怎么都对不上。记住:每移一位,判断一次,立即异或。不是移完8位再处理。
3.7 实际项目中怎么用?
说实话,现在谁还手算CRC?都是调库。但你要理解原理,不然出了问题不知道怎么排查。
我建议你这样做:
- 开发阶段:用在线CRC计算工具验证你的代码
- 调试阶段:用串口助手抓报文,手动算一遍CRC,看是不是对的
- 现场阶段:如果通信不稳定,先检查CRC对不对。很多时候是线路干扰导致CRC错误,从机直接丢弃了报文
嗯,说到现场,我记得有一次在新疆的棉田里调试,传感器老是丢包。查了半天,发现是CRC计算代码里有个字节顺序搞反了。那个项目差点耽误了农时,从那以后我每次写CRC代码都要对着标准算例验证三遍。
好了,这一节的内容就到这儿。帧结构是Modbus的骨架,CRC是它的灵魂。下一节咱们聊聊怎么用代码实现CRC计算,以及怎么在STM32上跑起来。