qt5-serialbus是 Qt5 用于访问各类工业现场总线和协议的附加模块,自 Qt 5.8 起正式引入,内部包含Modbus(TCP/RTU)与CAN Bus两大子系统,覆盖了 PLC 通信、仪表采集、汽车电子总线等工业场景。它不在qt5-base中,需通过 vcpkg 单独安装。
模块整体定位与两大子系统
| 维度 | Qt Modbus 子系统 | Qt CAN Bus 子系统 |
|---|---|---|
| 协议 | Modbus TCP / Modbus RTU | CAN 2.0A/B、CAN FD |
| 角色 | 主站(Client)/ 从站(Server) | 总线节点(收发帧) |
| 传输介质 | 以太网(502端口)/ RS-232/485 串口 | 各类 CAN 适配器(USB-PCIe) |
| 核心基类 | QModbusDevice | QCanBusDevice |
| 工厂类 | 直接 new 具体子类 | QCanBus::instance()->createDevice() |
| 数据单元 | QModbusDataUnit(4 类寄存器) | QCanBusFrame(ID + payload) |
| 异步句柄 | QModbusReply | framesReceived信号 +readFrame() |
| 典型场景 | 上位机与 PLC、仪表通信 | 车辆 ECU、工业控制器总线 |
类层级结构
下面用一张类图展示整个模块的继承关系,这是理解 API 设计的关键:
Modbus 侧:QModbusDevice是抽象基类,提供连接状态(state())、错误处理(error()/errorString())、连接参数(setConnectionParameter())等通用能力;QModbusClient与QModbusServer分别派生为主站和从站抽象,再按 TCP/RTU 细化为 4 个具体类。
CAN 侧:QCanBus是单例工厂,通过插件机制加载不同厂商后端;QCanBusDevice封装具体设备的连接、配置、收发;QCanBusFrame表示一个 CAN 帧。
四种 Modbus 寄存器类型
Modbus 协议规定每个从站设备维护 4 张数据表,QModbusDataUnit::RegisterType枚举与之对应:
| 寄存器类型 | 枚举值 | 数据宽度 | 读写权限 | 功能码 | 典型用途 |
|---|---|---|---|---|---|
| 离散输入 Discrete Inputs | 1 | 1 bit | 只读 | 02 | 按钮、开关状态 |
| 线圈 Coils | 0 | 1 bit | 读写 | 01/05/15 | 继电器输出、指示灯 |
| 输入寄存器 Input Registers | 3 | 16 bit | 只读 | 04 | ADC 采样值、传感器读数 |
| 保持寄存器 Holding Registers | 2 | 16 bit | 读写 | 03/06/16 | 设定参数、计数器 |
| 读写权限决定了主站能用哪些功能码访问:只读表只能用读功能码,读写表既能读也能写。 |
主站核心 API(QModbusClient)
QModbusClient提供 4 个异步请求方法,返回QModbusReply*句柄:
| 方法 | 功能 | 对应功能码 |
|---|---|---|
sendReadRequest(unit, serverAddress) | 读寄存器 | 01/02/03/04 |
sendWriteRequest(unit, serverAddress) | 写寄存器 | 05/06/15/16 |
sendReadWriteRequest(read, write, addr) | 原子读写 | 23 |
sendRawRequest(request, addr) | 发送原始 PDU | 任意 |
配置方法:setNumberOfRetries(n)(默认 3 次重试)、setTimeout(ms)(默认 1000ms)。 |
从站核心 API(QModbusServer)
QModbusServer维护内存中的寄存器映射表,核心方法:
setMap(QModbusDataUnitMap)—— 初始化 4 张表的大小与起始地址setData(table, address, value)—— 写入单个寄存器(程序内部更新)data(table, address, &data)—— 读取单个寄存器setServerAddress(int)—— 设置从站地址(1~247)- 信号
dataWritten(table, address, size)—— 主站写入时触发,用于业务联动
从站 setMap 初始化寄存器映射示例
从站必须先调用setMap()划分 4 张表的地址空间,主站才能读写:
// 创建 TCP 从站QModbusTcpServer*server=newQModbusTcpServer(this);server->setConnectionParameter(QModbusDevice::NetworkAddressParameter,"0.0.0.0");server->setConnectionParameter(QModbusDevice::NetworkPortParameter,502);server->setServerAddress(1);// 初始化 4 张数据表:各 100 个条目,起始地址 0QModbusDataUnitMap reg;reg.insert(QModbusDataUnit::DiscreteInputs,{QModbusDataUnit::DiscreteInputs,0,100});reg.insert(QModbusDataUnit::Coils,{QModbusDataUnit::Coils,0,100});reg.insert(QModbusDataUnit::InputRegisters,{QModbusDataUnit::InputRegisters,0,100});reg.insert(QModbusDataUnit::HoldingRegisters,{QModbusDataUnit::HoldingRegisters,0,100});server->setMap(reg);// 设置初始值server->setData(QModbusDataUnit::HoldingRegisters,0,1234);// 监听主站写入connect(server,&QModbusServer::dataWritten,[](QModbusDataUnit::RegisterType table,intaddress,intsize){qDebug()<<"主站写入:"<<table<<"addr="<<address<<"size="<<size;});server->connectDevice();RTU 主站完整示例(读保持寄存器)
这是工业上位机最典型的用法:配置串口 → 连接 → 异步读 → 在finished信号中取结果。
#include<QModbusRtuSerialMaster>#include<QModbusDataUnit>#include<QModbusReply>#include<QSerialPort>#include<QDebug>// 1. 创建 RTU 主站QModbusRtuSerialMaster*client=newQModbusRtuSerialMaster(this);// 2. 配置串口参数client->setConnectionParameter(QModbusDevice::SerialPortNameParameter,"COM3");client->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,QSerialPort::Baud9600);client->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,QSerialPort::Data8);client->setConnectionParameter(QModbusDevice::SerialParityParameter,QSerialPort::NoParity);client->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,QSerialPort::OneStop);client->setTimeout(1000);client->setNumberOfRetries(3);// 3. 连接if(!client->connectDevice()){qDebug()<<"连接失败:"<<client->errorString();return;}// 4. 读保持寄存器:从站地址=1,起始地址=0,数量=10QModbusDataUnitreadUnit(QModbusDataUnit::HoldingRegisters,0,10);if(auto*reply=client->sendReadRequest(readUnit,1)){if(!reply->isFinished()){connect(reply,&QModbusReply::finished,[reply](){if(reply->error()==QModbusDevice::NoError){constQModbusDataUnit unit=reply->result();for(inti=0;i<unit.valueCount();++i)qDebug()<<"Reg["<<unit.startAddress()+i<<"] ="<<unit.values().at(i);}else{qDebug()<<"读错误:"<<reply->errorString();}reply->deleteLater();});}else{deletereply;// 同步完成(广播请求)}}TCP 主站只需替换两行
把 RTU 示例中的创建与配置换成 TCP 版本,其余sendReadRequest/finished逻辑完全一致:
QModbusTcpClient*client=newQModbusTcpClient(this);client->setConnectionParameter(QModbusDevice::NetworkAddressParameter,"192.168.1.100");client->setConnectionParameter(QModbusDevice::NetworkPortParameter,502);client->connectDevice();写寄存器示例
// 写单个保持寄存器:地址 5 写入 0x1234QModbusDataUnitwriteUnit(QModbusDataUnit::HoldingRegisters,5,QVector<quint16>{0x1234});if(auto*reply=client->sendWriteRequest(writeUnit,1)){if(!reply->isFinished()){connect(reply,&QModbusReply::finished,[reply](){if(reply->error()==QModbusDevice::NoError)qDebug()<<"写入成功";elseqDebug()<<"写入失败:"<<reply->errorString();reply->deleteLater();});}else{deletereply;}}CAN Bus 子系统快速上手
CAN 侧采用插件机制,通过QCanBus工厂创建设备:
#include<QCanBus>#include<QCanBusFrame>#include<QDebug>// 1. 枚举可用插件qDebug()<<"可用插件:"<<QCanBus::instance()->plugins();// 2. 创建设备(Linux 用 socketcan,Windows 用 peakcan)QString err;QCanBusDevice*device=QCanBus::instance()->createDevice("socketcan","can0",&err);if(!device){qWarning()<<"创建失败:"<<err;return;}// 3. 配置波特率device->setConfigurationParameter(QCanBusDevice::BitRateKey,250000);// 4. 连接接收信号connect(device,&QCanBusDevice::framesReceived,[device](){while(device->framesAvailable()){QCanBusFrame frame=device->readFrame();qDebug()<<"ID:"<<QString::number(frame.frameId(),16)<<"Data:"<<frame.payload().toHex(' ');}});// 5. 连接并发送device->connectDevice();QCanBusFrame txFrame;txFrame.setFrameId(0x123);txFrame.setPayload(QByteArray::fromHex("0102030405"));device->writeFrame(txFrame);支持的 CAN 插件列表
Qt SerialBus 通过插件支持主流 CAN 硬件厂商:
| 插件 key | 厂商 | 平台 |
|---|---|---|
socketcan | Linux 内核 SocketCAN | Linux |
passthrucan | SAE J2534 Pass-Thru | Windows |
peakcan | PEAK-System (PCAN) | 全平台 |
vectorcan | Vector Informatik | Windows |
systeccan | SYS TEC electronic | 全平台 |
tinycan | MHS Elektronik | 全平台 |
virtualcan | 虚拟 CAN(TCP/IP) | 全平台(测试用) |
完整工程封装建议
一个健壮的 Modbus 通信层应分层封装:
- 配置层:把所有连接参数封装为结构体,支持从配置文件或 UI 读取
- 通信层:封装
QModbusClient生命周期,对外暴露readRegisters()/writeRegisters()异步接口 - 协议层:处理重试、超时、请求队列、字节序转换
- 应用层:响应
finished或dataWritten信号,更新 UI 或执行业务
常见问题与排查
① RTU 通信超时(TimeoutError)
默认超时 1000ms,部分仪表响应慢需调大;RTU 还有setTurnaroundDelay()(周转延迟)参数,控制主站发完请求到从站开始响应的最短间隔,设备响应慢时需调大。
② 串口参数不匹配导致乱码
波特率、校验位、数据位、停止位必须与从站完全一致。工业仪表常见 9600/8/N/1 或 19200/8/E/1,需查设备手册。参数不一致时主站会收到ResourceError或乱码。
③ TCP 连接被拒
检查目标 IP 是否可达、502 端口是否开放、防火墙是否放行;部分 PLC 需在工程中显式启用 Modbus TCP Server 功能。
④ 32 位浮点数字节序问题
Modbus 寄存器是 16 位,存储 32 位 float/long 时存在大端/小端和字交换 4 种组合。Qt 不自动转换,需用QByteArray+QDataStream手动处理,或按设备手册的字节序拼接两个quint16。
⑤ QModbusReply 内存泄漏sendReadRequest返回的reply必须在finished槽中调用deleteLater(),否则内存泄漏;若是广播请求(serverAddress=0),isFinished()立即为 true,需手动delete。
⑥ RTU 帧间隔不足
Modbus RTU 协议要求帧间至少 3.5 个字符时间的空闲。高波特率下若主站连续发送过快,从站可能无法识别帧边界。可通过setInterFrameDelay()调整。
⑦ 开启 Modbus 调试日志
在main()中添加以下代码,可看到完整的请求/响应报文,排查问题极为方便:
QLoggingCategory::setFilterRules(QStringLiteral("qt.modbus* = true"));若要查看更底层的字节内容,使用qt.modbus.lowlevel = true。
选型建议
- 与 PLC/仪表通信:选 Modbus 子系统,以太网环境用
QModbusTcpClient,串口环境用QModbusRtuSerialMaster - 模拟从站供主站调试:用
QModbusTcpServer或QModbusRtuSerialSlave,配合setMap()建数据表 - 汽车电子/ECU 通信:选 CAN 子系统,Linux 用
socketcan,Windows 用peakcan或vectorcan - 无硬件开发调试:CAN 用
virtualcan插件;Modbus 用虚拟串口 + Modbus Poll/Slave 工具 - 非 Qt 项目或需更轻量:可考虑
libmodbus(纯 C 库)作为替代方案