基于 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)将永远不会被执行。
相关推荐
Quz2 天前
QML Hello World 入门示例
qt
xcyxiner5 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner6 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner6 天前
DicomViewer (添加模型类)3
qt
xcyxiner7 天前
DicomViewer (目录调整) 2
qt
xcyxiner7 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能9 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G9 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt
森G9 天前
77、线程池原理和实现------服务器源码解析----云视频服务项目
服务器·c++·qt
森G9 天前
71、打包发布---------打包发布
c++·qt