← 返回博客列表

一、什么是 Modbus

Modbus是一种开放、简单、广泛使用的工业通信协议,由美国 Modicon 公司(现施耐德电气)于 1979 年发明,最初用于其生产的可编程逻辑控制器(PLC)之间通信。Modbus 是工业自动化领域事实上的标准,被誉为"工业界的 HTTP"——几乎所有工业设备都支持它。

Modbus 协议家族演进史 1979 · Modbus • Modicon 公司发明 • 用于 PLC 互连 • 串行 RS-232 • 主从 + 轮询 里程碑: 工业通信鼻祖 开放免费 简单易实现 1980s · Modbus RTU • RS-485 串行 • 二进制帧 • CRC16 校验 • 1.5 字符间隔 里程碑: 主流串行方案 支持多节点 抗干扰强 1990s · Modbus Plus • 高速令牌总线 • 1 Mbps • 施耐德私有 • 后逐渐式微 地位: 过渡产品 少用 1997 · Modbus TCP • 标准以太网 • TCP/IP 协议栈 • 端口 502 • 客户端/服务端 里程碑: 融入 IT 世界 支持互联网 现代主流
图 1-1:Modbus 协议家族的演进史(1979 → 现在)

Modbus 是应用层协议,独立于底层物理层和传输层。它有三大主流变体:

关键术语:开放协议
Modbus 是真正的"开放协议":① 规范公开免费;② 无专利授权费;③ 任何厂商可自由实现;④ 已被纳入 IEC 61158 和 IEC 61784 标准。这种开放性使其在 40 多年后仍占据工业通信"半壁江山"。

二、Modbus 与其他工业协议对比

Modbus 在工业通信生态中扮演"入门级"角色,与 EtherCAT、PROFINET、CANopen 等形成互补。

工业通信协议能力分布图 实时性 弱 强 协议复杂度 ↑ 高 Modbus RTU 最简单 Modbus TCP 以太网版 CANopen 中等 PROFINET 复杂 EtherCAT 最强 注:圆形大小代表市场普及度;位置反映实时性与复杂度
图 2-1:Modbus 在工业通信生态中的定位
对比维度 Modbus RTU Modbus TCP EtherCAT CANopen
物理层 RS-485/RS-232 以太网 100M 以太网 100M CAN 双绞线
通信模型 主从轮询 客户端/服务端 主从 + 飞越读取 主从 + 事件
周期速度 10~100ms 1~10ms 0.1ms 1~10ms
节点数 32(RS-485) 理论无限 65535 127
同步性 无 无 DC 纳秒级 SYNC 同步
开放性 完全开放 完全开放 开放 开放
实现成本 极低 低 高 中
典型场景 仪表/变频器 SCADA/IO 多轴伺服 移动机械
选型建议
• 简单仪表读取、对实时性要求不高(>100ms) → Modbus RTU(最便宜、最普及)
• 需要远程监控、对接 SCADA/上位机 → Modbus TCP(IT 友好、易扩展)
• 需要微秒级多轴运动控制 → EtherCAT(见同系列 EtherCAT 文章)
• 车载、移动设备 → CANopen(抗干扰、节点多)

三、Modbus 通信模型:主从架构

Modbus 采用经典的主从(Master/Slave)架构。在 RTU 中叫"主站/从站",在 TCP 中改叫"客户端/服务端"但本质相同:只有主站能主动发起请求,从站只能被动响应。

Modbus 主从通信模型 主站 Master PLC / IPC SCADA 职责: 发起请求 轮询从站 超时重发 特权: 主动权 从站 1 地址 0x01 变频器 Slave 1 从站 2 地址 0x02 温度仪表 Slave 2 从站 3 地址 0x03 电力仪表 Slave 3 从站 N 地址 0xN IO 模块 Slave N 请求 响应 RS-485 总线 / 以太网 通信规则 ① 同一时刻只有一个主站发起请求(RTU 总线不可多主) ② 主站必须等待从站响应或超时后才能发下一个请求 ③ 从站不能主动上报,需主站"问"才能"答"(广播除外)
图 3-1:Modbus 主从架构与通信规则

3.1 三种通信模式

Modbus 三种通信模式 ① 单播 Unicast 主站 地址 2 从2 从3 指定地址的从站 响应主站 最常用 ② 广播 Broadcast 主站 地址 0 从1 从2 从N 所有从站接收 但不响应 用于批量写 ③ 组播(仅 TCP) 客户端 服务端 A 基于 IP 组播 极少使用 少见
图 3-2:单播、广播、组播三种通信模式

3.2 一问一答的时序

