3. Modbus RTU帧结构

Modbus RTU 是运行在串行链路(RS-485/RS-232)上的二进制协议,它不像 Modbus TCP 那样有 MBAP 报文头,而是依靠“静默间隔”来划分一帧的边界。理解 RTU 帧结构,是驱动开发中收发状态机、超时判定和 CRC 校验实现的根基。一帧完整的 RTU 报文由四部分组成:从站地址域、功能码、数据域和 CRC 校验域,帧与帧之间还必须满足 3.5 个字符时间的静默间隔。

一、RTU 帧的四个组成部分

RTU 帧采用紧凑的二进制编码,没有起始符和结束符,各字段含义如下:

  • 地址域(1 字节):标识目标从站,取值范围 1~247。0 为广播地址,所有从站都接收但不应答;248~255 为保留地址。主站发送时填写目标从站地址,从站应答时回填自身地址。
  • 功能码(1 字节):说明要执行的操作,如 0x01 读线圈、0x03 读保持寄存器、0x06 写单个寄存器、0x10 写多个寄存器。从站正常应答时原样返回功能码;出错时返回“功能码 + 0x80”,并在数据域给出异常码。
  • 数据域(N 字节):内容随功能码变化。读请求中通常包含起始地址和数量;读应答中包含字节数和寄存器值;写请求中包含地址和待写数据。数据域长度可变,是帧长不固定的主要原因。
  • CRC 校验域(2 字节):对地址域、功能码、数据域逐字节计算 CRC-16,低字节在前、高字节在后。接收方重新计算并比对,不一致则丢弃整帧。

下面这张图展示了 RTU 帧的字段排布、字节顺序以及帧间的 3.5 字符静默间隔关系:

Modbus RTU 帧结构与帧间隔 ≥3.5T 帧前静默 地址域 1 字节 功能码 1 字节 数据域 N 字节(随功能码变化) CRC 校验 2 字节(低前高后) ≥3.5T 帧后静默 发送顺序:地址 → 功能码 → 数据 → CRC 低字节 → CRC 高字节 时间轴 帧 1(地址+功能码+数据+CRC) 帧 2 3.5T 关键规则 • 帧内字符间隔必须小于 1.5T,否则接收方认为帧已损坏并丢弃 • 帧间静默必须大于 3.5T,接收方据此判定一帧结束,开始解析 • 波特率越高,1T 越短;115200bps 时 3.5T 约 0.3ms,需用定时器精确判定

二、CRC 校验的计算与验证

Modbus RTU 使用 CRC-16/MODBUS 算法,多项式为 0xA001(反向表示),初始值 0xFFFF。计算时对地址、功能码、数据逐字节异或并移位,最终得到 16 位校验值。驱动中通常用查表法或位运算实现,接收端必须重新计算并比对。

// CRC-16/MODBUS 位运算实现(C 语言)
uint16_t modbus_crc16(const uint8_t *buf, uint16_t len)
{
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
    }
    return crc;   // 低字节在前,高字节在后
}

例如主站读取从站 1 的 2 个保持寄存器,起始地址 0x0000,请求帧为:01 03 00 00 00 02 C4 0B。其中 C4 0B 就是前 6 字节的 CRC 结果,低字节 0xC4 在前,高字节 0x0B 在后。

三、3.5 字符时间的工程实现

3.5 字符时间(3.5T)是 RTU 帧同步的核心参数。1 个字符时间取决于波特率和帧格式:常见配置为 1 起始位 + 8 数据位 + 无校验 + 1 停止位,共 10 位。因此 1T = 10 / 波特率 秒。下表列出常用波特率下的时间参数:

波特率 1 字符时间 1T 1.5T(帧内最大间隔) 3.5T(帧间最小静默)
9600 bps约 1.042 ms约 1.563 ms约 3.646 ms
19200 bps约 0.521 ms约 0.781 ms约 1.823 ms
38400 bps约 0.260 ms约 0.391 ms约 0.911 ms
115200 bps约 0.087 ms约 0.130 ms约 0.304 ms
在低波特率下,3.5T 可以用软件延时或系统滴答粗略判定;但在 115200bps 时 3.5T 仅约 0.3ms,普通任务调度难以保证精度,建议使用硬件定时器或 UART 空闲中断配合 DMA 来判定帧边界。

四、典型场景:一次读保持寄存器的完整交互

假设主站以 9600bps 读取从站 1 的保持寄存器 0x0000~0x0001,交互过程如下:

  1. 主站等待总线静默超过 3.5T,确认总线空闲。
  2. 主站发送请求帧:01 03 00 00 00 02 C4 0B,共 8 字节。
  3. 从站接收后先检查地址是否匹配,再校验 CRC,然后执行读操作。
  4. 从站等待 3.5T 后发送应答帧:01 03 04 00 0A 00 14 XX XX,其中 04 表示 4 个数据字节,后跟两个寄存器值。
  5. 主站接收应答,校验 CRC 后解析数据,完成一次事务。

如果从站返回异常,例如功能码不支持,应答帧为 01 83 02 XX XX,其中 0x83 = 0x03 + 0x80,0x02 表示非法数据地址。驱动必须能识别这种“功能码最高位置 1”的异常应答。

五、常见误区与复盘

误区一:把 3.5T 当成固定毫秒值。 3.5T 随波特率变化,代码中必须根据实际波特率动态计算,不能写死 3.5ms 或 4ms。
  • 误区二:忽略 1.5T 帧内间隔。 如果两个字节之间超过 1.5T,接收方会认为帧已中断,导致整帧被丢弃。高波特率下尤其要注意中断响应延迟。
  • 误区三:CRC 字节序写反。 Modbus RTU 规定 CRC 低字节先发、高字节后发,与很多 CRC 库默认的高字节在前不同,必须显式调整。
  • 误区四:广播帧等待应答。 地址 0 为广播,从站只执行不应答,主站若等待应答会超时。
  • 复盘建议: 用串口抓包工具记录原始字节流,对照帧结构逐字段核对;在驱动中加入 CRC 错误计数、帧间隔超时计数,便于现场定位通信问题。

掌握 RTU 帧结构和 3.5T 规则后,驱动开发的重点就落在“如何可靠地切分帧”和“如何快速校验并分发”上。后续实现中,建议把帧解析做成独立状态机,把 CRC 校验和异常码处理封装成公共函数,这样无论是 RTU 还是 TCP 都能复用大部分业务逻辑。