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抓包,带你们实际看看报文长啥样。