Modbus 请求-响应时序 主站 从站 请求帧:[地址 1][功能码 3][起始 0][数量 2][CRC] 处理 ~10ms 响应帧:[地址 1][功能码 3][字节数 4][数据...][CRC] 等待 超时? 主站发起 → 等待响应 → 超时则重发或转下一从站
图 3-3:Modbus 一问一答时序(同步轮询模型)
关键限制
Modbus RTU 在同一 RS-485 总线上只能有一个主站。如果需要多主站共享从站,必须用 Modbus TCP(多客户端可同时连接同一服务端,但服务端仍需处理并发)。

四、Modbus 数据模型:四类数据

Modbus 把所有数据抽象为四类数据表,每类有不同的读写权限和数据宽度。这是 Modbus 设计上最优雅、最经典的部分。

Modbus 四类数据模型 ① 线圈 Coil 数据类型: 单个 BIT 访问权限: 读 / 写 地址范围: 00001~09999 典型用途: 继电器输出 指示灯 电磁阀 电机启停 类比: 开关量输出 ② 离散输入 Discrete Input 数据类型: 单个 BIT 访问权限: 只读 地址范围: 10001~19999 典型用途: 按钮状态 限位开关 传感器数字量 故障信号 类比: 开关量输入 ③ 输入寄存器 Input Register 数据类型: 16 位 WORD 访问权限: 只读 地址范围: 30001~39999 典型用途: 温度采集 电压电流 A/D 转换值 计数器值 类比: 模拟量输入 ④ 保持寄存器 Holding Register 数据类型: 16 位 WORD 访问权限: 读 / 写 地址范围: 40001~49999 典型用途: 设定值 D/A 输出 运行参数 累计值 类比: 模拟量输出/参数
图 4-1:Modbus 四类数据模型(线圈 / 离散输入 / 输入寄存器 / 保持寄存器)
地址编号约定(5 位制)
Modbus 历史使用 5 位十进制编号,首位区分类型:
• 0xxxx → 线圈(可读写 1 位)
• 1xxxx → 离散输入(只读 1 位)
• 3xxxx → 输入寄存器(只读 16 位)
• 4xxxx → 保持寄存器(可读写 16 位)
注意:协议帧中实际传输的"寄存器地址"是去掉首位后的0 基偏移。例如 40001 对应协议地址 0x0000。

4.1 大端字节序与 32 位数据

Modbus 寄存器宽度为 16 位,但很多实际数据(如 IEEE 754 浮点、32 位整数)需要 32 位。这时需要用两个连续寄存器存储一个 32 位数据,这就涉及字节序问题。

32 位浮点数的 4 种字节序 原值:0x12345678(IEEE 754 浮点 2.305e-13) 大端 Big-Endian (ABCD): 寄存器 N=0x1234 寄存器 N+1=0x5678 最常见 字交换 Word-Swap (DCBA): 寄存器 N=0x5678 寄存器 N+1=0x1234 常见 字节交换 Byte-Swap (BADC): 寄存器 N=0x3412 寄存器 N+1=0x7856 少见 字+字节交换 (CDAB): 寄存器 N=0x7856 寄存器 N+1=0x3412 较常见
图 4-2:Modbus 32 位数据的四种字节序排列
实战陷阱
不同厂家设备使用不同字节序!施耐德 PLC 通常用 CDAB,西门子用 ABCD,部分国产仪表用 BADC。读取 32 位数据前必须先确认设备文档,或在调试时观察原始字节流判断。

五、Modbus 功能码详解

功能码(Function Code)是请求帧的"动作指令",告诉从站要做什么操作。功能码占 1 个字节,0x01~0x06 是最常用的 6 个。

功能码 操作 数据类型 典型用途
0x01 读线圈 多个 Bit(可读写) 批量读取继电器输出状态
0x02 读离散输入 多个 Bit(只读) 批量读取开关量输入
0x03 读保持寄存器 多个 Word(可读写) 读设定值、运行参数
0x04 读输入寄存器 多个 Word(只读) 读温度、电压等采集值
0x05 写单个线圈 1 个 Bit 启停单台电机
0x06 写单个保持寄存器 1 个 Word 设定单个目标值
0x0F 写多个线圈 多个 Bit 批量控制继电器
0x10 写多个保持寄存器 多个 Word 批量设定参数
0x17 读/写多个寄存器 多个 Word 原子操作:先读后写
最常用功能码 0x03(读保持寄存器)实例 请求帧(主站 → 从站) 字节1 0x01 从站地址 字节2 0x03 功能码 字节3-4 0x00 0x0A 起始地址=10 字节5-6 0x00 0x02 读2个寄存器 字节7-8 0x25 0xCA CRC16 校验 含义:从 1 号从站,读保持寄存器 #10 起 2 个连续寄存器 响应帧(从站 → 主站) 0x01 从站地址 0x03 功能码 0x04 字节数=4 0x00 0x0A 寄存器10=10 0x01 0x90 寄存器11=400 0x78 0x62 CRC16 含义:返回 4 字节数据(2 个寄存器 × 2 字节)= [10, 400] 注:地址在协议帧中是 0 基索引,故寄存器 40011 在帧中地址为 10
图 5-1:功能码 0x03 的请求与响应帧实例

