3. Modbus RTU协议详解:RTU帧结构、地址域、功能码、数据域、CRC校验原理

Modbus RTU(Remote Terminal Unit)是工业现场最常用的串行通信协议之一。它以紧凑的帧结构、高效的二进制编码和强大的CRC校验,在PLC、变频器、传感器、仪表等设备之间建立了可靠的“对话”规则。理解RTU帧结构是进行串行总线调试和故障排查的基础。

3.1 RTU帧结构总览

Modbus RTU的报文以“帧”为单位进行传输。每一帧由四个基本域组成:地址域、功能码、数据域和CRC校验域。帧与帧之间通过至少3.5个字符时间的静默间隔(Silent Interval)进行分隔。

关键概念:RTU模式采用二进制编码,每个8位字节直接发送,通信效率高于ASCII模式。但这也意味着对时序要求更严格,任何字节间的停顿超过1.5个字符时间,接收方都会认为帧不完整而丢弃。
Modbus RTU 帧结构分解 地址域 1 Byte 功能码 1 Byte 数据域 N Bytes (0 ~ 252) CRC校验 2 Bytes 帧间隔 ≥ 3.5字符 📌 帧结构要点 • 最小帧长度:1(地址) + 1(功能码) + 0(数据) + 2(CRC) = 4 Bytes • 最大帧长度:1 + 1 + 252 + 2 = 256 Bytes • 帧间隔用于帧同步,是RTU模式区别于ASCII模式的重要特征

3.2 地址域(Address Field)

地址域占用1个字节,取值范围为 0 ~ 247。其中:

  • 1 ~ 247:分配给从站设备的唯一地址。主站通过地址域指定要通信的从站。
  • 0:广播地址。所有从站都会接收并执行命令,但不需要回复。
调试经验:在RS485总线上,每个从站设备的地址必须唯一。如果出现地址冲突,会导致多个设备同时响应,造成总线数据冲突(Bus Contention)。建议在设备上电前,使用拨码开关或配置软件确认地址。
地址域取值与含义
地址值 目标设备 响应要求 典型应用场景
0 所有从站(广播) 不响应 同步时钟、启动批量操作
1 ~ 247 指定从站 必须响应 读取传感器数据、写入控制参数
248 ~ 255 保留 — 协议扩展或厂商自定义

3.3 功能码(Function Code)

功能码占用1个字节,用于告诉从站要执行什么操作。功能码分为公共功能码、用户自定义功能码和保留功能码三类。在厂务数据采集中,最常用的是以下公共功能码:

Modbus RTU 常用功能码分类 位操作 01 (0x01) 读线圈 02 (0x02) 读离散输入 05 (0x05) 写单线圈 字操作(寄存器) 03 (0x03) 读保持寄存器 04 (0x04) 读输入寄存器 06 (0x06) 写单寄存器 诊断与扩展 08 (0x08) 诊断 15 (0x0F) 写多线圈 16 (0x10) 写多寄存器
注意:功能码 0x03(读保持寄存器)和 0x04(读输入寄存器)是厂务数据采集中最常用的功能码。保持寄存器可读可写,用于存储设定值;输入寄存器只读,用于存储传感器实时测量值。调试时务必确认设备手册中寄存器地址对应的功能码类型。

3.4 数据域(Data Field)

数据域是帧中长度可变的部分,具体内容取决于功能码。数据域通常包含:

  • 起始地址:要操作的寄存器或线圈的起始编号(2字节,高字节在前)。
  • 数量:要读取或写入的寄存器/线圈数量(2字节,高字节在前)。
  • 数据值:写入操作时携带的具体数值。

以读取保持寄存器(功能码 0x03)为例,请求帧的数据域格式如下:

读保持寄存器请求帧数据域结构
字节偏移 内容 长度 示例值 说明
0 起始地址高字节 1 Byte 0x00 寄存器地址 = 0x0064 = 100
1 起始地址低字节 1 Byte 0x64
2 寄存器数量高字节 1 Byte 0x00 读取数量 = 0x000A = 10 个寄存器
3 寄存器数量低字节 1 Byte 0x0A

对应的响应帧数据域则包含:

  • 字节计数:后续数据的字节数(1字节)。
  • 寄存器数据:每个寄存器占2字节,高字节在前,低字节在后。
字节序(Endianness)提示:Modbus RTU 标准采用 Big-Endian(大端序),即高字节在前,低字节在后。例如寄存器值 0x1234,发送顺序为 0x12 0x34。部分设备厂商可能使用 Little-Endian,调试时需通过抓包对比确认。

3.5 CRC校验原理

CRC(Cyclic Redundancy Check,循环冗余校验)是Modbus RTU保证数据完整性的核心机制。它使用16位CRC,多项式为 0x8005(x¹⁶ + x¹⁵ + x² + 1),初始值为 0xFFFF。

CRC校验覆盖整个报文(地址域 + 功能码 + 数据域),但不包括CRC本身。接收方计算收到的报文CRC,并与帧尾的CRC值比较,若不一致则丢弃该帧并返回异常响应。

CRC-16 (Modbus) 计算流程 初始化 CRC = 0xFFFF 取下一个字节 ⊕ CRC 低字节 右移1位,高位补0 移出位=1? 是 CRC ⊕ 0xA001 否 继续右移 循环处理每个字节 处理完所有字节后, CRC 高低字节交换即为最终值

CRC计算的Python实现示例:

def crc16_modbus(data: bytes) -> int:
    """
    计算 Modbus RTU CRC-16 校验值
    :param data: 待校验的字节数据(地址 + 功能码 + 数据域)
    :return: 16位 CRC 值
    """
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    # CRC 高低字节交换
    return ((crc & 0xFF) << 8) | ((crc >> 8) & 0xFF)

# 示例:计算报文 01 03 00 64 00 0A 的 CRC
frame = bytes([0x01, 0x03, 0x00, 0x64, 0x00, 0x0A])
crc_value = crc16_modbus(frame)
print(f"CRC = 0x{crc_value:04X}")  # 输出: CRC = 0xBCB5
常见错误:CRC计算时,很多初学者忘记对最终结果进行高低字节交换。Modbus RTU 规定 CRC 在帧中的发送顺序是低字节在前,高字节在后。例如 CRC=0xBCB5,发送顺序为 0xB5 0xBC。如果交换错误,接收方会认为 CRC 校验失败。

3.6 完整报文示例

下面是一个完整的Modbus RTU请求与响应报文示例,主站读取地址为01的从站,从寄存器地址100(0x0064)开始读取10个保持寄存器。

读保持寄存器完整报文示例
方向 地址域 功能码 数据域 CRC (低字节) CRC (高字节)
请求 (主站→从站) 0x01 0x03 0x00 0x64 0x00 0x0A 0xB5 0xBC
响应 (从站→主站) 0x01 0x03 0x14 (20字节数据)... 0x?? 0x??

响应帧的数据域中,0x14 表示后续有20个字节的数据(10个寄存器 × 2字节/寄存器)。如果从站检测到错误(如寄存器地址不存在),则会返回异常响应帧:

  • 功能码:原功能码 + 0x80(例如 0x83)。
  • 数据域:1字节的异常码,指示错误类型(如 0x02 表示非法数据地址)。
调试要点:使用串口调试工具(如 ModScan、Serial Port Monitor)抓取报文时,重点关注:① 地址是否匹配;② 功能码是否正确;③ 寄存器地址是否在设备支持的范围内;④ CRC 是否计算正确。80% 的通信故障都可以通过检查这四点解决。