4. 串口通信在工业设备中的实战
在工业上位机中,串口通信是最常见也最容易“看起来简单、做起来踩坑”的一类通信方式。称重仪表、PLC 调试口、变频器、条码枪、温控器大多提供 RS-232 或 RS-485 接口,上位机通过 USB 转串口或板载串口与之相连。Qt 提供的 QSerialPort 模块封装了跨平台的串口操作,但真正决定采集是否稳定的,不是“能不能打开端口”,而是端口枚举是否可靠、参数是否与设备一致、粘包是否被正确切分、超时是否被合理处理、异常后能否自动恢复。
一、端口枚举:先确认“连的是哪个口”
工业现场常同时插入多个 USB 转串口设备,COM 号会随插拔顺序变化。因此不能把 COM3 写死在代码里,而应在打开前枚举可用端口,并结合设备描述信息辅助判断。QSerialPort 通过 QSerialPortInfo::availablePorts() 返回端口列表,每个 QSerialPortInfo 可读取 portName()、description()、manufacturer()、systemLocation() 以及 vendorIdentifier()/productIdentifier()。对于称重仪表,常见芯片为 CH340、CP210x、FTDI,描述中往往能看出线索。
for (const QSerialPortInfo &info : QSerialPortInfo::availablePorts()) {
qDebug() << info.portName()
<< info.description()
<< info.manufacturer()
<< QString::number(info.vendorIdentifier(), 16);
}
更稳妥的做法是把“端口名 + VID/PID + 描述”一起展示给操作员选择,或按 VID/PID 自动匹配。需要注意:枚举只说明系统当前识别到的端口,并不代表设备已上电或线缆正确。因此枚举之后仍要执行打开与参数配置,并通过一次实际读取来验证链路。
二、参数配置:波特率、数据位、校验、停止位、流控
串口参数必须与仪表或 PLC 调试口完全一致,否则表现为乱码、无数据或偶发错误。典型称重仪表常用 9600 8N1,部分 PLC 调试口使用 19200 8E1 或 115200 8N1。配置顺序建议为:设置波特率、数据位、校验位、停止位、流控,再调用 open()。若先打开再改参数,部分平台会失败或需要重新打开。
QSerialPort port;
port.setPortName("COM5");
port.setBaudRate(QSerialPort::Baud9600);
port.setDataBits(QSerialPort::Data8);
port.setParity(QSerialPort::NoParity);
port.setStopBits(QSerialPort::OneStop);
port.setFlowControl(QSerialPort::NoFlowControl);
if (!port.open(QIODevice::ReadWrite)) {
qWarning() << "open failed:" << port.errorString();
}
RS-485 半双工场景还要注意收发方向控制。若使用带自动流向控制的转换器,上位机按普通串口处理即可;若需要手动控制 RTS,则必须在发送完成后及时切回接收,否则会丢失设备回复。
三、粘包处理:串口是字节流,不是消息流
这是工业串口采集最核心的问题。串口本身没有“包”的概念,设备连续发送或上位机连续读取时,一次 readyRead() 可能拿到半帧、一帧或多帧。称重仪表常见协议为 ASCII 连续输出,例如每帧以 STX(0x02)开始、以 CR(0x0D)结束,中间是重量值;PLC 调试口可能是 Modbus RTU,帧间隔靠 3.5 字符时间区分。两种协议处理方式不同。
对 ASCII 定界协议,推荐维护一个接收缓冲区,每次把新数据追加进去,再循环查找完整帧:找到帧头与帧尾后截取、解析、移除;若缓冲区超过上限仍无完整帧,则丢弃最旧数据,防止内存无限增长。对 Modbus RTU,则更适合用“帧间隔定时器”判断一帧结束:每次收到数据就重启一个约 5ms 的定时器,定时器超时后把缓冲区当作一帧交给解析器。
void onReadyRead() {
m_buffer.append(m_port.readAll());
while (true) {
int start = m_buffer.indexOf(char(0x02));
int end = m_buffer.indexOf(char(0x0D), start + 1);
if (start < 0 || end < 0) break;
QByteArray frame = m_buffer.mid(start, end - start + 1);
m_buffer.remove(0, end + 1);
parseFrame(frame); // 解析重量、单位、状态
}
if (m_buffer.size() > 4096) m_buffer = m_buffer.right(512);
}
四、超时读取与异常恢复
工业设备可能因断电、线缆松动、仪表死机而不回复。若上位机只依赖 readyRead(),界面会一直停在旧值,操作员无法判断数据是否新鲜。因此需要引入超时机制:每次成功解析一帧就刷新“最后有效时间”,并用一个 1s 定时器检查;若超过设定阈值(例如 3s)没有新帧,就把该通道标记为“超时/离线”,界面显示灰色或告警,而不是继续显示旧重量。
异常恢复应覆盖三类情况:一是 QSerialPort::ResourceError(设备被拔出),此时应关闭端口并启动重连定时器;二是 PermissionError(端口被占用),提示操作员关闭占用程序;三是持续无数据,可尝试重新打开端口或复位设备。重连不宜过于频繁,建议 2s 一次,并限制连续失败次数后提示人工检查。
waitForReadyRead() 长时间阻塞等待。工业上位机界面需要保持响应,应使用信号槽异步接收,把解析结果通过信号发送给界面线程。五、称重仪表采集闭环示例
假设一台称重仪表通过 CH340 转串口接入,参数 9600 8N1,连续输出格式为 STX + 重量(6位) + 单位 + 状态 + CR,例如 0x02 30 30 31 32 2E 35 6B 67 53 0x0D 表示 12.5kg、稳定。上位机流程为:枚举端口并匹配 CH340 → 配置 9600 8N1 → 打开 → 绑定 readyRead → 缓冲区按 STX/CR 切帧 → 解析重量与稳定位 → 刷新界面与曲线 → 3s 无帧则标记离线 → 拔出后自动重连。这样才形成从物理链路到界面显示的完整闭环。
把上述环节串起来,串口采集就不再是“打开端口读数据”这么简单,而是一条包含枚举、配置、切帧、解析、超时与恢复的完整链路。实际项目中,建议把串口通信封装为独立类,对外只暴露“连接、断开、数据到达、状态变化”四类信号,界面层不直接操作 QSerialPort,这样既便于替换为 Modbus 或 TCP,也便于在异常时统一处理。