5.1 异常响应与异常码

当从站无法处理请求时(如地址越界、功能码不支持),会返回异常响应——把功能码最高位置 1(异或 0x80),并附带异常码。

异常码 名称 含义
0x01Illegal Function不支持此功能码
0x02Illegal Data Address地址越界
0x03Illegal Data Value值超出范围
0x04Slave Device Failure从站内部故障
0x05Acknowledge已收到,处理中(长任务)
0x06Slave Device Busy从站忙,请重试
0x0AGateway Path Unavailable网关路径不可用

六、Modbus RTU:串行链路实现

Modbus RTU(Remote Terminal Unit)是 Modbus 在串行链路上的实现,物理层通常用 RS-485(多节点)或 RS-232(点对点)。它使用二进制紧凑编码,比 ASCII 模式效率高出一倍。

Modbus RTU 典型网络拓扑 主站 PLC/IPC RS-485 从1 变频器 从2 仪表 从3 IO模块 从4 软启动 终端 电阻 RTU 网络关键参数 • 波特率:1200 / 2400 / 4800 / 9600 / 19200 / 38400 / 115200 bps • 数据格式:8 位数据位,1/2 位停止位,无/偶/奇校验 • 节点数:RS-485 标准下最多 32 个物理节点(含主站) • 总线长度:最大 1200 米(与波特率成反比) • 终端电阻:120Ω × 2,安装在总线两端
图 6-1:Modbus RTU 网络(RS-485 总线型拓扑)

6.1 RTU 的帧定界难题

RTU 帧没有起始/结束标志符,依靠 3.5 个字符时间的空闲间隔作为帧边界。这是一个聪明但容易出错的设计。

Modbus RTU 帧定界原理 ≥3.5 字符空闲 [地址][功能码][数据][CRC16] 完整 RTU 帧 ≥3.5 字符空闲 3.5 字符时间 = 9600 bps 下约 3.65ms 115200 bps 下约 0.28ms(规定不低于 1.75ms) 规则: 帧内字符间隔必须 < 1.5 字符时间,否则帧非法;帧间必须 ≥ 3.5 字符时间
图 6-2:RTU 帧通过 3.5 字符空闲间隔定界
关键陷阱
• Linux 串口驱动可能因中断合并、调度延迟导致帧内字符间隔超过 1.5 字符,造成"帧分裂"
• 高波特率(115200)下 3.5 字符时间不足 1ms,普通操作系统难以保证
• 解决方案:① 用低波特率(9600/19200);② 使用带硬件 FIFO 的串口芯片(如 FT232);③ 启用串口低延迟模式(Linux TTY 低延迟标志)

七、Modbus RTU 帧结构

RTU 帧由从站地址、功能码、数据、CRC16四部分组成,无起始/结束标志,最紧凑高效。

Modbus RTU 帧结构(请求 0x03 为例) 从站地址 1 字节 0x01~0xF7 功能码 1 字节 0x03 数据区 变长 起始地址 + 寄存器数 CRC16 2 字节 低字节在前 具体示例:读从站1保持寄存器40011~40012 字节1: 0x01(从站地址) 字节2: 0x03(功能码) 字节3: 0x00 字节4: 0x0A 字节5: 0x00 字节6: 0x02 字节7: 0x25 8: 0xCA 总 8 字节 → 地址 1, 功能码 03, 起始地址 0x000A=10, 数量 0x0002=2, CRC16 = 0xCA25(小端序) 注:协议地址 = 寄存器编号 - 1,故 40011 对应协议地址 10 = 0x000A
图 7-1:Modbus RTU 帧结构详解

7.1 三种帧类型对比

Modbus RTU vs ASCII vs TCP 帧对比 RTU: [空闲][地址 1B][功能码 1B][数据 N B][CRC 2B][空闲] 最紧凑,二进制 ASCII: [:][地址 2字符][功能码 2字符][数据 N字符][LRC 2字符][CR][LF] 可读,效率低,用 : 分隔 TCP: [MBAP 7B(事务ID+协议ID+长度+单元ID)][功能码 1B][数据 N B] 无 CRC(TCP 已校验),无地址(用单元ID)
图 7-2:三种 Modbus 变体的帧结构对比

