3. Modbus RTU帧结构:地址码、功能码、数据区、CRC校验详解

咱们做现场总线的人,天天跟Modbus RTU打交道。说白了,RTU就是一串字节在线上跑,但每个字节都有它的使命。今天我就把这四个部分——地址码、功能码、数据区、CRC校验——掰开了揉碎了讲清楚。

3.1 地址码:谁在说话?

地址码是帧的第一个字节。取值范围从1到247,0是广播地址,248到255保留。嗯,这里要注意:广播地址(0x00)所有从站都会接收,但从站不会回复。

我个人的习惯:在项目初期规划地址时,我会留出10%的余量。比如现场有30个设备,我不会从1排到30,而是从1排到33,中间跳几个号。为什么?因为后期加设备时,你不想重新烧录所有从站的地址吧?

地址码关键点:

  • 单播地址:1~247,每个从站唯一
  • 广播地址:0,所有从站执行但不回复
  • 地址冲突是现场最常见的故障之一

我在项目中遇到过一件事:一个水处理项目,30多个PLC走Modbus RTU,结果有两台设备地址都设成了5。你想想看,主站发个读指令,两个从站同时回复,总线直接乱套。查了我整整一个下午,最后拿万用表量地址拨码开关才发现。从那以后,我每个项目都会在调试前用扫描工具扫一遍所有在线设备的地址。

3.2 功能码:要干什么?

功能码是帧的第二个字节。它告诉从站:你要读什么?写什么?做什么操作?

常用的功能码其实就那几个,我列个表给你看:

功能码 含义 我常用的场景
0x01 读线圈状态 读取继电器输出、阀门状态
0x02 读离散输入 读取按钮、限位开关
0x03 读保持寄存器 读取传感器数值、设定参数
0x04 读输入寄存器 读取模拟量输入(AI)
0x05 写单个线圈 控制单个继电器
0x06 写单个寄存器 修改单个设定值
0x0F 写多个线圈 批量控制输出
0x10 写多个寄存器 批量修改参数

你可能会问:为什么功能码不能乱用?举个例子,你用0x03去读线圈,从站会返回异常码0x01(非法功能码)。我见过新手工程师把功能码写错,然后死活调不通,最后发现是代码里把0x01写成了0x02。这种低级错误,我早期也犯过。

我的小技巧:调试时先用串口助手抓一帧完整的报文,看功能码对不对。别一上来就怀疑硬件。

3.3 数据区:具体内容

数据区是帧的第三部分,长度可变。它跟在功能码后面,包含起始地址、寄存器数量、要写入的数据等。

举个例子,读保持寄存器的请求帧:

从站地址: 0x01
功能码:   0x03
起始地址: 0x00 0x00  (高字节在前)
寄存器数: 0x00 0x0A  (读10个寄存器)
CRC校验:  0x?? 0x??

回复帧的数据区就是这10个寄存器的值,每个寄存器2个字节,共20个字节。

这里有个坑:Modbus的数据是大端模式(Big-Endian),也就是高字节在前。比如寄存器值是0x1234,发送顺序是0x12、0x34。有些国产设备默认是小端,你得在配置里改过来。我曾经在项目里没注意这个,读出来的温度值直接翻了好几倍,吓得我以为传感器坏了。

避坑指南:我曾经在调试变频器时,发现写入的频率值总是对不上。查了半天,原来是数据区的字节顺序搞反了。从那以后,我每次写多字节数据前,都会先写一个已知值,读回来验证一下字节顺序。

数据区的长度取决于功能码和操作数量。写多个寄存器时,数据区还包括字节数(Byte Count)字段。比如写5个寄存器,数据区结构是:

起始地址: 0x00 0x00
寄存器数: 0x00 0x05
字节数:   0x0A  (5个寄存器 × 2字节 = 10字节)
数据:     0x?? 0x?? ... (10个字节)

3.4 CRC校验:数据对不对?

CRC校验是帧的最后两个字节。它用来检测数据在传输过程中有没有出错。Modbus RTU用的是CRC-16,多项式是0x8005。

说白了,CRC就是一个数学运算。发送方计算整个帧(地址码+功能码+数据区)的CRC值,放在帧尾。接收方用同样的算法重新计算,如果结果一致,说明数据没被篡改。

我手写一个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校验是低字节在前。比如计算出的CRC是0x1234,发送顺序是0x34、0x12。这个跟数据区的大端模式正好相反,别搞混了。

CRC校验的重要性:

  • 没有CRC校验,你无法知道数据是否被干扰
  • 现场环境恶劣(电机启停、变频器干扰),CRC能帮你过滤掉大部分错误帧
  • 我见过有人为了省CPU资源,把CRC校验给关了。结果呢?数据偶尔跳变,查了两个月没找到原因。

3.5 完整帧结构一览

把上面四个部分串起来,一个完整的Modbus RTU帧长这样:

| 地址码(1B) | 功能码(1B) | 数据区(NB) | CRC低字节(1B) | CRC高字节(1B) |

举个例子,读取地址为1的从站,从寄存器0开始读5个寄存器:

请求帧: 01 03 00 00 00 05 85 C9
回复帧: 01 03 0A 00 01 00 02 00 03 00 04 00 05 3A 1F

你看,请求帧里数据区是起始地址和数量,回复帧里数据区是实际的寄存器值。CRC校验在最后,保证整帧数据的完整性。

调试建议:我习惯用Modbus Poll或ModScan这类工具,先发一帧标准指令,看从站怎么回复。如果回复帧的CRC不对,要么是硬件干扰,要么是从站程序有bug。别急着改代码,先抓波形看看。

3.6 知识体系图

下面这张图把Modbus RTU帧结构的核心逻辑串起来了,你一看就明白:

Modbus RTU帧结构知识体系 Modbus RTU 帧结构 地址码 (1B) 功能码 (1B) 数据区 (N B) CRC校验 (2B) 取值范围: 1~247 0: 广播地址 唯一标识从站 0x01~0x10 常用 读/写操作 异常码: 0x80+功能码 起始地址+数量 大端模式 长度可变 CRC-16 多项式: 0x8005 低字节在前 完整帧 = 地址码 + 功能码 + 数据区 + CRC校验

这张图把帧结构的四个部分和它们的关键属性都标出来了。你调试的时候,对着这个图一帧一帧地拆,很快就能定位问题。


公众号:蓝海资料掘金营,微信deep3321