3、Modbus RTU帧结构:RTU帧的起始、地址域、功能码、数据域、CRC校验、帧结束的时序要求
好,咱们今天聊聊Modbus RTU的帧结构。说实话,这是很多初学者容易栽跟头的地方。你想想看,协议栈调通了,数据就是读不对,十有八九是帧结构没搞明白。
RTU模式跟ASCII模式最大的区别,就是它用二进制传输,效率高,但对时序要求极其严格。我刚开始做项目时,就因为忽略了帧间隔时间,被现场总线折腾得够呛。嗯,咱们一个一个来看。
3.1 帧的起始:静默时间
RTU帧没有像ASCII那样的起始字符(比如冒号“:”)。它靠什么来识别一帧的开始?靠的是总线上的静默时间。
具体来说,在发送一帧数据之前,总线必须保持空闲状态至少3.5个字符时间。这个“字符时间”怎么算?
字符时间 = 1个起始位 + 8个数据位 + 校验位(可选) + 停止位
举个例子:9600波特率,8数据位,无校验,1停止位。
一个字符的时间 = (1 + 8 + 0 + 1) / 9600 ≈ 1.04ms
那么3.5个字符时间 ≈ 3.64ms
我个人习惯在代码里把这个时间算得稍微宽松一点,比如4ms。为什么?因为有些从站设备的晶振精度不够,卡得太死容易丢帧。
避坑指南:我曾经在一个项目中,主站发送间隔只留了2ms,结果从站偶尔能收到,偶尔收不到。查了两天才发现是帧间隔时间不够。从那以后,我写驱动都会把帧间隔设成4ms以上,哪怕波特率是115200。
3.2 地址域:谁在说话?
地址域占1个字节,范围是0~247。其中0是广播地址,所有从站都要响应(但不需要回复)。1~247是单播地址。
这里有个细节:地址0xFF(255)在标准Modbus里是不用的。但有些厂家会拿它做特殊用途,比如“唤醒所有设备”。我个人不建议这么用,容易造成总线冲突。
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0 | 广播地址 | 所有从站接收,但不回复 |
| 1~247 | 单播地址 | 唯一设备地址,必须回复 |
| 248~255 | 保留 | 标准协议未定义,慎用 |
你想想看,如果总线上挂了20个设备,地址重复了会怎样?两个设备同时回复,数据就乱套了。所以设备上电前,一定要确认地址是唯一的。
3.3 功能码:要干什么?
功能码也是1个字节。常用的就那几个:03读保持寄存器、06写单个寄存器、16写多个寄存器。但功能码有个“影子”——异常响应。
当从站收到一个功能码,如果正常处理,就原样返回。如果出错了,从站会把功能码的最高位置1,然后返回一个异常码。比如你发03,从站回复83,那就说明出问题了。
正常响应:功能码 = 0x03
异常响应:功能码 = 0x83(0x03 | 0x80)
我记得有一次调试,主站一直收不到数据,我以为是线路问题。后来抓包一看,从站回复的是0x83,异常码是02(非法数据地址)。原来是寄存器地址写错了。所以,看到功能码的最高位被置1,别慌,先查异常码。
3.4 数据域:真正的内容
数据域长度可变,从0到252字节。它包含两部分:寄存器地址、寄存器数量(或数据值)。
以读保持寄存器(功能码03)为例:
- 起始地址:2字节,高字节在前(大端模式)
- 寄存器数量:2字节,范围1~125
写多个寄存器(功能码16)的数据域就复杂一些:
- 起始地址:2字节
- 寄存器数量:2字节
- 字节计数:1字节(等于寄存器数量 × 2)
- 寄存器值:N字节(每个寄存器2字节)
注意:数据域里的地址是“协议地址”,不是“物理地址”。比如你要读PLC里的地址40001,协议里填的是0x0000。这个映射关系我后面会专门讲,这里先记住:协议地址 = 物理地址 - 40001。
3.5 CRC校验:最后的防线
CRC校验占2字节,低字节在前(小端模式)。这是RTU帧里最容易搞错的地方。
CRC的计算范围是从地址域开始,到数据域结束。帧起始的静默时间和帧结束的静默时间不参与计算。
我贴一段常用的CRC16计算代码,你直接拿去用:
// CRC16计算函数
uint16_t crc16(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 = crc >> 1;
}
}
}
return crc;
}
注意:计算完的CRC,先发低字节,再发高字节。比如CRC结果是0x1234,发送顺序是0x34、0x12。
避坑指南:我曾经见过一个工程师,CRC计算对了,但发送顺序搞反了。从站一直回复异常,他以为是地址问题。后来我让他抓个波形,一看CRC高低字节颠倒了。嗯,这种低级错误,犯一次就记住了。
3.6 帧结束:又是静默时间
帧结束的标志,跟帧起始一样——3.5个字符时间的静默。也就是说,一帧数据发完后,总线必须空闲至少3.5个字符时间,才能发下一帧。
如果两帧之间的间隔小于3.5个字符时间,从站会认为它们是同一帧数据,然后CRC校验失败,直接丢弃。
为什么会这样?因为RTU模式没有帧起始和结束的特定字符,全靠时间间隔来区分。所以,时序就是一切。
总结一下RTU帧的完整结构:
- 帧起始:≥3.5字符静默时间
- 地址域:1字节(0~247)
- 功能码:1字节(0x01~0x7F)
- 数据域:0~252字节(大端模式)
- CRC校验:2字节(小端模式)
- 帧结束:≥3.5字符静默时间
最后说一句,RTU帧的时序要求,说白了就是“别着急”。发完一帧,等一会儿;收完一帧,也等一会儿。很多现场问题,都是因为时序没卡好。你把这个搞明白了,Modbus RTU就算入门了。