Qt 串口通讯「万金油」架构方案
串口通讯看起来简单------打开、读写、关闭。但到了实际项目里:数据粘包、线程安全、UI 卡死、端口热插拔、不同协议的兼容......哪个都够你调一整天。
这篇文章不讲 QSerialPort 的 API 怎么用(文档上都有),讲的是怎么用不出事。
一、最大的坑:QSerialPort 和线程的关系
第一条铁律 :QSerialPort 必须在拥有事件循环的线程中创建和使用。
为什么?因为它依赖 Qt 的事件驱动机制------readyRead 信号必须在事件循环中才能触发。如果你在纯工作线程里 new 一个 QSerialPort 而不开事件循环,readyRead 永远不触发。
cpp
// ❌ 错误姿势
QThread t;
SerialWorker* w = new SerialWorker();
w->moveToThread(&t);
// 如果 SerialWorker 的构造函数里 new 了 QSerialPort,已经在主线程中绑定了
// 跨线程调用 QSerialPort 的方法 = 未定义行为
// ✅ 正确姿势
auto* worker = new SerialWorker(); // 还未 moveToThread
worker->moveToThread(&workerThread);
// QSerialPort 的创建必须在 moveToThread 之后、在 worker 的槽函数中完成
推荐方案 :一个 SerialPortManager 类,移动到独立线程,内部通过信号槽与主线程通信。
二、万金油框架
下面这个架构,经过三轮迭代验证,覆盖了同步/异步、阻塞/非阻塞各种场景。核心思路:用事件驱动 + 环形缓冲区处理数据流,用信号槽解耦层间依赖。
2.1 整体架构
┌─────────────┐ 信号槽 ┌──────────────────┐ 信号槽 ┌──────────────┐
│ UI/业务层 │ ←─────────→ │ SerialManager │ ←─────────→ │ QSerialPort │
│ (主线程) │ │ (工作线程) │ │ (工作线程) │
└─────────────┘ └──────────────────┘ └──────────────┘
│
│ 内部
▼
┌──────────────┐
│ Protocol │
│ Parser │ ← 插拔式协议解析器
└──────────────┘
2.2 核心类设计
cpp
// serial_port_manager.h
#pragma once
#include <QObject>
#include <QSerialPort>
#include <QByteArray>
#include <QTimer>
#include <QMutex>
#include <functional>
class AbstractProtocolParser : public QObject {
Q_OBJECT
public:
virtual ~AbstractProtocolParser() = default;
/// 接收原始数据,返回解析完成的完整消息帧
/// @return list of complete frames parsed; remaining data stays in buffer
virtual QList<QByteArray> feed(const QByteArray& data) = 0;
/// 重置解析器状态(协议切换/错误恢复时调用)
virtual void reset() = 0;
signals:
void frameParsed(const QByteArray& frame);
void parseError(const QString& message);
};
/// 最简单的帧定界协议:固定末尾标记(如 \r\n 或 \n)
class SimpleDelimiterParser : public AbstractProtocolParser {
public:
explicit SimpleDelimiterParser(const QByteArray& delimiter = "\n",
int maxFrameSize = 4096)
: m_delimiter(delimiter), m_maxFrameSize(maxFrameSize) {}
QList<QByteArray> feed(const QByteArray& data) override {
m_buffer.append(data);
QList<QByteArray> frames;
int idx;
while ((idx = m_buffer.indexOf(m_delimiter)) != -1) {
int endPos = idx + m_delimiter.size();
if (idx > m_maxFrameSize) {
// 保护:超出最大帧长,丢弃
emit parseError("Max frame size exceeded, discarding");
m_buffer.remove(0, endPos);
continue;
}
QByteArray frame = m_buffer.left(endPos);
m_buffer.remove(0, endPos);
frames.append(frame);
}
// 防内存暴涨:缓冲区内超过 2 倍最大帧长仍无定界符 → 丢
if (m_buffer.size() > m_maxFrameSize * 2) {
emit parseError("Buffer overflow, discarding");
m_buffer.clear();
}
return frames;
}
void reset() override { m_buffer.clear(); }
private:
QByteArray m_buffer;
QByteArray m_delimiter;
int m_maxFrameSize;
};
/// 长度前缀协议:前 4 字节 = payload 长度
class LengthPrefixParser : public AbstractProtocolParser {
public:
explicit LengthPrefixParser(int headerSize = 4, int maxPayload = 65536)
: m_headerSize(headerSize), m_maxPayload(maxPayload) {}
QList<QByteArray> feed(const QByteArray& data) override {
m_buffer.append(data);
QList<QByteArray> frames;
while (true) {
if (m_buffer.size() < m_headerSize) break;
uint32_t payloadLen = 0;
// 按小端序解析长度(根据实际协议调整 endianness)
for (int i = 0; i < m_headerSize; ++i)
payloadLen = (payloadLen << 8) | (uint8_t)m_buffer[i];
if (payloadLen > (uint32_t)m_maxPayload) {
emit parseError("Payload too large, resetting");
reset();
break;
}
int totalLen = m_headerSize + payloadLen;
if (m_buffer.size() < totalLen) break; // 还没收完
QByteArray frame = m_buffer.left(totalLen);
m_buffer.remove(0, totalLen);
frames.append(frame);
}
return frames;
}
void reset() override { m_buffer.clear(); }
private:
QByteArray m_buffer;
int m_headerSize;
int m_maxPayload;
};
2.3 串口管理器
cpp
// serial_port_manager.h (续)
class SerialPortManager : public QObject {
Q_OBJECT
public:
explicit SerialPortManager(QObject* parent = nullptr);
~SerialPortManager() override;
/// 配置参数(线程安全,建议在启动前一次性设置完)
void configure(const QString& portName,
QSerialPort::BaudRate baudRate = QSerialPort::Baud115200,
QSerialPort::DataBits dataBits = QSerialPort::Data8,
QSerialPort::Parity parity = QSerialPort::NoParity,
QSerialPort::StopBits stopBits = QSerialPort::OneStop,
QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl);
/// 设置协议解析器(接管 lifecycle)
void setProtocolParser(AbstractProtocolParser* parser);
/// 打开串口,返回是否成功
bool open();
/// 关闭串口
void close();
/// 发送数据(线程安全,通过信号槽转发到工作线程)
void sendData(const QByteArray& data);
void sendFrame(const QByteArray& frame); // 带协议封包
/// 获取串口状态
bool isOpen() const;
signals:
// ── 向 UI/业务层发送的信号 ──
void connected();
void disconnected();
void dataReceived(const QByteArray& frame);
void errorOccurred(const QString& errorMessage);
void portReset(); // 串口被外部重置(如热插拔)
public slots:
// ── 在工作线程中执行的槽 ──
void onOpen();
void onClose();
void onSendData(const QByteArray& data);
void onReadyRead();
void onError(QSerialPort::SerialPortError error);
private:
void reconnect(); // 自动重连逻辑
QSerialPort* m_port = nullptr;
AbstractProtocolParser* m_parser = nullptr;
bool m_parserOwned = false; // 是否拥有解析器所有权
QString m_portName;
QSerialPort::BaudRate m_baudRate;
QSerialPort::DataBits m_dataBits;
QSerialPort::Parity m_parity;
QSerialPort::StopBits m_stopBits;
QSerialPort::FlowControl m_flowControl;
// 发送队列与重连控制
QTimer* m_reconnectTimer = nullptr;
int m_reconnectIntervalMs = 3000;
int m_maxReconnectAttempts = 5;
int m_reconnectCount = 0;
};
cpp
// serial_port_manager.cpp
#include "serial_port_manager.h"
#include <QDebug>
#include <QThread>
SerialPortManager::SerialPortManager(QObject* parent)
: QObject(parent)
{
// 注意:QSerialPort 不在构造函数中 new!
// 它必须在 moveToThread + 工作线程事件循环启动后在 onOpen 中创建
}
SerialPortManager::~SerialPortManager()
{
close();
if (m_parserOwned && m_parser) {
delete m_parser;
m_parser = nullptr;
}
}
void SerialPortManager::configure(const QString& portName,
QSerialPort::BaudRate baudRate,
QSerialPort::DataBits dataBits,
QSerialPort::Parity parity,
QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl)
{
m_portName = portName;
m_baudRate = baudRate;
m_dataBits = dataBits;
m_parity = parity;
m_stopBits = stopBits;
m_flowControl = flowControl;
}
void SerialPortManager::setProtocolParser(AbstractProtocolParser* parser)
{
if (m_parserOwned && m_parser) delete m_parser;
m_parser = parser;
m_parserOwned = true;
}
bool SerialPortManager::open()
{
if (m_port && m_port->isOpen()) return true;
// 跨线程调用:通过信号槽触发工作线程的 onOpen
QMetaObject::invokeMethod(this, "onOpen", Qt::QueuedConnection);
return true;
}
void SerialPortManager::onOpen()
{
// 确保在工作线程中创建
if (!m_port) {
m_port = new QSerialPort(this);
}
if (m_port->isOpen()) {
m_port->close();
}
m_port->setPortName(m_portName);
m_port->setBaudRate(m_baudRate);
m_port->setDataBits(m_dataBits);
m_port->setParity(m_parity);
m_port->setStopBits(m_stopBits);
m_port->setFlowControl(m_flowControl);
// ------ 关键的信号连接 ------
connect(m_port, &QSerialPort::readyRead,
this, &SerialPortManager::onReadyRead);
connect(m_port, &QSerialPort::errorOccurred,
this, &SerialPortManager::onError);
if (!m_port->open(QIODevice::ReadWrite)) {
emit errorOccurred("Failed to open serial port: "
+ m_port->errorString());
return;
}
m_reconnectCount = 0;
emit connected();
if (m_parser) {
m_parser->reset();
}
}
void SerialPortManager::close()
{
QMetaObject::invokeMethod(this, "onClose", Qt::QueuedConnection);
}
void SerialPortManager::onClose()
{
if (m_reconnectTimer) {
m_reconnectTimer->stop();
}
if (m_port && m_port->isOpen()) {
m_port->close();
emit disconnected();
}
}
void SerialPortManager::sendData(const QByteArray& data)
{
QMetaObject::invokeMethod(this, "onSendData",
Qt::QueuedConnection,
Q_ARG(QByteArray, data));
}
void SerialPortManager::sendFrame(const QByteArray& frame)
{
// 如果协议规定了封包格式,在这里加帧头/校验
QByteArray packet;
// 示例:Modbus-like: [addr(1)][len(2)][data][crc(2)]
QByteArray header;
// ... 根据实际协议组装 ...
sendData(packet);
}
void SerialPortManager::onSendData(const QByteArray& data)
{
if (!m_port || !m_port->isOpen()) {
emit errorOccurred("Port not open, cannot send");
return;
}
qint64 written = m_port->write(data);
if (written != data.size()) {
emit errorOccurred("Write incomplete: "
+ QString::number(written)
+ " / " + QString::number(data.size()));
}
// 关键:对于高速通讯,必须调用 flush 或 waitForBytesWritten
// 但不要在主线程或 UI 线程中调 waitForBytesWritten(会阻塞)
if (!m_port->waitForBytesWritten(100)) {
// 写入超时,缓冲区可能堆积
emit errorOccurred("Write timeout, buffer may be full");
}
}
void SerialPortManager::onReadyRead()
{
if (!m_port) return;
const QByteArray rawData = m_port->readAll();
if (m_parser) {
// 交给协议解析器拆帧
QList<QByteArray> frames = m_parser->feed(rawData);
for (const auto& frame : frames) {
emit dataReceived(frame);
}
} else {
// 无解析器:原始数据直接发射,上层自己处理粘包
emit dataReceived(rawData);
}
}
void SerialPortManager::onError(QSerialPort::SerialPortError error)
{
if (error == QSerialPort::NoError) return;
QString errMsg = m_port ? m_port->errorString() : "Unknown error";
emit errorOccurred(errMsg);
// ------ 关键错误处理策略 ------
switch (error) {
case QSerialPort::ResourceError:
case QSerialPort::PermissionError:
case QSerialPort::DeviceNotFoundError:
// 串口被拔出/占用
m_port->close();
emit disconnected();
reconnect();
break;
case QSerialPort::TimeoutError:
// 超时可以重试,不用关端口
break;
default:
break;
}
}
void SerialPortManager::reconnect()
{
if (!m_reconnectTimer) {
m_reconnectTimer = new QTimer(this);
m_reconnectTimer->setSingleShot(true);
connect(m_reconnectTimer, &QTimer::timeout, this, [this]() {
if (m_reconnectCount < m_maxReconnectAttempts) {
m_reconnectCount++;
emit portReset();
onOpen(); // 尝试重连
} else {
emit errorOccurred(
"Max reconnect attempts reached, giving up");
}
});
}
if (!m_reconnectTimer->isActive()) {
m_reconnectTimer->start(m_reconnectIntervalMs);
}
}
bool SerialPortManager::isOpen() const
{
return m_port && m_port->isOpen();
}
2.4 主线程调用示例
cpp
// 在 UI 层使用
auto* manager = new SerialPortManager();
auto* workerThread = new QThread(this);
manager->moveToThread(workerThread);
workerThread->start();
// 配置
manager->configure("COM3", QSerialPort::Baud9600);
manager->setProtocolParser(new SimpleDelimiterParser("\r\n"));
// 连接信号
connect(manager, &SerialPortManager::connected, this, [this]() {
ui->statusLabel->setText("Connected");
ui->sendButton->setEnabled(true);
});
connect(manager, &SerialPortManager::disconnected, this, [this]() {
ui->statusLabel->setText("Disconnected");
ui->sendButton->setEnabled(false);
});
connect(manager, &SerialPortManager::dataReceived, this, [this](const QByteArray& frame) {
ui->textEdit->append(QString::fromUtf8(frame)); // 主线程安全更新 UI
});
connect(manager, &SerialPortManager::errorOccurred, this, [](const QString& msg) {
qWarning() << "Serial error:" << msg;
});
manager->open();
三、那些年我踩过的坑
3.1 readyRead 不保证一次性收完一帧
TCP 和串口都是流式协议。readyRead 在数据到达时触发,但数据可能只到了一半。全量读取 + 协议解析器拆帧 是唯一正确做法。不要假设一次 readAll 能拿到完整的一帧数据。
3.2 waitForBytesWritten 是阻塞的
在主线程中调 waitForBytesWritten 超过几十毫秒,UI 直接卡死。解决方案:把串口放到独立线程 ,或者在非阻塞模式下靠 bytesWritten 信号驱动。
3.3 高速通讯下的缓冲区溢出
9600 波特率无所谓,115200 甚至更高时,如果 readyRead 里处理慢了,系统接收缓冲区会溢出丢数据。
解决:
- 事件驱动的解析不要做 I/O 或复杂计算
- 大量数据处理扔给异步队列或线程池
- 可能的话调大系统缓冲区(Windows:
SetCommTimeouts/DCB 中的InQueue)
3.4 串口热插拔
Windows 下串口拔了,QSerialPort::ResourceError 会触发。Linux 下可能是 QSerialPort::DeviceNotFoundError。两种都要处理:关端口、通知用户、自动重连(带退避策略)。
3.5 跨平台差异
| 项目 | Windows | Linux | macOS |
|---|---|---|---|
| 端口名称 | COM1~COM256 |
/dev/ttyUSB0 |
/dev/cu.usbserial-* |
| 热插拔通知 | 需要手动轮询 | 可以通过 udev 监听 | IOKit 通知 |
| 波特率支持 | 几乎所有整数 | 有限制,需驱动支持 | 同 Linux |
我的做法 :上层统一用逻辑名,底层一个 PortEnumerator 做平台适配,自动扫可用端口。
四、项目级建议
4.1 协议解析器做成插件式
上面设计中 AbstractProtocolParser 就是接口。不同设备挂不同解析器,管理器代码一行不用改。
常见协议类型:
- ASCII 行协议 :
\r\n或\n定界 →SimpleDelimiterParser - 二进制长度前缀 :
[len(4)] + [payload]→LengthPrefixParser - 固定帧长:每帧固定 N 字节 → 按字节数切
- 带校验协议 :CRC/Checksum 验证 → 继承
AbstractProtocolParser加校验步骤
4.2 日志至关重要
每一条收发的原始数据都记日志,加上时间戳。我见过太多"串口不通"最后发现是线的问题或者对方设备根本没发数据。
cpp
// 在 onReadReady 和 onSendData 中加入
qDebug().noquote() << QString("[SERIAL %1] TX: %2")
.arg(m_portName, data.toHex(' ').toUpper());
qDebug().noquote() << QString("[SERIAL %1] RX: %2")
.arg(m_portName, rawData.toHex(' ').toUpper());
实际项目中,上 QSLOG 或 spdlog 落文件,排查问题时不至于抓瞎。
4.3 线程生命周期管理
cpp
// 析构时确保正确清理
void MainWindow::closeEvent(QCloseEvent* event)
{
manager->close(); // 1. 先关串口
workerThread->quit(); // 2. 退出事件循环
workerThread->wait(3000); // 3. 等线程结束(带超时保护)
delete manager; // 4. 释放管理器
delete workerThread; // 5. 释放线程对象
QMainWindow::closeEvent(event);
}
顺序错了就可能 crash,尤其是 manager 在析构时还在发信号。
五、总结
一个生产级的串口通讯方案,核心就三件事:
- 架构解耦:串口工作线程 ≠ UI 主线程,通过信号槽交换数据
- 协议分层:流式数据 → 协议解析器拆帧 → 业务层消费
- 防御优先:自动重连、日志全录、异常恢复路径必须覆盖热插拔
上面给出的框架,我从 Qt 5.6 用到现在 Qt 6.7,从 windows 到 Linux ,没有因为设计问题出过事故。你拿去根据自己的业务协议改改解析器就行,别重新造轮子。
最后说一句:串口编程归根结底是管理好"时序"和"状态"。设备端发的是什么协议就用什么解析器,别试图写一个"万能解析器"------那种东西不存在。上面这个模板能兼容所有串口通讯,是因为它把解析逻辑交给了可拔插的协议层,而不是试图硬写一个解析所有协议的怪物类。