3. Modbus RTU协议详解:报文帧结构

好,咱们今天来聊聊Modbus RTU的报文帧。说实话,这是整个协议最核心的部分。你搞懂了报文怎么组、怎么拆,基本上就掌握了Modbus RTU的八成。

我刚开始接触这个协议的时候,总觉得它挺简单的——不就是发几个字节嘛。但后来在现场调试时,因为一个字节的顺序搞反了,折腾了整整一个下午。嗯,从那以后,我对报文结构就格外上心了。

3.1 报文帧的总体结构

Modbus RTU的报文帧,说白了就是一条指令。它由四个部分组成:

  • 地址码:1个字节,告诉总线上的设备“这条消息是发给谁的”
  • 功能码:1个字节,告诉设备“你要干什么”
  • 数据区:N个字节,具体要操作的数据
  • CRC校验:2个字节,检查报文在传输过程中有没有出错

你看,结构就这么简单。但简单归简单,每个部分都有讲究。

报文帧格式(十六进制表示)

+--------+--------+------------------+--------+--------+
| 地址码  | 功能码  |     数据区       | CRC低字节 | CRC高字节 |
| (1字节) | (1字节) |   (N字节)        |  (1字节) |  (1字节) |
+--------+--------+------------------+--------+--------+

这里有个细节我要提醒你:CRC校验字节的顺序是低字节在前,高字节在后。我见过不少新手,包括我自己早期也犯过这个错,把高低字节搞反了,结果设备死活不响应。

3.2 地址码——谁在说话?

地址码的取值范围是0x01到0xF7(1到247)。0x00是广播地址,所有设备都会接收,但不会回复。

我个人习惯把地址码分配做成一张表格,贴在机柜门上。这样现场维护的时候,一眼就能看出哪个设备是哪个地址。

地址范围 用途 说明
0x00 广播地址 所有设备接收,不回复
0x01 - 0xF7 单播地址 每个设备唯一,主站轮询
0xF8 - 0xFF 保留 协议保留,不要使用

你想想看,如果两个设备设成了同一个地址,会发生什么?两个设备都会回复,数据就乱套了。我在一个项目中就遇到过这种情况,排查了半天,最后发现是施工队把两个仪表的拨码开关拨成了一样的。

3.3 功能码——要干什么?

功能码决定了这次通信要执行什么操作。常用的功能码就那么几个:

  • 0x01:读取线圈状态(读DO)
  • 0x02:读取离散输入状态(读DI)
  • 0x03:读取保持寄存器(读AO/AI)
  • 0x04:读取输入寄存器(读AI)
  • 0x05:写单个线圈(写DO)
  • 0x06:写单个寄存器(写AO)
  • 0x0F:写多个线圈
  • 0x10:写多个寄存器

这里有个坑:功能码0x03和0x04的区别。0x03读的是保持寄存器,可读可写;0x04读的是输入寄存器,只能读不能写。我在做DCS系统集成时,有个工程师把0x04当成了0x03用,结果数据一直读不到,因为那个设备根本没有输入寄存器。

我的经验:在写上位机程序时,建议把功能码定义成枚举类型。这样代码可读性高,也不容易写错。

// C语言示例
typedef enum {
    READ_COILS         = 0x01,
    READ_DISCRETE_INPUTS = 0x02,
    READ_HOLDING_REGS  = 0x03,
    READ_INPUT_REGS    = 0x04,
    WRITE_SINGLE_COIL  = 0x05,
    WRITE_SINGLE_REG   = 0x06,
    WRITE_MULTI_COILS  = 0x0F,
    WRITE_MULTI_REGS   = 0x10
} ModbusFunctionCode;

3.4 数据区——具体内容

数据区的格式取决于功能码。以最常用的0x03(读取保持寄存器)为例:

  • 起始地址:2个字节,高字节在前
  • 寄存器数量:2个字节,高字节在前

举个例子,我要读取地址为0x0100的寄存器,连续读10个:

