3、Modbus RTU 帧结构:RTU 报文格式、地址域、功能码域、数据域、CRC校验原理与计算示例
Modbus RTU 是工业现场最常用的串行通信协议之一。理解其报文帧结构是开发数据采集驱动、边缘网关和协议转换器的核心基础。RTU 模式采用二进制编码,报文紧凑、效率高,但要求严格的时序控制。
3.1 RTU 报文帧格式
Modbus RTU 的报文以“帧”为单位进行传输。每一帧由四个主要域组成:地址域、功能码域、数据域和CRC 校验域。帧与帧之间通过至少 3.5 个字符时间的静默间隔(Silent Interval)来区分。
| 域名称 | 长度(字节) | 说明 |
|---|---|---|
| 地址域(Address) | 1 | 从站设备地址,范围 1~247 |
| 功能码域(Function Code) | 1 | 指示读/写操作类型(如 0x03 读保持寄存器) |
| 数据域(Data) | N(可变) | 包含起始地址、寄存器数量、数据值等 |
| CRC 校验域(CRC) | 2 | 循环冗余校验,低字节在前,高字节在后 |
3.2 地址域与功能码域
3.2.1 地址域(Address Field)
地址域长度为 1 字节,用于标识总线上的目标从站设备。主站发送的请求帧中包含目标从站地址;从站响应帧中则包含自身地址,以便主站识别响应来源。
- 有效地址范围:1 ~ 247(0x01 ~ 0xF7)
- 广播地址:0(0x00),所有从站接收但不回复
- 保留地址:248 ~ 255(0xF8 ~ 0xFF)
3.2.2 功能码域(Function Code Field)
功能码域长度为 1 字节,指示主站希望从站执行的操作类型。功能码分为公共功能码、用户定义功能码和保留功能码三类。以下是工业数据采集中最常用的功能码:
| 功能码 | 名称 | 操作对象 | 典型用途 |
|---|---|---|---|
| 0x01 | Read Coils | 线圈(DO) | 读取开关量输出状态 |
| 0x02 | Read Discrete Inputs | 离散输入(DI) | 读取开关量输入状态 |
| 0x03 | Read Holding Registers | 保持寄存器 | 读取模拟量、参数、设定值 |
| 0x04 | Read Input Registers | 输入寄存器 | 读取只读模拟量(如传感器值) |
| 0x05 | Write Single Coil | 单个线圈 | 控制单个 DO 输出 |
| 0x06 | Write Single Register | 单个保持寄存器 | 写入单个参数 |
| 0x0F | Write Multiple Coils | 多个线圈 | 批量控制 DO 输出 |
| 0x10 | Write Multiple Registers | 多个保持寄存器 | 批量写入参数 |
3.3 数据域(Data Field)
数据域是帧中长度可变的部分,其内容和长度取决于功能码。对于请求帧,数据域通常包含起始地址和寄存器/线圈数量;对于响应帧,数据域包含字节计数和实际数据值。
3.3.1 请求帧数据域示例(功能码 0x03)
读取从站地址 0x01 的保持寄存器,起始地址 0x0000,读取 2 个寄存器:
字节索引: 0 1 2 3 4 5 6 7
内容: 0x01 0x03 0x00 0x00 0x00 0x02 0xC4 0x0B
含义: 地址 功能码 起始地址高 起始地址低 数量高 数量低 CRC低 CRC高
3.3.2 响应帧数据域示例
从站正常响应,返回 4 字节数据(2 个寄存器,每个寄存器 2 字节):
字节索引: 0 1 2 3 4 5 6
内容: 0x01 0x03 0x04 0x00 0x64 0x00 0xC8 0x?? 0x??
含义: 地址 功能码 字节数 寄存器1高 寄存器1低 寄存器2高 寄存器2低 CRC低 CRC高
3.4 CRC 校验原理与计算示例
CRC(Cyclic Redundancy Check)是 RTU 帧中最重要的错误检测机制。Modbus RTU 使用 CRC-16(Modbus 多项式),多项式为 x¹⁶ + x¹⁵ + x² + 1(即 0x8005),初始值为 0xFFFF,输出时进行异或 0x0000 处理。
3.4.1 CRC 计算步骤
- 将 16 位 CRC 寄存器初始化为 0xFFFF。
- 取帧中第一个字节(地址域)与 CRC 寄存器低 8 位进行异或,结果放回 CRC 寄存器。
- 将 CRC 寄存器右移 1 位,高位补 0。
- 检查移出的最低位:如果为 1,则将 CRC 寄存器与多项式 0xA001(0x8005 的反射形式)进行异或;如果为 0,则继续下一步。
- 重复步骤 3~4 共 8 次(处理完一个字节的所有位)。
- 取下一个字节,重复步骤 2~5,直到所有字节处理完毕。
- 最终 CRC 寄存器中的值即为 CRC 校验码,先发送低字节,后发送高字节。
3.4.2 Python 计算示例
以下 Python 函数演示了标准的 Modbus RTU CRC-16 计算过程:
def calculate_crc(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
return crc
# 示例:计算请求帧 [0x01, 0x03, 0x00, 0x00, 0x00, 0x02] 的 CRC
frame_data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x02])
crc_value = calculate_crc(frame_data)
print(f"CRC 值: 0x{crc_value:04X}")
# 输出: CRC 值: 0xC40B
# 发送顺序: 低字节 0x0B 先发,高字节 0xC4 后发
3.4.3 手动验算示例
以请求帧 01 03 00 00 00 02 为例,CRC 计算结果为 0xC40B。验证步骤:
- 初始 CRC = 0xFFFF
- 处理 0x01:CRC = 0x1E0B
- 处理 0x03:CRC = 0x3A0B
- 处理 0x00:CRC = 0x740B
- 处理 0x00:CRC = 0xE80B
- 处理 0x00:CRC = 0xD00B
- 处理 0x02:CRC = 0xC40B
最终 CRC 为 0xC40B,低字节 0x0B 先发送,高字节 0xC4 后发送。
3.5 完整报文示例
以下是一个完整的 Modbus RTU 请求-响应交互示例,使用功能码 0x03 读取从站地址 0x01 的 2 个保持寄存器(起始地址 0x0000):
| 方向 | 地址 | 功能码 | 数据域 | CRC 低字节 | CRC 高字节 | 完整报文(Hex) |
|---|---|---|---|---|---|---|
| 主站→从站 | 0x01 | 0x03 | 0x00 0x00 0x00 0x02 | 0x0B | 0xC4 | 01 03 00 00 00 02 0B C4 |
| 从站→主站 | 0x01 | 0x03 | 0x04 0x00 0x64 0x00 0xC8 | 0x?? | 0x?? | 01 03 04 00 64 00 C8 ?? ?? |
- CRC 计算错误:未包含地址和功能码,或使用了错误的反射多项式。
- 字节序错误:多字节数据(如起始地址、寄存器值)未按大端序排列。
- 帧间隔不足:连续帧之间未保持 ≥ 3.5 字符时间的静默间隔,导致帧粘连。
- 数据域长度不匹配:响应帧中声明的字节数与实际数据长度不一致。
3.6 边缘计算场景中的帧处理要点
在边缘计算网关或数据采集终端中,处理 Modbus RTU 帧时需特别注意以下工程实践:
- 帧完整性检测:利用帧间静默间隔和 CRC 校验双重确认帧完整性,避免解析不完整或损坏的报文。
- 超时重传机制:主站发送请求后,应设置超时定时器(通常 100~1000 ms),超时未收到响应则重试。
- 多从站轮询调度:在边缘节点中,使用非阻塞 I/O 或多线程管理多个从站的轮询任务,确保实时性。
- 数据缓存与转发:解析后的数据应缓存到本地时序数据库或内存队列,再通过 MQTT/HTTP 转发到云端。