八、CRC16 校验:可靠性的基石

Modbus RTU 使用 CRC-16-Modbus算法(多项式 0xA001,即 0x8005 反转),对地址、功能码、数据全部计算 2 字节校验值,低字节在前传输。这是 RTU 抗干扰的关键。

CRC16 计算流程 初始化 CRC = 0xFFFF 取下一字节 遍历每个字节 XOR 低 8 位 CRC ^= byte 循环 8 次 右移 1 位 最低位=1? 是 → CRC ^= 0xA001 继续循环 所有字节处理完? 输出 CRC:低字节在前,高字节在后 [CRC_Low][CRC_High]
图 8-1:CRC-16-Modbus 计算流程

8.1 CRC16 C 语言实现

// CRC-16-Modbus 算法(查表法,速度优先)
#include <stdint.h>

static const uint16_t crc_table[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    // ...(共 256 项,此处省略,可在线生成)
};

uint16_t modbus_crc16(const uint8_t *data, size_t len) {
    uint16_t crc = 0xFFFF;
    while (len--) {
        crc = (crc >> 8) ^ crc_table[(crc ^ *data++) & 0xFF];
    }
    return crc;
}

// 用法:构造帧时附加 CRC(低字节在前)
uint8_t frame[8] = {0x01, 0x03, 0x00, 0x0A, 0x00, 0x02};
uint16_t crc = modbus_crc16(frame, 6);
frame[6] = crc & 0xFF;        // 低字节
frame[7] = (crc >> 8) & 0xFF;  // 高字节
// 结果:frame = 01 03 00 0A 00 02 25 CA

九、Modbus TCP:以太网实现

Modbus TCP(也叫 Modbus TCP/IP)于 1997 年发布,将 Modbus 应用层协议承载在标准 TCP/IP 协议栈之上,使用固定端口 502,让工业设备能直接接入 IT 网络。

Modbus TCP 协议栈分层 应用层:Modbus 协议(功能码 + 数据) MBAP 头(Modbus Application Protocol Header,7 字节) TCP 协议(端口 502,提供可靠字节流) IP 协议(寻址、路由) 以太网物理层(100M/1G 双绞线/光纤)
图 9-1:Modbus TCP 协议栈五层结构

9.1 Modbus TCP 的关键变化

RTU → TCP 的三大改造 ① 去除 CRC RTU 用 CRC16 防 止串行误码 TCP 已有校验和 → 无需重复校验 节省 2 字节 TCP 重传机制 保证可靠传输 → 完全省略 CRC 校验 ② 替换地址 RTU 用从站地址 区分设备(1 字节) TCP 用 IP 地址 区分设备 引入单元 ID 用于网关后端 串联 RTU 设备 单元 ID 保留 直连设备通常 0xFF ③ 加事务 ID RTU 同步轮询 一问一答 TCP 可异步多请求 用事务 ID 配对 支持并发 多个请求可同时 "在途"(in-flight) 客户端模型 不再叫主从
图 9-2:Modbus TCP 相对 RTU 的三大改造

9.2 TCP 网络拓扑

Modbus TCP 典型网络拓扑 客户端 SCADA 客户端 PLC 交换机 100M/1G 服务端 变频器 192.168.1.10 服务端 IO 模块 192.168.1.11 服务端 电力仪表 192.168.1.12 网关 → RTU 总线 192.168.1.254 多客户端可同时访问同一服务端;服务端通过 IP 区分;网关可桥接 RTU 总线 客户端 → 服务端:发起 TCP 连接 → 端口 502 → 发 Modbus 帧 → 关连接或保持长连接 长连接效率高,避免反复 TCP 握手开销
图 9-3:Modbus TCP 网络拓扑(多客户端/多服务端/网关混合)

十、Modbus TCP 帧结构(MBAP 头)

Modbus TCP 帧由 MBAP 头(7 字节)+ PDU(协议数据单元)组成。MBAP 头替代了 RTU 的从站地址和 CRC。

