3. RTU帧结构详解

Modbus RTU 的可靠性来自一套非常朴素的约定:用静默时间划清帧边界,用地址区分从站,用功能码说明意图,用数据域承载参数,用 CRC 兜底校验。理解这五个部分,等于拿到了从站开发最底层的“语法书”。本节把 RTU 帧拆开逐段讲解,并说明 3.5 字符定时为什么是从站状态机的第一驱动力。

一、RTU 帧的整体结构

一条标准 RTU 帧由四段组成,总长度取决于功能码和数据量:

字段长度作用从站处理要点
地址域1 字节标识目标从站0 为广播,1~247 为单播地址
功能码1 字节说明操作类型判断是否支持,决定后续数据长度
数据域N 字节寄存器地址、数量、值等按功能码解析,校验范围与数量
CRC 校验2 字节检测传输错误低字节在前,高字节在后

注意:RTU 帧没有起始符和结束符,帧与帧之间完全靠时间间隔区分。这也是它与 ASCII 帧最本质的区别。

二、地址域:谁在说话

地址域占 1 字节,取值范围 0~255,但工程上有效单播地址通常为 1~247。地址 0 是广播地址,所有从站都应接收并执行,但不应答;248~255 为保留地址。从站收到一帧后,第一件事就是比较地址:

  • 地址等于本机地址:正常处理并回复。
  • 地址为 0:执行写操作,但不回复。
  • 地址为其他值:直接丢弃,不产生任何响应。
从站固件中,地址比较应放在 CRC 校验之前还是之后?建议先做长度与地址粗判,再做 CRC 校验,最后才执行业务逻辑,避免无效帧触发副作用。

三、功能码:要做什么

功能码占 1 字节,常见的有 0x01 读线圈、0x02 读离散输入、0x03 读保持寄存器、0x04 读输入寄存器、0x05 写单个线圈、0x06 写单个寄存器、0x0F 写多个线圈、0x10 写多个寄存器。从站收到功能码后,需要判断两件事:是否支持该功能码,以及数据域长度是否符合该功能码的格式。

如果功能码不支持,从站应返回异常响应:功能码最高位置 1,并在数据域给出异常码。例如请求 0x03 不支持时,返回 0x83 加异常码 0x01(非法功能)。

四、数据域:参数与边界

数据域长度和内容由功能码决定。以 0x03 读保持寄存器为例,请求帧数据域为 4 字节:起始地址高、低字节,寄存器数量高、低字节。响应帧数据域为 1 字节字节数加 2N 字节寄存器值。从站必须对以下边界做检查:

  1. 起始地址加数量是否越界。
  2. 数量是否在 1~125 之间(0x03 的常见限制)。
  3. 字节数是否与数量匹配。

任何一项不满足,都应返回异常码 0x02(非法数据地址)或 0x03(非法数据值),而不是强行截断或返回错误数据。

五、CRC 校验:最后一道防线

CRC 采用 CRC-16/MODBUS,多项式 0xA001(反向表示),初值 0xFFFF。计算范围是地址域、功能码、数据域的全部字节,不包括 CRC 本身。发送时低字节在前,高字节在后。从站收到帧后,对前面所有字节重新计算 CRC,与帧尾两字节比较,不一致则丢弃。

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 b = 0; b < 8; b++) {
            if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
            else               crc >>= 1;
        }
    }
    return crc;
}

六、帧间隔与 3.5 字符定时

RTU 规定:帧内字符间隔不得超过 1.5 个字符时间,帧与帧之间至少需要 3.5 个字符时间的静默。一个字符时间取决于波特率和帧格式。以 9600bps、8 数据位、无校验、1 停止位为例,一个字符共 10 位,字符时间约为 1.042ms,3.5 字符约为 3.65ms。

从站通常用定时器实现:每收到一个字节就重置定时器;当定时器超过 3.5 字符时间且缓冲区非空,就认为一帧结束,进入解析流程。如果帧内出现超过 1.5 字符的间隔,应丢弃当前帧并重新同步。

Modbus RTU 帧结构与 3.5 字符定时 地址 功能码 数据域(N 字节) CRC 低 | 高 地址 功能码 ≥ 3.5 字符静默 帧内字符间隔 ≤ 1.5 字符 t0 帧1结束 帧2开始 t 接收中 判帧 解析/响应

七、案例:一次读保持寄存器的完整帧

假设从站地址为 0x01,主机读取起始地址 0x0000 的 2 个保持寄存器,请求帧为:

01 03 00 00 00 02 C4 0B
地址  功能码  起始地址  数量   CRC

从站正常响应(假设寄存器值为 0x1234、0x5678):

01 03 04 12 34 56 78 XX XX
地址  功能码  字节数  数据1  数据2   CRC

如果从站不支持 0x03,则返回:

01 83 01 XX XX
地址  异常功能码  异常码  CRC

八、常见误区与复盘

  • 误区一:把 3.5 字符定时写成固定毫秒。正确做法是按波特率和字符位数动态计算,否则换波特率后帧边界会错乱。
  • 误区二:CRC 校验通过就立即执行写操作。应先完成地址、功能码、数据范围检查,再执行,避免非法参数写入。
  • 误区三:广播帧也回复。广播地址 0 的帧不应答,否则总线会出现冲突。
  • 误区四:帧内间隔超过 1.5 字符仍继续拼接。应丢弃当前帧并重新同步,否则会把两帧错误地拼成一帧。
从站开发中,接收中断里只做“收字节 + 重置定时器”,解析和 CRC 校验放到主循环或任务中完成,避免在中断里做耗时运算导致丢字节。

把地址、功能码、数据域、CRC 和 3.5 字符定时这五件事逐一落实,RTU 从站的帧处理骨架就立住了。后续所有功能码实现,都是在这套骨架上填充具体的数据映射与异常分支。