基于 Qt 的多线程串口调试工具

采用 Qt 推荐的 QObject::moveToThread() 方案,将耗时的串口读写操作剥离到后台工作线程,主线程只负责 UI 刷新和用户交互。


一、 整体架构设计

我们将项目分为三个核心部分:

  1. UI 层 (MainWindow):负责界面展示、按钮响应、数据可视化。
  2. 业务层 (SerialWorker) :纯业务逻辑类,包含 QSerialPort,负责数据的打包、解析、CRC校验等。
  3. 线程层 (QThread):后台运行的工作线程,不处理具体业务,只为 Worker 提供事件循环。

数据流方向:

  • 发数据 :UI -> 信号触发 -> Worker 槽函数 -> QSerialPort::write()
  • 收数据QSerialPort 触发 readyRead -> Worker 读取 -> 发出信号 -> UI 更新

二、 核心代码实现

1. 串口工作类 (serialworker.h & .cpp)

这个类将在后台线程中运行,千万不要在此类中操作任何 UI 控件

cpp 复制代码
// serialworker.h
#ifndef SERIALWORKER_H
#define SERIALWORKER_H

#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QDebug>

class SerialWorker : public QObject
{
    Q_OBJECT
public:
    explicit SerialWorker(QObject *parent = nullptr);
    ~SerialWorker();

public slots:
    // 供 UI 线程调用的槽函数
    void openSerial(const QString &portName, int baudRate);
    void closeSerial();
    void sendData(const QByteArray &data);

private slots:
    // 内部私有槽,用于处理串口数据接收
    void onReadyRead();

signals:
    // 供 UI 线程接收的信号
    void dataReceived(const QByteArray &data);
    void statusChanged(const QString &status);

private:
    QSerialPort *m_serialPort;
};

#endif // SERIALWORKER_H
cpp 复制代码
// serialworker.cpp
#include "serialworker.h"

SerialWorker::SerialWorker(QObject *parent) : QObject(parent)
{
    m_serialPort = new QSerialPort(this);
    connect(m_serialPort, &QSerialPort::readyRead, this, &SerialWorker::onReadyRead);
}

SerialWorker::~SerialWorker()
{
    if (m_serialPort->isOpen()) {
        m_serialPort->close();
    }
}

void SerialWorker::openSerial(const QString &portName, int baudRate)
{
    m_serialPort->setPortName(portName);
    m_serialPort->setBaudRate(baudRate);
    m_serialPort->setDataBits(QSerialPort::Data8);
    m_serialPort->setParity(QSerialPort::NoParity);
    m_serialPort->setStopBits(QSerialPort::OneStop);
    m_serialPort->setFlowControl(QSerialPort::NoFlowControl);

    if (m_serialPort->open(QIODevice::ReadWrite)) {
        emit statusChanged("串口已打开: " + portName);
    } else {
        emit statusChanged("打开失败: " + m_serialPort->errorString());
    }
}

void SerialWorker::closeSerial()
{
    if (m_serialPort->isOpen()) {
        m_serialPort->close();
        emit statusChanged("串口已关闭");
    }
}

void SerialWorker::sendData(const QByteArray &data)
{
    if (m_serialPort->isOpen() && !data.isEmpty()) {
        m_serialPort->write(data);
    }
}

void SerialWorker::onReadyRead()
{
    // 这里的 sleep 是为了应对数据粘包,根据实际情况调整或移除
    QThread::msleep(10); 
    QByteArray buffer = m_serialPort->readAll();
    
    // 如果数据没读完,继续读(防止高频率数据丢失)
    while (m_serialPort->waitForReadyRead(10)) {
        buffer.append(m_serialPort->readAll());
    }

    if (!buffer.isEmpty()) {
        emit dataReceived(buffer); // 发射信号,将数据抛给 UI 层
    }
}
2. 主界面与多线程绑定 (mainwindow.cpp)

在这里完成线程的创建、Worker 的转移以及信号槽的连接。