Modbus TCP 帧结构(MBAP + PDU) 事务 ID 2 字节 配对请求/响应 协议 ID 2 字节 恒为 0x0000 长度 2 字节 后续字节数 单元 ID 1 字节 从站地址 功能码 1 字节 PDU 起始 数据区 变长 地址 + 数量 / 数据 ← MBAP 头 7 字节 → ← PDU → 实例:读保持寄存器(与 RTU 例对应) 00 01 00 00 00 06 FF 03 00 0A 00 02 事务ID=1 协议ID=0 长度=6 单元ID=255 功能码=3 起始=10 数量=2 总 12 字节,比 RTU 多 4 字节(MBAP 头比地址+CRC 多 4 字节)
图 10-1:Modbus TCP 帧结构与 MBAP 头详解
MBAP 头字段说明
• 事务 ID:客户端自增计数,用于异步请求-响应配对。响应帧必须回填相同 ID
• 协议 ID:0x0000 表示 Modbus 协议(保留给未来其他协议)
• 长度:从单元 ID 起的字节数(含单元 ID + PDU)
• 单元 ID:用于通过 TCP-RTU 网关访问后端 RTU 从站;直连设备通常用 0x00 或 0xFF

十一、Modbus RTU vs TCP 全面对比

对比维度 Modbus RTU Modbus TCP
物理层 RS-485 / RS-232 以太网 100M/1G
通信模型 主从(轮询) 客户端/服务端(并发)
寻址方式 1 字节从站地址(1~247) IP 地址 + 单元 ID(0~255)
帧定界 3.5 字符空闲时间 TCP 字节流(无需定界)
校验 CRC16(必选) TCP 校验和(无需 CRC)
波特率 1200~115200 bps 100 Mbps+
响应时间 10~100 ms 1~10 ms(局域网)
节点数 ≤32(RS-485 标准) 理论无限(受带宽限制)
拓扑 总线型 星型/树型
多主 不支持 支持(多客户端)
穿透 NAT 不支持 支持(可上云)
布线成本 低(双绞线) 中(网线 + 交换机)
实时性 低(10ms 级) 中(1ms 级,无确定性)
典型场景 现场仪表组网 车间 SCADA、上云
RTU vs TCP 选型决策树 应用需求 现场设备少? 远程/上云? → Modbus RTU → 用网关转 TCP → Modbus TCP → OPC UA + TCP 是 (<32) 否 是 需安全
图 11-1:Modbus RTU 与 TCP 选型决策树

十二、Modbus 特性与能力矩阵

Modbus 能力雷达图 普及度 实现成本 易用性 开放性 实时性 节点容量
图 12-1:Modbus 能力雷达图(普及度/成本/易用性高,实时性/节点数低)

12.1 Modbus 的优势

12.2 Modbus 的局限

安全警告
Modbus 协议本身无任何安全机制!Modbus TCP 直接暴露 502 端口到公网将面临扫描、未授权访问、数据篡改等风险。生产环境务必:① 通过防火墙白名单限制访问;② 使用 VPN 接入;③ 不直接暴露到互联网;④ 关键操作前用 Modbus 网关做协议过滤。

十三、Modbus 应用场景全景

Modbus 典型应用场景 楼宇自动化 • 空调机组监控 • 照明控制 • 电表集抄 • 冷水机组 能源管理 • 电力仪表集抄 • 水气热表 • 光伏并网 • 储能 PCS 工业自动化 • 变频器控制 • PLC 互连 • 温控仪 • 软启动器 环境监控 • 机房温湿度 • 水质监测 • 气体检测 • 气象站 典型架构:分层 Modbus 网络 现场层:仪表 → Modbus RTU → RS-485 总线 → 接入网关 车间层:网关 → Modbus TCP → 以太网 → SCADA / 能源管理服务器 企业层:SCADA → OPC UA / MQTT → MES / ERP / 云平台 → Modbus 是"最后一公里"的工业数据采集标准
图 13-1:Modbus 应用场景与典型分层架构

13.1 经典案例:电力监控系统

电力监控 Modbus 网络实例 SCADA 能源管理 Modbus TCP 网关 4 路 RS-485 RS-485 电力仪表1 0x01 电力仪表2 0x02 电力仪表3 0x03 仪表4 0x04 仪表5 0x05 仪表6 0x06 终端 120Ω 3 条 RS-485 总线,每条挂 6 块仪表,网关汇总后通过以太网上传 SCADA 仪表数量多、距离远 → RTU;上层需 IT 对接 → TCP;网关完成协议转换
图 13-2:电力监控 Modbus 网络实例(RTU + TCP 混合架构)

十四、代码实战:Qt/C++ Modbus RTU + TCP

Qt 提供了官方的 QtSerialBus 模块(含 Modbus RTU/TCP 主从端封装),但需要单独编译。下面我们用更通用的方式:RTU 用开源 libmodbus 库,TCP 用 Qt 网络模块自行实现,便于跨平台部署。

14.1 基于 libmodbus 的 RTU 主站封装

libmodbus 是 C 语言编写的轻量 Modbus 库,支持 RTU/TCP,被工业界广泛使用。我们用 Qt 包装它。

