Qt 串口通讯架构

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());

实际项目中,上 QSLOGspdlog 落文件,排查问题时不至于抓瞎。

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 在析构时还在发信号。


五、总结

一个生产级的串口通讯方案,核心就三件事

  1. 架构解耦:串口工作线程 ≠ UI 主线程,通过信号槽交换数据
  2. 协议分层:流式数据 → 协议解析器拆帧 → 业务层消费
  3. 防御优先:自动重连、日志全录、异常恢复路径必须覆盖热插拔

上面给出的框架,我从 Qt 5.6 用到现在 Qt 6.7,从 windows 到 Linux ,没有因为设计问题出过事故。你拿去根据自己的业务协议改改解析器就行,别重新造轮子


最后说一句:串口编程归根结底是管理好"时序"和"状态"。设备端发的是什么协议就用什么解析器,别试图写一个"万能解析器"------那种东西不存在。上面这个模板能兼容所有串口通讯,是因为它把解析逻辑交给了可拔插的协议层,而不是试图硬写一个解析所有协议的怪物类。

相关推荐
一条泥憨鱼1 小时前
深入理解2026AI最大公约数:Agent
开发语言·人工智能·ai·agent
刻BITTER1 小时前
Alpine.js + Chart.js 踩坑记:一次 Maximum Call Stack Exceeded 排查之旅
开发语言·javascript·ecmascript
郝学胜-神的一滴1 小时前
干货版《算法导论》05:从集合接口到排序
开发语言·数据结构·c++·程序人生·算法·排序
之歆1 小时前
Day15_JavaScript DOM 事件完全指南:从基础到实战(下)
开发语言·javascript·ecmascript
一切皆是因缘际会1 小时前
终结拟合式智能:记忆博弈心智架构重塑硅基生命进化逻辑
大数据·人工智能·深度学习·机器学习·架构
顾凌陵1 小时前
Python 数据可视化实战
开发语言·python·信息可视化
星恒随风1 小时前
从0开始的操作系统(3)
开发语言·笔记·学习
开发者联盟league1 小时前
pip install出现报错ERROR: Cannot set --home and --prefix together
开发语言·python·pip
FlagOS智算系统软件栈1 小时前
众智FlagOS完成腾讯混元MT2多语翻译模型全系列多芯片适配:英伟达/华为/平头哥三芯开箱即用
开发语言·人工智能·开源