3. Modbus RTU帧结构:从报文到校验,一次讲透
各位同学,咱们今天聊点实在的。Modbus RTU的帧结构,说白了就是设备之间通信的「语言格式」。你发出去的数据长什么样,对方怎么知道你没发错——这些都在帧结构里规定死了。
我记得刚入行那会儿,拿着示波器抓总线波形,对着文档一行行对报文。那时候就想,要是有人能把这些零碎的知识点串起来讲一遍该多好。今天咱们就来干这件事。
3.1 RTU报文格式:一个完整的帧长什么样
先看整体。一个完整的Modbus RTU帧,由四部分组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址域 | 1字节 | 从站地址,范围1~247 |
| 功能码域 | 1字节 | 告诉从站要干什么 |
| 数据域 | N字节 | 具体参数或数据 |
| CRC校验 | 2字节 | 低字节在前,高字节在后 |
嗯,这里要注意:CRC校验是低字节在前。很多新手在这里栽跟头,我当年也犯过这个错——算出来的CRC明明是0x8001,结果发出去怎么都对不上。后来才发现,要先发0x01,再发0x80。
核心要点:RTU帧没有起始位和停止位标记。它靠「总线空闲时间」来区分帧边界。这就是为什么报文间隔时间那么重要。
3.2 地址域:谁在说话?
地址域只有1个字节。主站发请求时,地址域指明要跟哪个从站对话。从站回复时,地址域告诉主站「是我在回话」。
- 有效地址范围:1 ~ 247
- 广播地址:0(所有从站都要接收,但不回复)
- 保留地址:248 ~ 255(别用)
我个人习惯把从站地址写在设备标签上,方便现场排查。有一次在工厂调试,二十多个从站,地址全乱了。最后只能一个个断电、上电、看报文,折腾了一下午。从那以后,我每个设备都贴标签,地址、波特率、奇偶校验,一目了然。
3.3 功能码域:你想干什么?
功能码告诉从站要执行什么操作。常用的就那么几个:
| 功能码 | 含义 | 我常用的场景 |
|---|---|---|
| 0x01 | 读取线圈状态 | 读继电器输出 |
| 0x02 | 读取离散输入 | 读按钮、传感器 |
| 0x03 | 读取保持寄存器 | 读模拟量、参数 |
| 0x04 | 读取输入寄存器 | 读AD转换值 |
| 0x05 | 写单个线圈 | 控制一个继电器 |
| 0x06 | 写单个寄存器 | 设置一个参数 |
| 0x0F | 写多个线圈 | 批量控制 |
| 0x10 | 写多个寄存器 | 批量设置参数 |
你想想看,功能码其实就两种:读和写。读用0x01~0x04,写用0x05、0x06、0x0F、0x10。记住这个规律,用起来就顺手多了。
小技巧:如果从站返回的功能码最高位是1(比如0x81),说明出错了。这叫异常响应。具体是什么错误,看数据域里的异常码。
3.4 数据域:具体参数放这里
数据域是帧里最灵活的部分。长度不固定,取决于功能码和具体操作。
举个例子,读取保持寄存器(功能码0x03):
主站发送:01 03 00 00 00 0A XX XX
从站回复:01 03 14 [20字节数据] XX XX
这里:
- 00 00:起始寄存器地址(从0开始)
- 00 0A:要读10个寄存器
- 14:从站回复的数据字节数(20字节 = 10个寄存器 × 2字节)
我曾经遇到过一个坑:某个国产温控器,它的寄存器地址从1开始编号,但Modbus协议里地址是从0开始的。结果我读地址1,它给我返回地址2的数据。折腾了半天才发现是地址偏移的问题。所以,拿到设备手册,先看地址定义是0-based还是1-based。
3.5 CRC校验原理与计算:别让数据出错
CRC校验,说白了就是给报文算一个「指纹」。接收方收到后重新算一遍,如果指纹对不上,说明数据在传输过程中被污染了。
Modbus RTU用的是CRC-16,多项式是0x8005。计算过程不复杂,但手算太麻烦。我一般直接用查表法:
// CRC-16查表法计算(C语言)
uint16_t crc16(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
uint16_t i, j;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
这段代码我用了十年,从8位单片机到ARM Cortex-M,从没出过问题。你直接拿去用就行。
注意:计算完的CRC要低字节在前发送。比如算出来是0x1234,先发0x34,再发0x12。很多通信异常都是因为这个顺序搞反了。
3.6 报文间隔时间:帧与帧之间的「呼吸」
这是最容易忽略,也最容易出问题的地方。Modbus RTU没有帧起始和结束标记,它靠时间间隔来区分帧。
标准规定:
- 帧内间隔:两个字节之间的时间不能超过1.5个字符时间
- 帧间间隔:两个帧之间至少要有3.5个字符时间的空闲
字符时间怎么算?很简单:
字符时间 = 1 / 波特率 × (数据位 + 起始位 + 停止位 + 校验位)
举例:9600波特率,8数据位,1停止位,无校验
字符时间 = 1/9600 × (8 + 1 + 1 + 0) = 1.04ms
3.5个字符时间 ≈ 3.64ms
1.5个字符时间 ≈ 1.56ms
我在项目里一般这样处理:
- 接收时,用定时器监控字节间隔。超过1.5字符时间,认为帧结束
- 发送时,确保帧间空闲至少3.5字符时间
有一次在PLC项目里,通信老是丢包。查了半天,发现是上位机发送间隔太短,只有2ms。而我们的波特率是19200,3.5字符时间需要1.8ms。理论上够,但实际因为系统调度延迟,偶尔会超。后来把间隔改成5ms,问题就解决了。
实战建议:帧间间隔不要卡着3.5字符时间算。留点余量,5~10个字符时间更稳妥。尤其是用Windows上位机时,系统调度不确定性很大。
知识体系总览
下面这张图,把RTU帧结构的核心逻辑串起来了。你一看就明白:
这张图把今天讲的内容都串起来了。你保存下来,写代码或者调试的时候对照着看,思路会清晰很多。
好了,RTU帧结构就讲到这里。记住:地址域找对人,功能码说对事,数据域带对参数,CRC保正确,间隔时间别乱来。这五条做到了,Modbus通信就稳了八成。
公众号:蓝海资料掘金营,微信deep3321