// ModbusRtuMaster.h —— 基于 libmodbus 的 Qt 封装
#ifndef MODBUSRTUMASTER_H
#define MODBUSRTUMASTER_H

#include <QObject>
#include <QTimer>
#include <QVector>
#include <QDebug>
#include "modbus.h"  // libmodbus 头文件

class ModbusRtuMaster : public QObject {
    Q_OBJECT
public:
    explicit ModbusRtuMaster(QObject *parent = nullptr) : QObject(parent), m_ctx(nullptr) {}
    ~ModbusRtuMaster() { if (m_ctx) modbus_free(m_ctx); }

    // 初始化串口
    bool init(const QString& port, int baud, int dataBits = 8,
                 int parity = 'N', int stopBits = 1) {
        // 创建 RTU 上下文:"/dev/ttyUSB0" 或 "COM3"
        m_ctx = modbus_new_rtu(port.toLatin1().constData(),
                                       baud, parity, dataBits, stopBits);
        if (!m_ctx) return false;

        // 设置从站地址
        modbus_set_slave(m_ctx, 1);
        // 设置响应超时 500ms
        uint32_t tv_sec = 0, tv_usec = 500000;
        modbus_set_response_timeout(m_ctx, tv_sec, tv_usec);
        // 启用调试输出(开发期)
        modbus_set_debug(m_ctx, TRUE);

        if (modbus_connect(m_ctx) == -1) {
            modbus_free(m_ctx);
            m_ctx = nullptr;
            return false;
        }
        return true;
    }

    // 读保持寄存器(功能码 0x03)
    QVector<uint16_t> readHoldingRegisters(int slave, int addr, int num) {
        modbus_set_slave(m_ctx, slave);
        QVector<uint16_t> dest(num);
        int rc = modbus_read_registers(m_ctx, addr, num, dest.data());
        if (rc == -1) {
            qWarning() << "读寄存器失败:" <&slave << addr;
            return {};
        }
        return dest;
    }

    // 写单个寄存器(功能码 0x06)
    bool writeSingleRegister(int slave, int addr, uint16_t value) {
        modbus_set_slave(m_ctx, slave);
        return modbus_write_register(m_ctx, addr, value) != -1;
    }

    // 读线圈(功能码 0x01)
    QVector<uint8_t> readCoils(int slave, int addr, int num) {
        modbus_set_slave(m_ctx, slave);
        QVector<uint8_t> dest(num);
        int rc = modbus_read_bits(m_ctx, addr, num, dest.data());
        if (rc == -1) return {};
        return dest;
    }

    // 写多个线圈(功能码 0x0F)
    bool writeMultipleCoils(int slave, int addr,
                                   const QVector<uint8_t>& values) {
        modbus_set_slave(m_ctx, slave);
        return modbus_write_bits(m_ctx, addr, values.size(),
                                   values.constData()) != -1;
    }

private:
    modbus_t *m_ctx;
};

#endif

14.2 完整 Modbus TCP 客户端(纯 Qt 实现)

下面用 Qt 的 QTcpSocket 手工实现 Modbus TCP 客户端,体现协议帧构造原理。

// ModbusTcpClient.h —— 纯 Qt 实现的 Modbus TCP 客户端
#ifndef MODBUSTCPCLIENT_H
#define MODBUSTCPCLIENT_H

#include <QObject>
#include <QTcpSocket>
#include <QQueue>
#include <QDebug>
#include <QDataStream>

struct ModbusRequest {
    quint16 transId;      // 事务 ID(配对用)
    quint8  unitId;        // 单元 ID
    quint8  funcCode;      // 功能码
    QByteArray payload;  // PDU 数据
};

class ModbusTcpClient : public QObject {
    Q_OBJECT
public:
    explicit ModbusTcpClient(QObject *parent = nullptr)
        : QObject(parent), m_transId(0) {
        connect(&m_socket, &QTcpSocket::readyRead,
                this, &ModbusTcpClient::onReadyRead);
        connect(&m_socket, &QTcpSocket::connected,
                this, &ModbusTcpClient::onConnected);
        connect(&m_socket, &QTcpSocket::disconnected,
                this, &ModbusTcpClient::onDisconnected);
    }

    // 连接服务端
    void connectToServer(const QString& host, quint16 port = 502) {
        m_socket.connectToHost(host, port);
    }

    // 读保持寄存器(0x03)
    quint16 readHoldingRegisters(quint8 unitId, quint16 addr, quint16 count) {
        QByteArray pdu;
        QDataStream ds(&pdu, QIODevice::WriteOnly);
        ds.setByteOrder(QDataStream::BigEndian);
        ds << addr << count;  // 起始地址 + 数量(4 字节)

        return sendRequest(unitId, 0x03, pdu);
    }

