You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

QT下串口通信线程实现周期发送与触发接收的正确架构方案

Qt串口通信线程正确架构方案

现有方案的核心问题

你当前的实现存在几个关键缺陷,无法稳定运行:

  • 继承QThread重写run的逻辑里是无间隔死循环,会占满单核心CPU,同时没有调用exec()启动线程事件循环,你定义的QTimer、QSerialPort的信号完全无法触发,绑定的槽函数不会执行
  • 用互斥锁tryLock作为任务触发标记的逻辑完全冗余,且代码中tryLock后再次调用lock会直接触发死锁(非递归QMutex不允许同一线程重复加锁)
  • 串口、定时器对象的初始化和操作跨线程,会触发Qt的线程安全警告,甚至直接崩溃

推荐架构实现要点

Qt官方更推荐Worker对象+moveToThread的方案实现异步任务线程,天生适配你需要的周期性任务+触发式任务的需求,无需手动用锁模拟触发逻辑:

  1. 抽离所有通信逻辑到独立的Worker类,仅继承QObject即可,不需要继承QThread
  2. 主线程创建QThread实例和Worker实例,调用moveToThread将Worker绑定到子线程,所有Worker的槽函数都会在子线程上下文执行
  3. 周期性发送任务直接用Worker内部的QTimer实现,timeout信号绑定发送逻辑槽,设置对应周期即可自动触发
  4. 触发式接收任务直接用QSerialPort的readyRead信号绑定接收解析槽,有数据到达时自动执行
  5. 共享通信结构体的访问统一加QMutex保护:
    • 子线程写、主线程读的字段(返回状态、反馈角度):子线程更新时加锁,主线程读取时也加锁
    • 主线程写、子线程读的字段(发送参数、配置信息):要么通过跨线程信号槽传递(队列连接自动保证线程安全),要么加锁访问
  6. 不允许丢失的位置数据,每次提取后直接通过信号发往主线程绘图控件,Qt的队列连接会自动缓存所有信号不会丢失,非敏感状态数据可以攒固定间隔再发信号更新界面,降低主线程压力

代码示例

通信Worker类实现

#include <QObject>
#include <QSerialPort>
#include <QTimer>
#include <QMutex>
#include <QMutexLocker>

// 提前注册自定义类型用于跨线程信号传递
Q_DECLARE_METATYPE(Communication_Packets_st)

class CommWorker : public QObject
{
    Q_OBJECT
public:
    explicit CommWorker(QObject *parent = nullptr) : QObject(parent) {
        m_serial = new QSerialPort(this);
        m_sendTimer = new QTimer(this);
        // 内部信号槽绑定,都在子线程执行
        connect(m_sendTimer, &QTimer::timeout, this, &CommWorker::handleSendTask);
        connect(m_serial, &QSerialPort::readyRead, this, &CommWorker::handleRecvTask);
        m_commMutex = new QMutex(QMutex::Recursive);
    }

    ~CommWorker() {
        if(m_serial->isOpen()) m_serial->close();
        delete m_commMutex;
    }

public slots:
    // 子线程启动后执行初始化,保证所有IO对象在子线程上下文创建
    void initPort(QString portName, int baudRate, int sendIntervalMs) {
        m_periodSec = sendIntervalMs / 1000.0;
        m_serial->setPortName(portName);
        m_serial->setBaudRate(baudRate);
        m_serial->setDataBits(QSerialPort::Data8);
        m_serial->setParity(QSerialPort::NoParity);
        m_serial->setStopBits(QSerialPort::OneStop);
        if(m_serial->open(QIODevice::ReadWrite)) {
            m_sendTimer->start(sendIntervalMs);
        } else {
            emit errorOccurred(m_serial->errorString());
        }
    }

    // 主线程更新发送参数的入口,线程安全
    void updateSendAngle(double angle) {
        QMutexLocker locker(m_commMutex);
        m_sendAngle = angle;
    }

    // 主线程读取全量状态的入口,线程安全
    void getFullStatus(Communication_Packets_st *outStatus) {
        QMutexLocker locker(m_commMutex);
        memcpy(outStatus, &m_commManager, sizeof(Communication_Packets_st));
    }

private slots:
    // 周期性发送任务
    void handleSendTask() {
        QMutexLocker locker(m_commMutex);
        // 填充发送报文
        m_commManager.ControlCommandPacket_s.CommandAngle_i32 = static_cast<int32_t>(m_sendAngle * COMMAND_ANGLE_MULTIPLIER);
        // 调用发送接口
        Comm_SendControlCommand(&m_commManager);
        // 发信号通知主线程更新发送位置,不会丢失
        emit sendPosUpdated(m_sendIndex * m_periodSec, m_sendAngle);
        m_sendIndex++;
    }

    // 触发式接收任务
    void handleRecvTask() {
        QByteArray recvBuf = m_serial->readAll();
        QMutexLocker locker(m_commMutex);
        // 解析报文
        Comm_PacketParser(&m_commManager, recvBuf.constData(), recvBuf.size());
        // 提取反馈位置发往主线程
        double recvAngle = static_cast<double>(m_commManager.ControlCommandResponsePacket_s.AxisAngle_i32) * FEEDBACK_ANGLE_MULTIPLIER;
        emit recvPosUpdated(m_sendIndex * m_periodSec, recvAngle);
    }

signals:
    void sendPosUpdated(double time, double angle);
    void recvPosUpdated(double time, double angle);
    void errorOccurred(QString errMsg);

private:
    QSerialPort *m_serial;
    QTimer *m_sendTimer;
    QMutex *m_commMutex;
    Communication_Packets_st m_commManager;
    double m_sendAngle = 0.0;
    double m_periodSec = 0.01;
    uint32_t m_sendIndex = 0;
};

主线程调用示例

// 主线程头文件中声明成员
QThread *m_commThread;
CommWorker *m_commWorker;

// 主线程初始化逻辑
void MainWindow::initCommThread() {
    qRegisterMetaType<Communication_Packets_st>();
    m_commThread = new QThread(this);
    m_commWorker = new CommWorker();
    m_commWorker->moveToThread(m_commThread);

    // 绑定信号槽
    connect(m_commThread, &QThread::started, this, [=](){
        m_commWorker->initPort("COM3", 115200, 10); // 10ms间隔对应100Hz发送频率
    });
    // 位置数据更新绑定到绘图控件
    connect(m_commWorker, &CommWorker::sendPosUpdated, ui->plotWidget, &PlotWidget::addSendPoint);
    connect(m_commWorker, &CommWorker::recvPosUpdated, ui->plotWidget, &PlotWidget::addRecvPoint);
    // 自动回收资源
    connect(m_commThread, &QThread::finished, m_commWorker, &QObject::deleteLater);
    connect(m_commThread, &QThread::finished, m_commThread, &QObject::deleteLater);

    m_commThread->start();
}

// 窗口关闭时退出线程
void MainWindow::closeEvent(QCloseEvent *event) {
    if(m_commThread && m_commThread->isRunning()) {
        m_commThread->quit();
        m_commThread->wait(1000);
    }
    event->accept();
}

注意事项

  • 不要在主线程直接操作Worker内部的QSerialPort、QTimer对象,所有操作都通过槽函数触发
  • 自定义类型如果要作为跨线程信号的参数,必须提前用qRegisterMetaType注册
  • 尽量用QMutexLocker自动管理锁的生命周期,避免异常场景下锁未释放导致死锁

内容的提问来源于stack exchange,提问作者neko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 18:30:04