4. RTU帧格式详解
Modbus RTU 是工业现场应用最广泛的串行通信协议之一,其帧结构紧凑、无格式开销,但同时对字节顺序、校验算法和时序要求极为严格。理解 RTU 帧的四个组成部分——地址码、功能码、数据域和 CRC 校验——是进行设备调试、协议解析和故障定位的基础。本章将从帧结构出发,逐一拆解每个字段的编码规则、字节序约定和 CRC 计算原理,并结合实际报文案例,帮助读者建立从物理层字节流到应用层数据的完整映射关系。
4.1 地址码:从站寻址与广播机制
地址码是 RTU 帧的第一个字节,取值范围为 0x00~0xF7(0~247),其中 0x00 保留为广播地址,所有从站必须接收并执行广播命令,但不需要回复响应帧。实际项目中,从站地址通常设置为 1~247,主站通过地址码唯一确定通信对象。地址码的编码规则简单,但工程中容易忽略两个细节:一是地址 0x00 的广播帧不能用于写操作(如写保持寄存器),因为从站无法确认是否执行成功;二是地址码与功能码共同构成帧头,接收方在解析时必须先校验地址匹配,再校验 CRC,否则可能被错误帧干扰。
4.2 功能码:操作类型与异常响应
功能码占用 1 个字节,定义了主站请求的操作类型。常用功能码包括:0x01 读线圈、0x02 读离散输入、0x03 读保持寄存器、0x04 读输入寄存器、0x05 写单线圈、0x06 写单寄存器、0x0F 写多线圈、0x10 写多寄存器。功能码的高位(bit7)用于异常响应标志:当从站返回的功能码最高位为 1 时(如 0x83 对应 0x03),表示异常响应,数据域的第一个字节为异常码(如 0x01 非法功能、0x02 非法数据地址、0x03 非法数据值)。
| 功能码 | 名称 | 数据域方向 | 典型应用 |
|---|---|---|---|
| 0x03 | 读保持寄存器 | 请求:起始地址(2B)+数量(2B);响应:字节数(1B)+寄存器值(N×2B) | 读取 PLC 的设定参数 |
| 0x06 | 写单寄存器 | 请求:地址(2B)+值(2B);响应:原样返回请求帧 | 修改单个设定值 |
| 0x10 | 写多寄存器 | 请求:起始地址(2B)+数量(2B)+字节数(1B)+数据(N×2B) | 批量下发参数表 |
| 0x83 | 异常响应(读保持寄存器) | 异常码(1B) | 报告非法地址或非法功能 |
功能码的选择直接影响数据域的编码方式。例如,读保持寄存器(0x03)的请求数据域固定为 4 个字节(起始地址高字节、起始地址低字节、寄存器数量高字节、寄存器数量低字节),而响应数据域则包含字节计数字段和寄存器值。理解功能码与数据域的对应关系,是编写协议解析代码的前提。
4.3 数据域:字节序规则与多寄存器编码
数据域是帧中长度可变的部分,承载实际读写数据。Modbus RTU 规定,所有多字节数据(如寄存器地址、寄存器值、数量)均采用大端字节序(Big-Endian),即高字节先发送,低字节后发送。例如,寄存器地址 0x0102 在帧中表现为 01 02,而不是 02 01。这一规则在读写多个寄存器时尤为重要:每个寄存器值占 2 个字节,先发高字节,再发低字节。
// 读保持寄存器请求帧示例(读取地址 0x0000 开始的 2 个寄存器)
// 地址码: 0x01
// 功能码: 0x03
// 起始地址: 0x0000 → 发送 0x00 0x00
// 寄存器数量: 0x0002 → 发送 0x00 0x02
// CRC: 计算后得到 0xC4 0x0B(低字节先发)
// 完整帧: 01 03 00 00 00 02 C4 0B
对于浮点数或 32 位整数等跨寄存器数据,Modbus 协议本身未定义字节序,通常由设备厂商在寄存器映射表中说明。常见的有两种方式:AB CD(寄存器顺序 + 寄存器内大端)和 CD AB(寄存器顺序交换)。工程中必须查阅设备手册确认,否则会出现数据解析错误。
12 34,若按小端解析则为 0x3412,而正确值应为 0x1234。建议在解析代码中统一封装字节序转换函数,并在测试阶段用已知值验证。
4.4 CRC 校验:字节顺序与计算原理
CRC(循环冗余校验)是 RTU 帧的最后两个字节,用于检测传输过程中的数据错误。Modbus RTU 采用 CRC-16/IBM 算法,多项式为 x^16 + x^15 + x^2 + 1(即 0x8005,反射形式为 0xA001),初始值为 0xFFFF,计算完成后结果取反。CRC 的字节顺序为低字节在前,高字节在后,与数据域的大端序相反。
CRC 计算步骤如下:
- 预置 16 位寄存器为 0xFFFF。
- 取帧中第一个字节(地址码)与 CRC 寄存器的低 8 位异或,结果放回 CRC 寄存器。
- 将 CRC 寄存器右移 1 位,最高位补 0。若移出的最低位为 1,则与 0xA001 异或。
- 重复步骤 3 共 8 次,完成一个字节的处理。
- 对帧中所有字节(地址码、功能码、数据域)重复步骤 2~4。
- 最终得到的 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 值取反,先发送低字节,再发送高字节。例如,计算得到 CRC 为 0x0BC4,则帧中发送 C4 0B。接收方对收到的完整帧(包括 CRC 字段)重新计算 CRC,若结果为 0x0000,则校验通过;否则丢弃该帧。
4.5 实战案例:从报文到数据的完整解析
假设某温度采集模块地址为 0x11,使用功能码 0x03 读取保持寄存器 0x0000 和 0x0001 中的温度值(浮点数,AB CD 格式)。主站发送请求帧:11 03 00 00 00 02 C4 0B(CRC 为示例值)。从站响应帧为:11 03 04 41 20 00 00 8A 9C。解析过程如下:
- 地址码:0x11,确认是发给本设备的帧。
- 功能码:0x03,读保持寄存器,且最高位为 0,表示正常响应。
- 字节计数:0x04,表示后续有 4 个数据字节。
- 寄存器值:
41 20 00 00,按大端序组合为 0x41200000,即 IEEE 754 浮点数 10.0。 - CRC:
8A 9C,接收方重新计算前 6 个字节的 CRC,取反后与帧中 CRC 比较,一致则接受。
若响应帧的 CRC 校验失败,可能原因包括:波特率不匹配导致字节错位、RS-485 总线干扰导致数据翻转、从站发送时序异常(如帧间隔不足)。此时应使用示波器或逻辑分析仪抓取物理层波形,确认每个字节的起始位和停止位是否完整。
4.6 常见误区与总结
在实际工程中,以下误区需要特别注意:
- 误区一: 认为 CRC 的字节顺序与数据域一致。实际上 CRC 低字节在前,而数据域高字节在前,两者相反。
- 误区二: 忽略广播地址 0x00 的特殊性。广播帧不产生响应,若主站程序等待从站回复,会超时。
- 误区三: 将功能码 0x03 与 0x04 混淆。0x03 读保持寄存器(可写),0x04 读输入寄存器(只读),地址空间不同。
- 误区四: 在计算 CRC 时忘记对结果取反。取反是 Modbus RTU 与标准 CRC-16/IBM 的差异点,直接使用标准库函数可能导致校验错误。
总结而言,RTU 帧格式的精髓在于“字节顺序”和“校验算法”两个维度。地址码和功能码决定了通信对象和操作类型,数据域承载具体参数,CRC 则保障了传输可靠性。掌握这些规则后,无论是编写主站程序、调试从站设备,还是分析总线报文,都能做到有据可依。建议读者在开发过程中,使用 Modbus 调试助手或自写脚本对每一帧进行 CRC 验证,逐步形成对帧结构的直觉判断。