跳转至

第 7 章 Modbus TCP

Modbus TCP 是 Modbus PDU 直接跑在 TCP/IP 上的传输变体(Modbus Organization 文档:MODBUS Messaging on TCP/IP Implementation Guide V1.0b)。它把 RTU 里的"从站地址 + CRC16"换成"MBAP 头 + TCP 自身校验",其余 PDU 完全不变——这一"最小改动"哲学让 RTU 与 TCP 网关实现极薄,也让同一套应用逻辑可在两种传输间平移。机器人上 Modbus TCP 常用于慢变量跨网段接入:BMS、电力仪表、温湿度、环境传感器,通过 TCP 走到工控网络与上位。

TCP 侧去掉 CRC,网关把 RTU 设备接进来:

flowchart LR
    cli["Modbus TCP 客户端"] --> mbap["MBAP 头 + PDU<br/>无 CRC(由 TCP 保证)"]
    mbap --> srv["502 端口服务端"]
    rtu["Modbus RTU 从站"] -.->|"串口网关转换"| srv

1. 从 RTU 到 TCP:映射与差异

RTU 与 TCP 的关系可以按"哪些字段换了、哪些保持不变"来记:

层 Modbus RTU Modbus TCP
PDU(协议层) 功能码 + 数据 完全相同
ADU 头 Slave Addr(1 B) MBAP 头(7 B)
ADU 尾 CRC-16(2 B) 无(依赖 TCP 校验)
传输 RS-485 半双工串口 TCP,端口 502(IANA 注册)
会话 一次事务一帧 长连接、pipelining 允许、多客户端并发
单元标识 Slave Addr MBAP 里的 Unit ID(可为网关侧的 RTU 从站地址)

默认端口 502:/etc/services 里 modbus/tcp 502;实际部署常因权限(< 1024 需 root)或安全考虑改用 5020 / 50200 等,第 6 章「pymodbus 最小主从站示例」也用了 5020。

2. MBAP 头结构

MBAP(Modbus Application Protocol)头 7 字节,取代 RTU 的"从站地址 + CRC"两端字段。

┌───────────────────── ADU(TCP 完整帧)─────────────────────┐
│  MBAP Header (7 B)                            │  PDU (1–253 B)  │
│  TID(2) │ PID(2) │ Length(2) │ Unit ID(1)    │  FC + Data      │
└────────────────────────────────────────────────────────────────┘

四个字段各有独立语义。

2.1 事务标识与协议标识

  • Transaction Identifier(TID,2 字节):客户端自己选、服务端原样回;用来在同一 TCP 连接上把响应配对到请求。TCP 是字节流、没有消息边界,若客户端连续发出请求 A、B、C,响应可能以 A、B、C 顺序到达但也可能服务端交错返回;TID 让客户端从流里挑出属于自己某次请求的响应。
  • Protocol Identifier(PID,2 字节):固定 0x0000,标识"这是 Modbus 报文"。保留给未来其他协议共存(同一端口复用);目前所有实现都发 0x0000。

2.2 长度字段与单元标识

  • Length(2 字节):紧跟其后的字节数(Unit ID + PDU 全部),最大 253 + 1 = 254;TCP 分帧靠这个字段决定"一条完整 Modbus 报文有多长"。收方读满 MBAP 后按 Length 继续读,边界清晰。
  • Unit Identifier(1 字节):取代 RTU 的 Slave Addr。当服务端本身就是从站时,Unit ID 一般填 0xFF 或 0x00(本端);当服务端是TCP → RTU 网关时,Unit ID 就是网关下挂的 RTU 从站地址(1–247),网关据此把请求转发到 RTU 段。

要点:MBAP 头没有 CRC——TCP 层已经有校验和、重传与按序保证,Modbus 层不再重复。这也是"Modbus TCP 有 CRC"这种误解的来源:帧里没有 CRC 字段,校验由 TCP 层完成。

3. 无 CRC 与 TCP 传输语义

Modbus 应用层不再关心位翻转、丢包与乱序——TCP 已经解决。工程后果有三:

  • 响应丢失只能靠超时判定:TCP 保证"要么送达要么断开",但若服务端应用层卡住不回,客户端只能等到自己设的请求超时(1–10 s 常见);断开重连后 TID 空间需要重置。
  • 服务端断开时 TCP 会立刻反映:recv() 返回 0 或抛 ConnectionResetError;pymodbus 上层会看到 ModbusException。相比之下 RTU 上"从站没响应"和"从站响应错乱"混在超时里不易区分。
  • 网络延迟叠加到 RTT:局域网内 TCP 一般 < 1 ms;跨广域网或经 VPN 时可到几十毫秒,直接放大到 Modbus 响应时延。RTU 主站轮询设计里的"每从站 100 ms"预算在 TCP 上要重新审视。