cpp 复制代码
// mainwindow.cpp 关键片段
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 1. 创建线程和 Worker
    QThread* serialThread = new QThread(this);
    SerialWorker* serialWorker = new SerialWorker();

    // 2. 将 Worker 移动到新线程
    serialWorker->moveToThread(serialThread);

    // 3. 连接信号槽(跨线程,Qt::QueuedConnection 是默认的自动连接方式)
    // UI -> Worker (触发打开/关闭/发送)
    connect(ui->btnOpen, &QPushButton::clicked, {
        serialWorker->openSerial(ui->comboPort->currentText(), 115200);
    });
    connect(ui->btnSend, &QPushButton::clicked, {
        QByteArray data = ui->textEditSend->toPlainText().toUtf8();
        serialWorker->sendData(data);
    });

    // Worker -> UI (更新接收区/状态栏)
    connect(serialWorker, &SerialWorker::dataReceived, this, const QByteArray& data{
        ui->textEditRecv->append(QString::fromUtf8(data));
    });
    connect(serialWorker, &SerialWorker::statusChanged, this, const QString& msg{
        ui->statusbar->showMessage(msg);
    });

    // 4. 启动线程
    serialThread->start();
}

MainWindow::~MainWindow()
{
    // 优雅退出线程
    serialThread->quit();
    serialThread->wait();
    delete serialWorker; // 注意:由于使用了 moveToThread,必须在线程结束后删除
    delete ui;
}

三、 进阶功能实现建议(针对嵌入式调试)

作为一个硬核的嵌入式开发者,你的调试工具还需要以下几个"杀手锏"功能:

1. 十六进制(Hex)收发

SerialWorker::sendDataonReadyRead 中加入转换逻辑:

cpp 复制代码
// 发送前:将 "AA BB CC" 转换为 QByteArray
QByteArray hexStrToByteArray(const QString &str) {
    QByteArray ret;
    QStringList sl = str.trimmed().split(" ");
    foreach (QString s, sl) {
        ret.append((char)s.toUInt(nullptr, 16));
    }
    return ret;
}

// 接收后:将 QByteArray 转换为 "AA BB CC"
QString byteArrayToHexStr(const QByteArray &ba) {
    QString ret;
    for (int i = 0; i < ba.size(); ++i) {
        ret.append(QString("%1 ").arg((quint8)ba.at(i), 2, 16, QChar('0')).toUpper());
    }
    return ret;
}
2. 高并发数据实时绘图(使用 QCustomPlot)

如果在 UI 层直接 new QPoint 并刷新图表,CPU 占用率会飙升。

  • 优化方案 :在 SerialWorker 中解析出数据后,通过信号把 double 数值发给 UI,UI 层使用 QTimer(例如每 50ms 刷新一次图表)批量从队列中取数据绘制,而不是来一个数据画一个点。
3. 串口自动重连与设备热插拔

利用 Windows 的本地 API 或轮询 QSerialPortInfo::availablePorts() 来检测设备拔出或插入,并通过信号通知 UI 弹出提示。

参考代码 基于QT的多线程串口调试工具 www.youwenfan.com/contentcsu/70247.html

四、 避坑

  1. 不要跨线程直接调用对象方法
    永远使用 signal/slot 机制。如果你在 UI 线程直接调用 serialWorker->sendData(),它就不再具备线程安全性,会导致不可预知的崩溃。
  2. 内存释放顺序
    由于使用了 moveToThreadSerialWorker 的析构函数会在其所在的线程中执行。绝对不能 在 UI 线程的析构函数中直接 delete serialWorker;,必须使用 deleteLater() 或者等待线程完全结束后再释放。
  3. 事件循环(Event Loop)
    moveToThread 强依赖目标线程的事件循环。如果你在子线程中写了死循环或者阻塞了线程,SerialWorker 中的槽函数(包括 onReadyRead)将永远不会被执行。
相关推荐
郑同学的笔记2 小时前
【Qt教程29】Qt5和Qt6版本对比
开发语言·qt
基德爆肝c语言2 小时前
Qt 主窗口全家桶:菜单栏、工具栏、状态栏与对话框完全指南
开发语言·qt
BatyTao5 小时前
QT Creator快速创建主项目-子项目
qt
赤水无泪7 小时前
Qt 全模块汇总列表
开发语言·qt
GoKu~8 小时前
QT视图界面
qt
冰山一脚201310 小时前
QWidget的初始化颜色来自于哪里的笔记
qt
小陶来咯12 小时前
aimrt中间件的使用
开发语言·qt·中间件
music score12 小时前
google 的C++自动化测试框架详解(Google Test)(2)
c++·qt·lucene
小短腿的代码世界13 小时前
Qt_Qwt深度解析:从源码到工业级性能优化
开发语言·qt·性能优化