3. Modbus TCP 协议基础:协议帧结构、功能码分类与寄存器地址映射
Modbus TCP 是工业物联网边缘网关与 PLC、RTU、仪表等现场设备通信的事实标准协议。它基于 TCP/IP 栈,在以太网上传输 Modbus 应用数据单元(ADU)。掌握协议帧结构、功能码分类和寄存器地址映射,是调试边缘网关数据采集通道的必备基础。
💡 核心认知:Modbus TCP 与 Modbus RTU 的最大区别在于去掉了 CRC 校验(由 TCP 层保证)并增加了 MBAP 报文头(7 字节)。理解这一差异,就能快速定位抓包中的帧结构问题。
3.1 协议帧结构:MBAP + PDU
Modbus TCP 帧由两部分组成:MBAP 报文头(Modbus Application Protocol Header)和 PDU(Protocol Data Unit)。PDU 又包含功能码和数据域。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 事务处理标识符 | 2 | 用于请求/响应配对,客户端自增 |
| 协议标识符 | 2 | 固定为 0x0000(Modbus 协议) |
| 长度 | 2 | 后续字节数(单位标识符 + PDU 长度) |
| 单位标识符 | 1 | 从站地址(网关转发时使用) |
| 功能码 | 1 | 操作类型(读/写/诊断) |
| 数据域 | N | 寄存器地址、数量、数据值等 |
📐 帧结构速记:MBAP(7 字节)+ 功能码(1 字节)+ 数据域(N 字节)。抓包时,前 7 字节是 MBAP,第 8 字节是功能码,第 9 字节起是数据域。
3.2 功能码分类与速查表
Modbus TCP 定义了三类功能码:位操作、字操作和诊断。边缘网关最常用的是 01/02/03/04(读)和 05/06/15/16(写)。
| 功能码 | 名称 | 操作对象 | 读写方向 | 典型用途 |
|---|---|---|---|---|
| 01 (0x01) | 读线圈 | DO(数字输出) | 读 | 读取继电器/阀门状态 |
| 02 (0x02) | 读离散输入 | DI(数字输入) | 读 | 读取按钮/限位开关 |
| 03 (0x03) | 读保持寄存器 | AO(模拟输出/参数) | 读 | 读取设定值、累计量 |
| 04 (0x04) | 读输入寄存器 | AI(模拟输入) | 读 | 读取温度、压力、流量 |
| 05 (0x05) | 写单线圈 | DO | 写 | 控制单路开关 |
| 06 (0x06) | 写单寄存器 | AO | 写 | 修改单个设定值 |
| 15 (0x0F) | 写多线圈 | DO | 写 | 批量控制输出 |
| 16 (0x10) | 写多寄存器 | AO | 写 | 批量写入参数表 |
⚠️ 常见误区:功能码 03 和 04 都读 16 位寄存器,但 03 对应保持寄存器(可读可写),04 对应输入寄存器(只读)。在设备文档中,保持寄存器地址通常以 4xxxx 开头,输入寄存器以 3xxxx 开头。
3.3 寄存器地址映射:PLC 与网关的“翻译表”
寄存器地址映射是边缘网关配置的核心环节。PLC 端使用物理地址(如 40001),而 Modbus TCP 报文中的地址域使用协议地址(0x0000 起始)。网关需要完成这两者之间的转换。
3.3.1 地址映射规则
| 寄存器类型 | PLC 地址范围 | 协议地址(十六进制) | 功能码 | 数据宽度 |
|---|---|---|---|---|
| 线圈(DO) | 00001 ~ 09999 | 0x0000 ~ 0x270E | 01 / 05 / 15 | 1 bit |
| 离散输入(DI) | 10001 ~ 19999 | 0x0000 ~ 0x270E | 02 | 1 bit |
| 输入寄存器(AI) | 30001 ~ 39999 | 0x0000 ~ 0x270E | 04 | 16 bit |
| 保持寄存器(AO) | 40001 ~ 49999 | 0x0000 ~ 0x270E | 03 / 06 / 16 | 16 bit |
🔢 地址转换公式:
例如:PLC 地址 40001 → 协议地址 = 40001 - 40001 = 0x0000
PLC 地址 30005 → 协议地址 = 30005 - 30001 = 4 = 0x0004
协议地址 = PLC 地址 - 地址基址例如:PLC 地址 40001 → 协议地址 = 40001 - 40001 = 0x0000
PLC 地址 30005 → 协议地址 = 30005 - 30001 = 4 = 0x0004
3.3.2 典型映射示例(边缘网关配置)
假设某厂务系统需要采集以下数据点:
| 数据点 | PLC 地址 | 寄存器类型 | 协议地址 | 功能码 | 数据类型 |
|---|---|---|---|---|---|
| 冷冻水供水温度 | 30001 | 输入寄存器 | 0x0000 | 04 | FLOAT32(2 寄存器) |
| 冷冻水回水温度 | 30003 | 输入寄存器 | 0x0002 | 04 | FLOAT32(2 寄存器) |
| 冷却塔风机启停 | 00001 | 线圈 | 0x0000 | 01 / 05 | BOOL(1 bit) |
| 冷冻泵频率设定 | 40001 | 保持寄存器 | 0x0000 | 03 / 06 | UINT16(1 寄存器) |
3.4 实战:使用 Python 构造 Modbus TCP 请求帧
以下代码演示如何手动构造一个读保持寄存器请求帧(功能码 03),读取从地址 0x0000 开始的 2 个寄存器。
import struct
import socket
# 构造 MBAP 报文头
transaction_id = 1 # 事务标识,自增
protocol_id = 0 # 协议标识,固定 0x0000
unit_id = 1 # 单元标识(从站地址)
# 长度 = 单元标识(1) + 功能码(1) + 数据域(4) = 6
length = 6
# 构造 PDU
function_code = 0x03 # 读保持寄存器
start_address = 0x0000 # 起始地址
quantity = 2 # 读取 2 个寄存器
# 打包为字节流
mbap_header = struct.pack('>HHHB', transaction_id, protocol_id, length, unit_id)
pdu = struct.pack('>BHH', function_code, start_address, quantity)
request_frame = mbap_header + pdu
print("请求帧(十六进制):", request_frame.hex())
# 输出示例:000100000006010300000002
# 分解:0001 | 0000 | 0006 | 01 | 03 | 0000 | 0002
🧪 调试技巧:使用 Wireshark 抓包时,设置过滤条件
modbus 或 tcp.port == 502。观察 MBAP 中的 Length 字段是否与实际 PDU 长度一致,这是排查帧结构错误的第一检查点。
3.5 常见问题与避坑指南
- 地址偏移错误:PLC 地址 40001 对应协议地址 0x0000,不是 0x0001。网关配置时若填 0x0001,会读到错误的寄存器。
- 字节序问题:Modbus 默认使用 Big-Endian(大端序)。读取 32 位浮点数时,高 16 位在前,低 16 位在后。部分 PLC 使用 Little-Endian,需要在网关侧配置字节交换。
- 功能码不匹配:读模拟量必须用 03 或 04,不能用 01/02。写多寄存器必须用 16,不能用 06 逐个写(效率低)。
- 超时与重试:边缘网关默认超时通常为 1~3 秒。对于响应慢的串口透传设备,建议将超时设为 5 秒以上。
⚠️ 生产环境警示:在调试阶段,不要对正在运行的设备使用功能码 05/06/15/16 进行写操作,除非已确认写入值不会导致设备停机或安全事故。建议先在测试设备上验证写功能。