工程惯例:Modbus TCP 端口的长连接是常态,客户端不做"每次事务新建连接";服务端应容忍客户端长时间空连(keep-alive)与突然断开。pymodbus 与 libmodbus 都内建自动重连;重试节奏与从站侧的会话上限一起配置。

4. 并发多客户端与会话管理

Modbus TCP 允许一台从站同时对多个客户端:

  • 服务端一般用多线程 / 异步事件循环,每客户端一个会话;不同客户端的请求可以在到达顺序上交错。
  • 从站侧的写操作没有事务隔离:客户端 A 与 B 同时向 4x 空间某地址写不同值,最终值是最后到达服务端并被处理的那次;不存在"锁"或"版本"语义。工程上如需要并发写安全,由应用层设计(单一写主、其他只读;或用某几个寄存器做互信号)。
  • 读并发无冲突:任意时刻读回的都是"从站当前值",不违反语义。

Modbus TCP 不支持多主。多个客户端并发访问只是"多个主站对一个从站",从站仍被动响应、不主动上报;跨从站的协调、事件通知、订阅推送都不在协议范围内;需要"多主"或"事件驱动"时应改用其他协议(OPC-UA、MQTT 桥接),事件告警则另开 SNMP trap 或邮件通道,与 Modbus 的主从模型无关。

5. 网关注册与 RTU / TCP 桥接

Modbus TCP 常见拓扑之一:上位机走 TCP、现场设备走 RTU,中间一台TCP / RTU 网关。

┌────────┐   TCP:502    ┌────────────┐   RS-485 (RTU)   ┌────────┐┌────────┐┌────────┐
│ 上位机  │─────────────→│  Modbus    │─────────────────→│ 从站 1 ││ 从站 2 ││ 从站 3 │
│(client)│              │  网关       │                  │ unit=1 ││ unit=2 ││ unit=3 │
└────────┘              └────────────┘                  └────────┘└────────┘└────────┘
  • 客户端在 MBAP 的 Unit ID 里填目标 RTU 从站地址(1–247),网关据此决定往 RTU 段发往哪个 Slave Addr。
  • 网关侧同时是 TCP 服务器与 RTU 主站,收到 TCP 请求 → 剥 MBAP 头 → 保留 PDU → 拼 RTU ADU(加 Slave Addr + CRC)→ 串口发出;响应按反向流程处理。
  • 网关不"缓存"数据(除非实现做了数据聚合);一次 TCP 请求对应一次 RTU 事务,RTT 相加。

工程坑:

  • 网关串行化:网关对同一条 RTU 段的多个 TCP 客户端请求会串行发出(RTU 半双工),后到的请求要排队;若客户端按 TCP pipelining 快速发多条,网关前部积压到某阈值可能触发 0x06 SERVER DEVICE BUSY。控制客户端并发度、按 RTU 段的实际吞吐预算发。
  • 广播语义:RTU 段广播(Unit ID = 0)通过网关时行为不一致——有的网关直接返回 ACK 而不等所有从站,有的转发但丢弃响应。工程上避免通过网关广播。

6. RTU over TCP 的另一种扩展

RTU over TCP(也叫 Modbus RTU Serial over TCP、"透传")与 Modbus TCP 不是同一个东西:

维度 Modbus TCP RTU over TCP
头 MBAP 7 B 无(或自定义分隔)
尾 无 CRC-16 2 B
帧内容 标准 PDU 完整 RTU ADU 塞进 TCP
分帧 靠 MBAP Length 靠超时或长度约定
语义 原生 TCP 串口透传

RTU over TCP 常见于串口服务器(如 Moxa NPort、有人 USR-TCP232)——设备本身只讲 RTU,串口服务器把 TCP 收到的字节直接透传到 RS-485,串口侧仍以 t3.5 静默分帧。协议层看是 Modbus RTU、传输层是 TCP;客户端需要按 RTU 帧格式打(含 Slave Addr + CRC)、并按长度或超时判断帧边界。

