4. Modbus通信实战
在工业设备应用开发中,Modbus 是最常见、最容易落地的现场总线协议之一。它简单、开放、易于调试,既能跑在串口上形成 Modbus RTU,也能跑在以太网上形成 Modbus TCP。Qt 通过 QtSerialBus 模块提供了 QModbusClient、QModbusServer、QModbusRtuSerialClient、QModbusTcpClient 等类,让我们可以用统一的 API 完成主站请求、从站响应、寄存器读写和异常处理。本章围绕一个典型场景展开:上位机作为主站,通过 Modbus RTU 读取温控器保持寄存器,同时通过 Modbus TCP 与 PLC 交换数据,并处理超时、非法地址、CRC 错误等异常。
核心原理:主从模型与寄存器空间
Modbus 采用主从问答模型。主站发起请求,从站按地址返回响应;从站不会主动上报,因此轮询周期和超时设置直接决定通信实时性。协议中最重要的数据对象是四类寄存器:线圈(Coil,可读写布尔量)、离散输入(Discrete Input,只读布尔量)、保持寄存器(Holding Register,可读写 16 位字)、输入寄存器(Input Register,只读 16 位字)。在 Qt 中,这些对象通过 QModbusDataUnit 的寄存器类型枚举来区分,例如 QModbusDataUnit::HoldingRegisters。
Modbus RTU 与 Modbus TCP 的差异主要在链路层:RTU 使用串口,帧间需要 3.5 个字符时间间隔,并带 CRC16 校验;TCP 使用 502 端口,报文前增加 MBAP 头,不再需要 CRC。Qt 将两者抽象为同一套 QModbusClient 接口,因此业务层代码可以高度复用,只需替换具体客户端类。
方法流程:从连接配置到寄存器读写
实现一个 Modbus 主站,通常按以下步骤推进:
- 创建客户端对象,RTU 使用
QModbusRtuSerialClient,TCP 使用QModbusTcpClient。 - 配置连接参数:RTU 设置串口号、波特率、数据位、校验位、停止位;TCP 设置主机地址和端口。
- 调用
connectDevice()建立连接,并监听stateChanged确认状态。 - 构造
QModbusDataUnit,指定寄存器类型、起始地址和读取数量。 - 调用
sendReadRequest()或sendWriteRequest(),返回QModbusReply。 - 连接
QModbusReply::finished信号,在槽函数中判断reply->error(),再读取result()。
下面是一段读取保持寄存器的核心代码,展示了请求构造与响应处理:
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, 0, 10);
if (auto *reply = client->sendReadRequest(unit, 1)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, this, [this, reply]() {
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit result = reply->result();
for (int i = 0; i < result.valueCount(); ++i) {
qDebug() << "寄存器" << i << "=" << result.value(i);
}
} else {
qWarning() << "读取失败:" << reply->errorString();
}
reply->deleteLater();
});
} else {
delete reply;
}
}
从站实现则围绕 QModbusServer 展开:先通过 setMap() 建立寄存器映射,再调用 setValue() 或 data() 维护数据区。当主站写入时,从站会触发 dataWritten 信号,可在其中更新设备状态或做范围校验。
案例场景:温控器轮询与 PLC 数据交换
假设一条产线上有两类设备:温控器通过 RS485 接入,使用 Modbus RTU,从站地址为 1,保持寄存器 0x0000 保存当前温度,0x0001 保存设定温度;PLC 通过以太网接入,使用 Modbus TCP,IP 为 192.168.1.10,端口 502,保持寄存器 0x0100 保存产线速度。上位机需要每 500ms 轮询一次温度,每 1s 读取一次速度,并在温度超过设定值时写入报警寄存器。
实现时,建议为每个设备维护独立的客户端对象和轮询定时器,避免一个设备超时阻塞另一个设备。对于写操作,要特别注意“写单个寄存器”和“写多个寄存器”的区别:QModbusDataUnit 中 valueCount 为 1 时通常走功能码 06,大于 1 时走功能码 16。若从站不支持多写,需要拆分为多次单写。
| 设备 | 链路 | 从站地址 | 寄存器 | 功能 | 轮询周期 |
|---|---|---|---|---|---|
| 温控器 | Modbus RTU | 1 | 0x0000 | 当前温度 | 500ms |
| 温控器 | Modbus RTU | 1 | 0x0001 | 设定温度 | 500ms |
| PLC | Modbus TCP | 1 | 0x0100 | 产线速度 | 1000ms |
| PLC | Modbus TCP | 1 | 0x0110 | 报警写入 | 事件触发 |
异常处理与常见误区
Modbus 的异常分为链路异常和协议异常。链路异常包括串口打开失败、TCP 连接断开、响应超时;协议异常则是从站返回异常码,例如 01 非法功能、02 非法数据地址、03 非法数据值、04 从站设备故障。Qt 中应同时监听 QModbusClient::errorOccurred 和 QModbusReply::error(),前者反映设备级错误,后者反映单次请求错误。
常见误区一:把超时设置得过短。RTU 在 9600 波特率下,一帧 8 字节约需 8ms,加上从站处理时间,建议超时不低于 300ms。
常见误区二:忽略寄存器地址偏移。PLC 手册常写 40001,而 Modbus 协议地址从 0 开始,实际应写 0x0000,需确认“协议地址”与“文档地址”的换算关系。
常见误区三:在 UI 线程中同步等待响应。应使用信号槽异步处理,避免界面卡死。
调试建议:先用 Modbus Poll、Modbus Slave 或 mbpoll 验证从站行为,再接入 Qt 代码;串口抓包可用 cutecom 或逻辑分析仪,TCP 可用 Wireshark 过滤 modbus 端口 502。确认链路无误后,再排查寄存器映射和数据类型(16 位有符号、无符号、浮点高低字序)。
复盘来看,Qt 实现 Modbus 的关键不在协议本身,而在于连接管理、轮询调度和异常恢复。建议将客户端封装为独立类,对外暴露“读寄存器”“写寄存器”“连接状态”三个接口,内部统一处理重连、超时和错误码转换。这样,无论是 RTU 还是 TCP,业务层都只需关注寄存器语义,而不必关心底层链路差异。