Modbus TCP 协议基础

Modbus 是工业现场最普及的通信协议,而 Modbus TCP 是它在以太网上的实现:把经典的 Modbus 报文直接塞进 TCP 负载,用 TCP 保证传输可靠,省去了 RTU 里的 CRC 校验和从站地址(地址信息由 TCP 连接本身承载)。

与 RS485 上的 Modbus RTU 相比,Modbus TCP 有几个明显差异:没有设备地址限制(每个连接一个从站)、吞吐更高、能同时承载多个连接,但也引入了 TCP 连接管理与并发处理的问题。一个 Modbus TCP 服务端默认监听 502 端口,客户端通过三次握手建立连接后收发报文。

本文核心知识点
  • MBAP事务标识、协议标识、长度、单元标识。
  • 功能码03 读保持寄存器、06 写单寄存器。
  • 映射寄存器地址与工艺量一一对应。
  • 多站轮询调度、超时重试与并发连接。

对工控工程师来说,Modbus TCP 最大的价值是"拿来即用":几乎所有 PLC、仪表、变频器都内置支持,不需要专用驱动,用 TCP 工具就能先手动把寄存器读一遍,再写程序自动化。

Modbus TCP 配图:深蓝渐变背景上左侧工业控制器机柜延伸出发光网络线缆,连接右侧三个设备节点与数据包方块的扁平科技插画
Modbus TCP 把 PLC、仪表与上位机连成一张可寻址的以太网。

MBAP 报文与功能码

Modbus TCP 请求由 MBAP 报文头加 PDU 组成。MBAP 共 7 字节:事务标识(2 字节,客户端自增,用于匹配响应)、协议标识(2 字节,恒为 0)、长度(2 字节,后续字节数)、单元标识(1 字节,用于网关背后的从站号)。PDU 则是功能码加数据。

// 读保持寄存器请求(功能码 03)报文构造
// 事务ID(2) | 协议ID(2)=0 | 长度(2) | 单元ID(1)
// 功能码(1)=0x03 | 起始地址(2) | 寄存器数量(2)
uint8_t req[12] = {
  0x00, 0x01,             // 事务标识
  0x00, 0x00,             // 协议标识
  0x00, 0x06,             // 长度 = 6
  0x01,                   // 单元标识
  0x03,                   // 功能码:读保持寄存器
  0x00, 0x00,             // 起始地址 0
  0x00, 0x0A              // 读 10 个寄存器
};

常用功能码就几个:01 读线圈、03 读保持寄存器、04 读输入寄存器、05 写单线圈、06 写单寄存器、16 写多寄存器。保持寄存器可读可写,输入寄存器只读,采集系统绝大多数读 03 和 04。

寄存器映射与字节序

通信本身不关心寄存器代表什么,映射表才是工程的灵魂。每个寄存器地址对应一个工艺量:温度、压力、转速、运行状态位。做系统前先和厂商拿到点表,确认地址、数据类型、量程与字节序,否则读回来的数字对不上现场。

  • 地址偏移:Modbus 地址从 0 开始编号,但很多设备用 40001 风格(4xxxx 表示保持寄存器),换算时注意减 1。
  • 数据类型:16 位整数、32 位浮点、32 位整数,两个寄存器拼一个浮点是常态。
  • 字节序:大端(高字节在前)最常见,但部分设备用字交换,必须按点表核对。
  • 缩放:原始值乘量程系数才是工程量,比如 0.1 精度对应值要除以 10。

建议把映射表做成配置文件或数据表,而不是散落在代码里:新增设备、改地址、调系数时只改配置,不动逻辑代码。这样一套驱动可以复用给整条产线的不同设备。

服务端(从站)实现

设备或网关作为 Modbus TCP 服务端时,要能接受多个客户端连接,并正确处理并发读写。核心是把"网络层"和"业务层"分开:网络层解析 MBAP 与功能码,业务层按地址读写真实数据区。

// 服务端读寄存器处理(伪代码)
int handle_read_holding(int start, int count,
                        uint16_t *out) {
  for (int i = 0; i < count; i++) {
    int idx = start + i;
    if (idx < 0 || idx >= REG_COUNT) {
      return MODBUS_ERR_ILLEGAL_ADDR;  // 越界
    }
    out[i] = regs[idx];   // 从真实数据区取值
  }
  return 0;
}

注意数据一致性:读请求正在打包时,如果后台任务同时改写寄存器,会读到不一致的中间值。常见的做法是数据区用互斥锁或临界区保护,或者把工艺数据做成"发布-快照"机制,保证读到的是一致快照。