    // 写单个寄存器(0x06)
    quint16 writeSingleRegister(quint8 unitId, quint16 addr, quint16 value) {
        QByteArray pdu;
        QDataStream ds(&pdu, QIODevice::WriteOnly);
        ds.setByteOrder(QDataStream::BigEndian);
        ds << addr << value;

        return sendRequest(unitId, 0x06, pdu);
    }

signals:
    void responseReceived(quint16 transId, quint8 funcCode, const QByteArray& data);
    void errorOccurred(const QString& msg);

private slots:
    void onReadyRead() {
        while (m_socket.bytesAvailable() >= 9) {  // MBAP 头 7B + 至少 2B
            QByteArray mbap = m_socket.peek(7);
            QDataStream ds(mbap);
            ds.setByteOrder(QDataStream::BigEndian);
            quint16 transId, protoId, length;
            quint8 unitId;
            ds >> transId >> protoId >> length >> unitId;

            if (m_socket.bytesAvailable() < 7 + length - 1) break;

            m_socket.read(7);  // 丢弃 MBAP 头
            QByteArray pdu = m_socket.read(length - 1);

            quint8 funcCode = (quint8)pdu[0];
            QByteArray data = pdu.mid(1);

            if (funcCode & 0x80) {
                qWarning() << "Modbus 异常:" << (int)(pdu[1] & 0xFF);
                errorOccurred("Modbus 异常响应");
            } else {
                responseReceived(transId, funcCode, data);
            }
        }
    }

    void onConnected() { qDebug() << "Modbus TCP 已连接"; }
    void onDisconnected() { qDebug() << "Modbus TCP 已断开"; }

private:
    quint16 sendRequest(quint8 unitId, quint8 funcCode, const QByteArray& payload) {
        quint16 transId = ++m_transId;
        quint16 length = 1 + 1 + payload.size();  // 单元 ID + 功能码 + PDU

        QByteArray frame;
        QDataStream ds(&frame, QIODevice::WriteOnly);
        ds.setByteOrder(QDataStream::BigEndian);
        ds << transId            // 事务 ID(2B)
           << (quint16)0      // 协议 ID(2B,恒 0)
           << length             // 长度(2B)
           << unitId              // 单元 ID(1B)
           << funcCode            // 功能码(1B)
           << payload;           // 数据区

        m_socket.write(frame);
        m_socket.flush();
        return transId;
    }

    QTcpSocket m_socket;
    quint16 m_transId;
};

#endif

14.3 使用示例:电力仪表数据采集

// 电力采集主流程
#include "ModbusRtuMaster.h"
#include "ModbusTcpClient.h"
#include <QCoreApplication>
#include <QTimer>

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    // === RTU 示例:读 1 号仪表的电压、电流、功率 ===
    ModbusRtuMaster rtu;
    if (!rtu.init("COM3", 9600, 8, 'N', 1)) {
        qCritical() << "串口打开失败";
        return -1;
    }

    // 假设仪表寄存器表:
    // 0x4000 = 电压 (0.1V),0x4001 = 电流 (0.01A),0x4002 = 功率 (0.1W)
    auto regs = rtu.readHoldingRegisters(1, 0x4000, 3);
    if (regs.size() == 3) {
        qInfo() << "电压:" << regs[0] / 10.0 << "V";
        qInfo() << "电流:" << regs[1] / 100.0 << "A";
        qInfo() << "功率:" << regs[2] / 10.0 << "W";
    }

    // === TCP 示例:远程控制变频器启停 ===
    ModbusTcpClient tcp;
    QObject::connect(&tcp, &ModbusTcpClient::responseReceived,
        [](quint16 transId, quint8 funcCode, const QByteArray& data){
            qDebug() << "收到响应:" << transId << funcCode << data.toHex();
        });

    tcp.connectToServer("192.168.1.10", 502);

    // 延迟 1s 后发送命令
    QTimer::singleShot(1000, [&](){
        // 写寄存器 0x0001 = 0x0001(启动变频器)
        tcp.writeSingleRegister(0xFF, 0x0001, 0x0001);
    });

    return app.exec();
}
实战经验
• 采集多块仪表时建议按波特率计算总轮询时间,确保 1 秒内能完成所有从站
• 9600bps 下读 1 块仪表 3 个寄存器约需 30ms,10 块仪表串行轮询需 300ms
• 实际工程中常做分组并行:每个 RS-485 总线一个线程,同时采集
• Modbus TCP 利用异步特性可同时发多个请求,配对事务 ID 提升吞吐