pymodbus 里 Modbus TCP 走 ModbusSocketFramer(FramerType.SOCKET),RTU over TCP 走 ModbusRtuFramer(FramerType.RTU);两者对帧头与帧尾的判定规则不同,选错会把 MBAP 头当数据或把 CRC 当载荷,响应解析随之失败。

判据:Modbus TCP 前 7 字节是 MBAP 头——TID(可任意)│ PID 00 00 │ Length │ Unit ID,功能码从第 8 字节起;RTU over TCP 没有 MBAP,功能码在第 2 字节、末尾 2 字节是 CRC,按 Slave Addr + PDU + CRC 解析。

7. pymodbus 与 Wireshark 抓包对照

以下示例展示一台 TCP 服务端与一台客户端之间的一次读取,并用 tshark 解出 MBAP 字段。运行环境:Ubuntu 22.04、Python 3.10、pymodbus 3.6、tshark 3.6;服务端与第 6 章「pymodbus 最小主从站示例」相同(略)。

发起一次读多个保持寄存器:

## 运行环境:Ubuntu 22.04(Python 3.10),pymodbus 3.6
import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def one_read():
    cli = AsyncModbusTcpClient("127.0.0.1", port=5020)
    await cli.connect()
    # 读 4x 空间起始 0、数量 4;unit=1(对应网关下的 1 号 RTU 从站,或本地从站)
    rr = await cli.read_holding_registers(address=0, count=4, slave=1)
    # rr.registers 为 4 个 16 位寄存器值,顺序与地址 0–3 对应
    cli.close()

asyncio.run(one_read())

tshark 抓包与解码:

## 运行环境:Ubuntu 22.04,tshark 3.6.14
sudo tshark -i lo -Y "tcp.port == 5020 && modbus" -T fields -e hex -e modbus_mbap \
   -e modbus.func_code -e modbus.read_write_addr -e modbus.read_write_qty

tshark 输出(一次 read_holding_registers 请求 + 响应):

00 01 00 00 00 06 01 03 00 00 00 04     0001..01    3    0    4
00 01 00 00 00 0b 01 03 08 00 00 00 00 00 00 00 00  0001..01    —    —    —

第一行请求:TID=0x0001、PID=0x0000、Length=0x0006、Unit ID=0x01、FC=0x03、Addr=0x0000、Qty=0x0004。第二行响应:TID 回显、Length=0x000B、Unit ID=0x01、FC=0x03、ByteCount=0x08、后 8 字节为 4 个寄存器的数据(本例全 0)。

对照要点:MBAP 的 Length 字段是"Unit ID + PDU 全部字节数"——响应里 Length=0x000B = 1(Unit ID)+ 1(FC)+ 1(ByteCount)+ 8(数据),共 11 字节,字段自洽。客户端把 Length 用来切包;TCP 分段落在 MBAP 中段的场景下靠 Length 补齐才是完整 Modbus 报文。

8. 要点

  • Modbus TCP = Modbus PDU + MBAP 头:PDU 与 RTU 完全一致,MBAP 7 字节(TID / PID=0x0000 / Length / Unit ID)取代 Slave Addr 与 CRC;默认端口 502。
  • TID 用于响应配对:长连接上多请求并发时,客户端靠 TID 从字节流里挑回自己的响应;PID 恒 0x0000,保留给未来协议共存。
  • CRC 由 TCP 承担:帧内不再有 CRC;响应丢失只能靠超时判定,断开立即反映;跨广域网时 RTT 直接进入响应时延预算。
  • 多客户端并发不等于多主:写并发无事务隔离、从站仍被动应答,仍是主从模型。需要事件驱动或跨从站协调请换协议(OPC-UA、MQTT 桥接)。
  • Unit ID 承载网关下游地址:网关场景下 Unit ID = 目标 RTU Slave Addr;同一物理 TCP 连接可以访问多个下游从站,网关做 PDU 转发与帧格式转换。
  • RTU over TCP ≠ Modbus TCP:串口透传方案里 ADU 仍是完整 RTU(含 Slave Addr 与 CRC),MBAP 缺席;两种 framer 按 MBAP 头与 CRC 尾的差异区分,不可混用。

参考文献与延伸阅读

  • Modbus Organization. Modbus TCP 与串口规范入口. 规范
  • pymodbus. 文档. 文档
  • Wireshark. 文档. 文档