采用 Qt 推荐的 QObject::moveToThread() 方案,将耗时的串口读写操作剥离到后台工作线程,主线程只负责 UI 刷新和用户交互。
一、 整体架构设计
我们将项目分为三个核心部分:
- UI 层 (
MainWindow):负责界面展示、按钮响应、数据可视化。 - 业务层 (
SerialWorker) :纯业务逻辑类,包含QSerialPort,负责数据的打包、解析、CRC校验等。 - 线程层 (
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::sendData 和 onReadyRead 中加入转换逻辑:
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
四、 避坑
- 不要跨线程直接调用对象方法 :
永远使用signal/slot机制。如果你在 UI 线程直接调用serialWorker->sendData(),它就不再具备线程安全性,会导致不可预知的崩溃。 - 内存释放顺序 :
由于使用了moveToThread,SerialWorker的析构函数会在其所在的线程中执行。绝对不能 在 UI 线程的析构函数中直接delete serialWorker;,必须使用deleteLater()或者等待线程完全结束后再释放。 - 事件循环(Event Loop) :
moveToThread强依赖目标线程的事件循环。如果你在子线程中写了死循环或者阻塞了线程,SerialWorker中的槽函数(包括onReadyRead)将永远不会被执行。