3. Modbus RTU 帧结构深度解析
好,咱们今天来聊聊Modbus RTU的帧结构。说实话,这是整个协议最核心的部分。你搞懂了帧结构,抓包分析基本就学会了一半。
RTU帧结构其实很简单,就四个部分:地址码、功能码、数据区、CRC校验。我一个个说,顺便聊聊我在现场踩过的坑。
3.1 地址码:谁在说话?
地址码占1个字节,范围是1-247。0是广播地址,248-255保留。
关键点:
- 主站发请求时,地址码填的是从站地址
- 从站回复时,地址码填的是自己的地址
- 地址0表示广播,所有从站都要接收,但不需要回复
我遇到过的一个坑: 有次现场调试,从站地址设成了0。结果主站发啥它都回,但回的都是广播地址。主站根本不认,折腾了一下午才发现是地址配错了。所以啊,地址码千万别设0,除非你真要做广播。
3.2 功能码:要干啥?
功能码也是1个字节。常用的就这几个:
| 功能码 | 含义 | 我常用的场景 |
|---|---|---|
| 01 | 读线圈状态 | 读取继电器输出 |
| 02 | 读离散输入 | 读取按钮、传感器 |
| 03 | 读保持寄存器 | 读取模拟量、参数 |
| 04 | 读输入寄存器 | 读取模拟量输入 |
| 05 | 写单个线圈 | 控制继电器 |
| 06 | 写单个寄存器 | 设置参数 |
| 15 | 写多个线圈 | 批量控制 |
| 16 | 写多个寄存器 | 批量设置 |
你想想看,功能码其实就是个指令。主站发03,意思就是"把数据给我"。从站回03,意思就是"好的,数据在这"。
我的习惯: 调试时先用03功能码读几个寄存器,确认通信通了,再试写操作。这样能快速定位问题是出在通信上还是设备上。
3.3 数据区:具体内容
数据区长度可变,从0到252字节。具体格式取决于功能码。
举个例子,读保持寄存器(功能码03):
- 主站发:地址码 + 03 + 起始地址(2字节) + 寄存器数量(2字节) + CRC
- 从站回:地址码 + 03 + 字节数(1字节) + 数据(N字节) + CRC
// 主站请求:读取从站1,起始地址0,读2个寄存器
01 03 00 00 00 02 C4 0B
// 从站回复:数据为 0x1234 和 0x5678
01 03 04 12 34 56 78 3E C9
这里要注意,Modbus的数据是大端模式。高位字节在前,低位字节在后。我见过不少新手在这上面栽跟头。
我曾经踩过的坑: 有次解析一个32位浮点数,按小端方式拼了半天,数值死活不对。后来才发现Modbus是大端,得把字节顺序反过来。嗯,从那以后我写代码都会加个注释:// Modbus是大端!
3.4 CRC校验:数据对不对?
CRC校验占2个字节,校验范围是地址码+功能码+数据区。CRC校验码本身不参与计算。
算法是CRC-16/MODBUS,多项式是0x8005。说白了,就是算一个16位的循环冗余校验值。
// 我常用的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校验是Modbus RTU的"最后一道防线"。我建议你在写程序时,收到报文先算CRC,不对就直接丢弃。别问我为什么,问就是吃过亏——有次现场干扰大,CRC错的报文被当成正常数据,结果设备乱动,差点出事故。
3.5 报文最小时间间隔:3.5字符时间
这个知识点,说实话,很多人容易忽略。但它特别重要。
3.5字符时间是什么意思?
- 就是一个报文结束到下一个报文开始,中间至少要有3.5个字符的静默时间
- 如果两个字节之间的间隔超过1.5个字符时间,接收方就认为报文结束了
具体计算:
- 1个字符 = 1个起始位 + 8个数据位 + 1个停止位(无校验)= 10位
- 波特率9600时,1位的时间 = 1/9600 ≈ 104μs
- 3.5字符时间 = 3.5 × 10 × 104μs ≈ 3.64ms
| 波特率 | 1字符时间 | 3.5字符时间 | 1.5字符时间 |
|---|---|---|---|
| 9600 | 1.04ms | 3.64ms | 1.56ms |
| 19200 | 0.52ms | 1.82ms | 0.78ms |
| 38400 | 0.26ms | 0.91ms | 0.39ms |
| 115200 | 0.087ms | 0.30ms | 0.13ms |
我曾经踩过的坑: 有次用PLC和变频器通信,波特率设到115200。程序里定时器精度不够,3.5字符时间算出来0.3ms,但实际延时只有0.1ms。结果报文经常粘包,变频器动不动就报错。后来我把波特率降到38400,问题就解决了。所以啊,高速率下一定要确认你的硬件能不能精确计时。
实际应用建议:
- 写程序时,建议把3.5字符时间设得稍微宽松一点,比如4-5个字符时间
- 接收时用超时机制,超过1.5字符时间没收到新字节,就认为报文结束
- 发送时,发完一帧后等3.5字符时间再发下一帧
我的经验: 如果你用的是现成的Modbus库,这些时间参数通常已经帮你处理好了。但如果你是自己写协议栈,一定要在代码里加上超时判断。我习惯用状态机来做,收到字节就重置定时器,超时了就处理完整报文。这样既简单又可靠。
小结
好了,Modbus RTU帧结构就这些内容。说白了就是:
- 地址码:告诉谁在说话
- 功能码:告诉要干啥
- 数据区:具体内容
- CRC:保证数据没错
- 3.5字符时间:保证报文不粘包
下一章咱们聊聊Modbus TCP,看看它和RTU有啥区别。到时候我会用Wireshark抓包,带你们实际看看报文长啥样。