请求报文:01 03 01 00 00 0A [CRC]
          |  |  |     |     |
          |  |  |     |     +-- 寄存器数量(10个)
          |  |  |     +-------- 起始地址高字节
          |  |  +-------------- 起始地址低字节
          |  +----------------- 功能码0x03
          +-------------------- 地址码0x01

设备回复的报文格式是这样的:

响应报文:01 03 14 [20个字节数据] [CRC]
          |  |  |       |
          |  |  |       +-- 数据内容(每个寄存器2字节)
          |  |  +---------- 数据字节数(20 = 10个寄存器 × 2字节)
          |  +------------- 功能码0x03
          +---------------- 地址码0x01

注意看,响应报文里多了一个字节计数字段。这是告诉主站“我回了多少数据”。

3.5 CRC校验——保证数据不出错

CRC校验是Modbus RTU的“安全锁”。它用的是CRC-16算法,多项式是0x8005。

计算过程是这样的:

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

CRC计算示例(C语言)

uint16_t crc16(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;  // 0xA001是0x8005的反转
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

我曾经在一个项目中,因为CRC计算错误,导致主站和从站之间通信时断时续。排查了三天,最后发现是单片机的中断影响了CRC计算的时序。从那以后,我写CRC计算函数时都会加上临界区保护。

3.6 字节格式与时序要求

Modbus RTU的字节格式是:1个起始位 + 8个数据位 + 1个校验位(可选) + 1个停止位。最常用的是无校验、1个停止位。

时序要求是Modbus RTU的“软肋”。它要求:

  • 帧间隔:至少3.5个字符时间
  • 字节间隔:不能超过1.5个字符时间

为什么要有这个要求?因为Modbus RTU没有帧头帧尾标记,它是靠“静默时间”来判断一帧数据结束的。如果字节间隔太长,接收方会认为数据已经发完了,然后开始解析——结果数据还没传完,解析出来的就是错的。

重要提醒:在9600波特率下,3.5个字符时间大约是3.65毫秒。如果你的程序在处理数据时耗时过长,或者串口缓冲区设置太小,很容易出现帧间隔超时的问题。

我建议你在做SCADA系统集成时,串口接收缓冲区至少设置256字节,并且用定时器来检测帧间隔。不要用简单的“收到数据就处理”的方式,那样很容易出问题。

3.7 知识体系结构图

下面这张图,是我自己总结的Modbus RTU报文帧的知识体系。你看一眼,就能把整个结构串起来。

Modbus RTU报文帧知识体系 报文帧结构 地址码 (1字节) 0x01-0xF7 单播 0x00 广播 功能码 (1字节) 0x01-0x10 常用 读/写操作 数据区 (N字节) 起始地址+数量 或具体数据 CRC校验 (2字节) CRC-16算法 低字节在前 字节格式 1起始+8数据+1停止 时序要求 帧间隔≥3.5字符 字节间隔≤1.5字符

这张图把报文帧的四个部分、字节格式、时序要求都串起来了。你保存下来,以后做项目时对照着看,不容易出错。

3.8 避坑指南

最后,我把自己这些年踩过的坑总结一下:

  • CRC高低字节顺序:发送时先发低字节,再发高字节。这个顺序搞反了,设备就不认。
  • 寄存器地址偏移:很多PLC的寄存器地址是从1开始的,但Modbus协议里是从0开始的。你写程序时要注意这个偏移量。
  • 帧间隔时间:在高速率(如115200)下,3.5个字符时间很短,大约0.3毫秒。如果你的程序处理不及时,很容易丢帧。
  • 数据区字节序:Modbus RTU默认是大端模式(高字节在前)。但有些设备厂商会用自己的字节序,你得看设备手册确认。

我的建议:在调试阶段,先用Modbus调试工具(比如ModScan、ModSim)验证报文格式。等报文格式确认无误后,再写正式的通信程序。这样可以省去很多排查时间。

好了,关于Modbus RTU的报文帧结构,我就讲到这里。你把这些内容消化掉,后面讲功能码详解和异常处理时,就会轻松很多。


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