4、Modbus RTU帧结构详解:地址域、功能码、数据域、CRC校验、报文示例与手动解析

好,咱们今天来啃一块硬骨头——Modbus RTU的帧结构。

说实话,我刚开始接触Modbus那会儿,看着一串十六进制数,脑袋都是懵的。后来在项目现场,拿着示波器一点一点抓波形,才真正搞明白每个字节到底在干什么。今天我就把这点经验掰开了揉碎了讲给你听。

4.1 整体帧结构:一个完整的报文长什么样?

一个标准的Modbus RTU帧,说白了就是四段数据拼在一起。我习惯叫它「四段式」结构:

字段 长度 说明
地址域 1字节 从站地址,范围1-247
功能码 1字节 告诉从站要干什么
数据域 N字节 具体参数或数据内容
CRC校验 2字节 低字节在前,高字节在后

你想想看,这就像寄快递:地址域是「收件人」,功能码是「要办的事」,数据域是「包裹内容」,CRC就是「保价单」——确保东西没坏。

4.2 地址域:谁在说话?

地址域只有1个字节,取值范围0-255。但实际能用的只有1-247。

  • 0:广播地址,所有从站都要接收,但不回复
  • 1-247:单播地址,每个从站唯一
  • 248-255:保留,别乱用

我的经验:我在一个水处理项目里,有人把从站地址设成了250,结果怎么都通信不上。查了半天手册才发现,有些从站芯片压根不支持248以上的地址。所以啊,老老实实用1-247,别给自己找麻烦。

4.3 功能码:到底要干什么?

功能码也是1个字节。常用的就那么几个,我列个表给你看:

功能码 名称 作用
0x01 读线圈 读取DO状态
0x02 读离散输入 读取DI状态
0x03 读保持寄存器 读取AO/寄存器值
0x04 读输入寄存器 读取AI值
0x05 写单线圈 控制单个DO
0x06 写单寄存器 写入单个寄存器
0x0F 写多线圈 批量控制DO
0x10 写多寄存器 批量写入寄存器

嗯,这里要注意:如果功能码的最高位(bit7)被置1,说明从站返回了异常响应。比如你发了0x03,从站回了0x83,那就是出错了。

4.4 数据域:真正的干货

数据域的长度是可变的,取决于功能码和具体操作。我拿最常见的0x03功能码举例:

主站请求(读保持寄存器):

从站地址: 0x01
功能码:   0x03
起始地址: 0x00 0x6B  (即107)
寄存器数量: 0x00 0x03 (读3个)
CRC校验:  0x?? 0??

从站响应:

从站地址: 0x01
功能码:   0x03
字节数:   0x06  (3个寄存器 × 2字节)
数据:     0x02 0x2B 0x00 0x00 0x00 0x64
CRC校验:  0x?? 0x??

关键点:Modbus的数据都是大端模式(Big-Endian),高字节在前。比如0x02 0x2B,实际值是0x022B = 555。我见过有人把这个搞反了,读出来的数据完全不对。

4.5 CRC校验:最后的守护神

CRC校验是Modbus RTU的命根子。它用的是CRC-16(Modbus),多项式是0x8005。

计算步骤其实不复杂:

  1. 初始化CRC寄存器为0xFFFF
  2. 对每个字节,与CRC寄存器的低8位异或
  3. 右移1位,如果移出的位是1,则与0xA001异或
  4. 重复8次,处理完所有字节
  5. 最终结果取反,得到CRC值

我直接给你一个C语言实现,现场调试时我经常用这个:

unsigned int crc16_modbus(unsigned char *buf, int len) {
    unsigned int crc = 0xFFFF;
    for (int i = 0; i < len; i++) {
        crc ^= buf[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc = crc >> 1;
            }
        }
    }
    return crc;
}

避坑指南:我曾经在一个项目里,CRC怎么算都不对。折腾了半天才发现,我忘了把CRC寄存器初始化为0xFFFF。还有一次,我把高低字节顺序搞反了——Modbus规定低字节在前,高字节在后。这两个坑,你记住了。

4.6 报文示例与手动解析

光说不练假把式。咱们来手动解析一个真实报文。

原始报文(十六进制):

01 03 00 6B 00 03 76 87

手动解析步骤:

  1. 地址域:0x01 → 从站地址1
  2. 功能码:0x03 → 读保持寄存器
  3. 起始地址:0x00 0x6B → 十进制107
  4. 寄存器数量:0x00 0x03 → 读3个
  5. CRC校验:0x76 0x87 → 低字节0x76,高字节0x87

咱们来验证一下CRC:

  • 计算对象:01 03 00 6B 00 03(共5字节)
  • CRC结果应为:0x8776
  • 报文中的CRC:0x76 0x87 → 组合起来就是0x8776

完全正确!

我的小技巧:现场调试时,我习惯用串口助手抓原始数据,然后拿计算器手动算一遍CRC。虽然慢,但能帮你快速定位问题。等熟练了,再用工具自动算。

4.7 常见问题与排查思路

最后,我总结几个现场常见的坑:

  • CRC不对:先检查是不是把CRC字节顺序搞反了,再检查计算范围是否包含了所有字节
  • 地址不对:确认从站地址是否在1-247范围内,有没有重复
  • 数据不对:检查字节序(大端/小端),特别是多字节数据
  • 超时无响应:看看是不是广播地址(0),或者从站根本没收到

好了,Modbus RTU帧结构就讲到这里。说白了就是四个部分,每个部分都有它的规矩。你只要把地址、功能码、数据、CRC这四个东西搞明白了,Modbus通信就通了八成。

下一节咱们聊Modbus TCP,那个更简单,因为CRC被TCP/IP协议栈包办了。但RTU的基础打牢了,TCP就是小菜一碟。