十五、调试技巧与常见问题

15.1 常用调试工具

工具 用途 平台
Modbus Poll RTU/TCP 主站模拟器,可视化寄存器 Windows
Modbus Slave 从站模拟器,配合 Poll 测试 Windows
Wireshark TCP Modbus 协议解析 跨平台
comto / 串口助手 RTU 原始字节流抓取 Windows
mbpoll (CLI) 命令行 Modbus 主站 Linux/Win
diagslave 开源从站模拟器 跨平台

15.2 八大常见故障

故障现象 可能原因 排查方法
从站不响应 波特率/校验位不匹配;从站地址错误;接线反 用串口助手对比波特率;确认 485 A/B 极性
CRC 校验错 帧内字符间隔过大;线材质量差引入干扰 示波器检查总线波形;缩短总线长度
偶发通信失败 终端电阻缺失;节点过多;波特率过高 两端加 120Ω 电阻;节点超过 32 用中继器
响应数据错乱 多主站同时访问;时序混乱 同一 RS-485 仅一个主站;加锁防并发
TCP 连接被拒 服务端 502 端口未开放;防火墙拦截 telnet 测试端口;检查防火墙规则
32 位数据错 字节序不匹配(ABCD vs CDAB) 读两寄存器原始字节,按 4 种序试解
异常码 0x02 寄存器地址越界(含 0 基索引偏移) 查仪表文档,确认协议地址 = 显示地址 - 1
异常码 0x06 从站忙(如正在写参数) 主站重试机制,间隔 100~500ms
调试忠告
① 永远先确认波特率、校验位、停止位三者匹配,80% 的 RTU 故障源于此
② 检查终端电阻:RS-485 总线两端必须各加 120Ω,否则信号反射
③ 确认从站地址:仪表面板显示地址 ≠ 协议帧地址(前者多为 1~247,后者相同但需正确编码)
④ 抓不到 RTU 帧?检查串口是否支持低延迟模式,关闭 VTIME/VMIN 缓冲

十六、总结

Modbus 是工业通信领域的"普通话"——简单、开放、普及。它不是最快的,不是最强的,但因为它足够简单、足够开放,成为了所有工业设备都支持的"最低公约数"。

Modbus 知识图谱回顾 数据模型层 • 线圈 Coil • 离散输入 • 输入寄存器 • 保持寄存器 字节序: ABCD / CDAB BADC / DCBA 关键词: 4 类数据 + 5 位编号 协议层 • 功能码 0x01-0x06 • 异常响应 0x8x • CRC-16-Modbus RTU: 3.5 字符帧定界 TCP: MBAP 头 关键词: 主从 + 轮询 能力层 • RTU 9600~115200 • TCP 100M+ • RTU 32 节点 • TCP 无限 优势: 简单、开放、普及 局限: 无同步、无安全 关键词: 事实标准 应用层 • 楼宇自动化 • 能源管理 • 变频器/PLC • 环境监控 工具: libmodbus QtSerialBus Modbus Poll/Slave 关键词: 最后一公里
图 16-1:Modbus 知识图谱(四层架构回顾)

核心要点速记

  1. 主从架构是 Modbus 的灵魂——主站主动询问,从站被动响应,简单可靠
  2. 四类数据:线圈/离散输入/输入寄存器/保持寄存器,覆盖开关量输入输出和模拟量输入输出
  3. 功能码 0x01~0x06 是日常主力,覆盖 90% 应用场景
  4. RTU 用二进制 + CRC16 + 3.5 字符定界,TCP 用 MBAP 头 + TCP 自带校验
  5. 32 位数据 字节序是常见陷阱,不同厂家不同序(ABCD / CDAB / BADC / DCBA)
  6. RTU 选型:现场设备少、距离远、对延迟要求不高
  7. TCP 选型:需要 IT 对接、多主访问、上云或跨网段
  8. 调试三要素:① 波特率/校验位 ② 终端电阻 ③ 从站地址
  9. 安全:Modbus 无内建安全机制,Modbus TCP 切勿直接暴露公网
  10. 定位:Modbus 是"工业通信的普通话"——不是最快,但所有人都会说

嵌入式工业通信系列至此涵盖 RS-232/485 → CAN → 串口全景 → EtherCAT → Modbus 五大主题。Modbus 与 EtherCAT 互补:Modbus 负责"最后一公里"采集与 IT 对接,EtherCAT 负责实时运动控制。两者结合,覆盖了工业通信 95% 以上的需求。

ModbusModbus RTUModbus TCP主从架构功能码线圈离散输入输入寄存器保持寄存器CRC16MBAPRS-485以太网libmodbusQtSerialBusQt/C++