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 接口,因此业务层代码可以高度复用,只需替换具体客户端类。

Qt Modbus 主站 / 从站通信结构 Qt 主站 (Client) QModbusRtuSerialClient 串口 RTU / CRC16 QModbusTcpClient 以太网 TCP / MBAP QModbusDataUnit Holding / Input / Coil sendReadRequest / sendWriteRequest 异常处理 超时 / 非法地址 / CRC errorOccurred 信号 Qt 从站 (Server) QModbusRtuSerialServer 从站地址 1~247 QModbusTcpServer 监听 502 端口 QModbusDataUnitMap setData / data 读写 寄存器映射表 异常响应 异常码 01/02/03/04 非法功能 / 非法数据 Modbus RTU 请求 / 响应 从站地址 + 功能码 + 数据 + CRC16 串口参数:9600 8N1,帧间隔 3.5T Modbus TCP 请求 / 响应 MBAP 头 + PDU 事务标识 + 协议标识 + 长度 + 单元标识 统一 API:QModbusClient / QModbusServer 业务层可复用,仅替换链路实现

方法流程:从连接配置到寄存器读写

实现一个 Modbus 主站,通常按以下步骤推进:

  1. 创建客户端对象,RTU 使用 QModbusRtuSerialClient,TCP 使用 QModbusTcpClient。
  2. 配置连接参数:RTU 设置串口号、波特率、数据位、校验位、停止位;TCP 设置主机地址和端口。
  3. 调用 connectDevice() 建立连接,并监听 stateChanged 确认状态。
  4. 构造 QModbusDataUnit,指定寄存器类型、起始地址和读取数量。
  5. 调用 sendReadRequest() 或 sendWriteRequest(),返回 QModbusReply。
  6. 连接 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,业务层都只需关注寄存器语义,而不必关心底层链路差异。