2. Modbus协议基础

Modbus 是工业自动化领域最广泛使用的串行通信协议之一,它的设计哲学非常朴素:一个主站、多个从站,主站问、从站答。理解这套主从架构与请求响应模型,是后续开发 RTU/TCP 驱动、调试现场设备、排查通信故障的根基。本节从通信角色、报文交互、功能码分类到数据模型与寄存器类型,逐层拆解 Modbus 协议的核心骨架。

一、主从架构:谁说话,谁应答

Modbus 网络采用单主多从的总线型结构。主站(Master,也称 Client)是唯一主动发起通信的一方,从站(Slave,也称 Server)只能被动响应,绝不主动发送数据。在 Modbus RTU 中,从站通过 1~247 的从站地址区分;在 Modbus TCP 中,则通过 IP 地址 + 单元标识符(Unit ID)定位目标设备。

这种架构带来两个关键约束:一是同一时刻总线上只能有一个主站,否则会出现报文冲突;二是从站之间不能直接通信,若两个从站需要交换数据,必须由主站分别读取后再写入,即“主站中转”。

RTU 是主从轮询(Polling)模式,主站按顺序逐个访问从站;TCP 虽然底层是以太网,但应用层仍保持主从请求响应语义,只是允许多个客户端连接同一服务器。

二、请求响应模型:一次完整交互的四个阶段

无论 RTU 还是 TCP,一次 Modbus 交互都遵循“请求—响应”模型,可拆解为四个阶段:

  1. 主站组帧:主站根据目标从站地址、功能码、起始地址、数量等参数构造请求帧。
  2. 发送与等待:RTU 通过串口发送,TCP 通过 Socket 发送,随后主站进入等待响应状态,并启动超时计时。
  3. 从站解析与执行:从站校验地址、CRC/长度、功能码是否支持,然后读取或写入对应寄存器。
  4. 从站应答:正常时返回功能码 + 数据;异常时返回“功能码 | 0x80” + 异常码。

下面是一次读取保持寄存器的典型请求与响应示意(以 RTU 为例):

请求:01 03 00 00 00 02 C4 0B
      从站地址 01 | 功能码 03 | 起始地址 0000 | 数量 0002 | CRC

响应:01 03 04 00 0A 00 14 7B 8F
      从站地址 01 | 功能码 03 | 字节数 04 | 数据 000A 0014 | CRC

异常:01 83 02 C0 F1
      从站地址 01 | 异常功能码 83 | 异常码 02(非法数据地址)

开发驱动时,超时时间、重试次数、帧间隔是三个必须显式配置的参数。RTU 中帧间至少需要 3.5 个字符时间的静默间隔来界定帧边界;TCP 则由 MBAP 头中的长度字段界定,不依赖时间间隔。

三、功能码分类:四类操作,一张表看清

功能码是 Modbus 的“动词”,决定从站执行什么操作。常用功能码可按操作对象分为四类:

功能码名称操作对象读写属性
0x01读线圈线圈(Coil)读
0x02读离散输入离散输入(Discrete Input)读
0x03读保持寄存器保持寄存器(Holding Register)读
0x04读输入寄存器输入寄存器(Input Register)读
0x05写单个线圈线圈写
0x06写单个寄存器保持寄存器写
0x0F写多个线圈线圈写
0x10写多个寄存器保持寄存器写

功能码最高位为 1 时表示异常响应,例如 0x03 的异常响应为 0x83。异常码常见有:01 非法功能、02 非法数据地址、03 非法数据值、04 从站设备故障、05 确认、06 从站忙。

四、数据模型与寄存器类型:四个独立地址空间

Modbus 将数据抽象为四个相互独立的地址空间,每个空间有自己的地址范围,互不重叠。这是初学者最容易混淆的地方:同样是“地址 0”,在线圈、离散输入、输入寄存器、保持寄存器中代表完全不同的数据。

  • 线圈(Coil):可读可写,1 位,常用于控制继电器、阀门、启停信号。
  • 离散输入(Discrete Input):只读,1 位,常用于采集限位开关、按钮、故障触点。
  • 输入寄存器(Input Register):只读,16 位,常用于采集温度、电压、电流等模拟量。
  • 保持寄存器(Holding Register):可读可写,16 位,常用于设定值、参数配置、PID 目标值。

传统 Modbus 文档中常使用“5 位/6 位地址”表示法,例如 40001 表示保持寄存器第 1 个,30001 表示输入寄存器第 1 个。开发驱动时,协议帧中实际传输的是从 0 开始的偏移地址,因此 40001 对应帧内地址 0x0000,40002 对应 0x0001,务必注意“文档地址”与“协议地址”之间的换算。

常见误区:把 40001 直接填入报文起始地址。正确做法是减去 40001 得到偏移量 0。若设备手册写“寄存器地址 40001”,驱动中应发送 0x0000;写“地址 1”时则要确认它指的是 1-based 还是 0-based。

五、案例与复盘:一次读温失败的分析

某现场使用 Modbus RTU 读取温度变送器,主站发送 01 04 00 00 00 01,从站无响应。排查过程如下:

  1. 确认从站地址:设备实际地址为 2,请求中写成了 1,地址不匹配导致从站静默丢弃。
  2. 确认功能码:温度变送器数据位于输入寄存器,应使用 0x04,而非 0x03。
  3. 确认串口参数:波特率 9600、8 数据位、无校验、1 停止位,与设备一致。
  4. 确认接线:A/B 线接反会导致电平翻转,表现为完全无响应或 CRC 错误。

修正后请求为 02 04 00 00 00 01,从站返回 02 04 02 00 FA,即温度值 250(0x00FA),按 0.1℃ 分辨率换算为 25.0℃。这个案例说明:地址、功能码、串口参数、接线是 Modbus 调试的四道关卡,任何一道出错都会表现为“无响应”或“异常码”。

驱动开发建议:把“从站地址 + 功能码 + 起始地址 + 数量”封装为请求对象,把“超时、重试、异常码映射”封装为统一错误处理层,这样 RTU 与 TCP 可以复用同一套业务逻辑,只在传输层做差异化实现。
Modbus 主从架构与请求响应模型 主站 Master 唯一主动发起方 组帧 / 超时 / 重试 RTU 串口 或 TCP Socket Modbus 总线 / 网络 从站 01 被动响应 校验地址 / CRC 执行读 / 写 从站 02 独立地址空间 不主动发送 从站 N 地址 1~247 TCP 用 Unit ID ① 请求 Request ② 响应 Response ③ 异常响应 功能码 | 0x80 + 异常码 Modbus 四个独立数据区 线圈 1位 可读写 离散输入 1位 只读 输入寄存器 16位 只读 保持寄存器 16位 可读写 功能码 01/02/03/04 读 · 05/06/0F/10 写 文档地址 40001 → 协议偏移 0x0000 常用功能码 0x01 读线圈 0x02 读离散输入 0x03 读保持寄存器 0x04 读输入寄存器 0x05/0x06 写单个 0x0F/0x10 写多个

掌握以上四点后,再看任意 Modbus 设备手册,都能快速定位到“用哪个功能码、访问哪个数据区、地址如何换算”。这是驱动开发中编写请求构造、响应解析、异常处理三部分代码的直接依据。