Qt 多线程串口编程

一、问题

以前串口编程使用第三方的CnComm.h编程,CnComm作者博客链接,使用起来还蛮好的,不过既然用qt了就想着用qt自带的QSerialPort,移植性更好一些,结果折腾了好几天,主要遇到的问题就是多线程使用串口的问题,我使用串口有以下要求:

  • 需间隔周期发送数据,循环发送可开启\关闭,周期可设置;
  • 接收数据一帧数据量较大,且长度不定,需按照协议拼包处理。
  • 串口可打开可关闭;

基于以上需求和之前串口编程的经验,我做了如下设计:

  • 在主进程里申请了QSerialPort,主进程中将QSerialPort的readyRead和接收函数connect,在接收函数里将接收的数据不处理扔进队列。
  • 新建一个继承QThread的发送线程Object,这个Object包含一个发送数据的信号,将此Object信号和主进程的QSerialPort发送connect起来;在此Object的线程循环里拼装发送数据,控制发送周期。

程序运行起来之后也可以运行,通过虚拟串口与串口调试助手通信也都正常,但是每次发送数据,qt都会提示类似如下错误:

QObject: Cannot create children for a parent that is in a different thread,(Parent is QSerialPort(0x4ab1ab0), parent's thread is QThread(0xbe3860), current thread is QThread(0xc00df8)

虽然好像用着也没啥问题,但是心理总犯嘀咕,然后就想着把这个问题解决,然后就折腾了两天。

二、错误做法

2.1 错误做法1

因为提示不在同一个线程,于是我将QSerialPort在线程中创建和打开,让QSerialPort完全属于一个线程,但是这样带来的问题是readReady就无法被触发了,测试connect方式自动、直接、队列都无法被触发;在QSerialPort创建时候还是在串口打开之后再connect都无法成功,原因未明。

2.2 错误做法2

查阅资料,有些博主解释了这个报错的原因和解决方式,在主线程中创建QSerialPort,然后新建一个Object,将主线程的QSerialPort指针传入这个Object,然后再结合MoveToThread(),参见博主博客,可能是我理解不到位,我按照这个方式实现后,还是提示对象不在一个线程的问题;

三、最终做法

在知乎一个博主回答中,对多线程QSerialPort编程有进一步了解,摘抄原博主内容如下:

  • 不要写死循环!需要持续做的事情,做个timer,绑到timeout信号上
  • 写一个object,将其moveToThread
  • 该object,对外的所有操作,全部用信号槽,通过槽接收外部的调用操作(比如open、close、write),通过信号发送数据给外部(比如接收到的数据)
  • 需要在子线程分配的资源,比如QSerialPort,全部在该object的某个槽函数(如init)中进行,将该槽函数绑定到线程的started信号上
  • 程序退出时,不要直接delete object,因为那个对象不处于子线程。在需要退出时,执行object的deleteLater函数(直接调用或通过信号槽触发均可),这样就会由它所属的线程负责delete这个object。然后将object的destroyed信号,绑定到线程的quit槽上,将线程的finished信号绑定到QThread对象的deleteLater槽上。这样,销毁流程就是->子线程删除object->线程停止->线程对象销毁
  • 线程对象,以及move到线程里的对象,都不要设置parent7、Qt 4.8之后,可以把线程的finish信号直接绑到对象的deleteLater上,QThread会保证在子线程中删除这些对象。QThread这个基于类的子线程,比起传统的基于函数的子线程好处在于,你可以在线程中放任意多个对象运行。只要你保证所有对象都是通过信号槽运作,而不是像写c的子线程那样用while(true)运作,那么一个QThread就可以管理任意多个对象。

于是我做了如下修改,代码如下:

QSerialWorker.h:

#ifndef SERIALWORKER_H
#define SERIALWORKER_H

#include 
#include 
#include 
#include "QtCommon.h"
#include "dataqueue.h"
#include 

class SerialWorker : public QObject
{
    Q_OBJECT
public:
    explicit SerialWorker(DataQueue *pSerialQue,QObject *parent = nullptr);
    ~SerialWorker();
public:
    Qt::HANDLE getThreadID(){
        return QThread::currentThreadId();
    }
    void setSerialTimeout(int sec){
        if(sec > 0){
            s32SerialTimeout = sec;
        }
    }

    void setSerialParams(QString name,int baudrate,int parity,int data,int stop){
        serial_name=name;
        serial_baudrate = baudrate;
        serial_parity = parity;
        serial_data = data;
        serial_stop = stop;
    }

signals:
    void serialOperaRet(bool openOpera = true,bool ret = true);
    void serialBytesCount(int tx,int rx);
public slots:
    void serialInit();
    void sendSerialData(const char *buf,int len);
    void onRecvSerialData();

private:
    QSerialPort *mSerialPort = nullptr;
    QString     serial_name="COM10";
    int         serial_baudrate = 9600;
    int         serial_parity = 0;
    int         serial_data = 8;
    int         serial_stop = 1;
    QMutex      serialWriteMutex;
    DataQueue   *m_SerialDataQue;//主线程创建的接收队列
    int         s32SerialTimeout = 3000;
};

#endif // SERIALWORKER_H

QSerialWorker.cpp:

#include "SerialWorker.h"

SerialWorker::SerialWorker(DataQueue *pSerialQue,QObject *parent)
    : QObject(parent),
      m_SerialDataQue(pSerialQue)
{

}

SerialWorker::~SerialWorker()
{
    if(mSerialPort){
        if(mSerialPort->isOpen()){
            mSerialPort->clear();
            mSerialPort->close();
            LOG(INFO)<<"close serial";
            emit serialOperaRet(false,true);
            mSerialPort->deleteLater();
        }
    }
}

void SerialWorker::serialInit()
{
    mSerialPort = new QSerialPort();
    mSerialPort->setPortName(serial_name);
    mSerialPort->setBaudRate(serial_baudrate);
    mSerialPort->setDataBits(static_cast<QSerialPort::DataBits>(serial_data));
    mSerialPort->setStopBits(static_cast<QSerialPort::StopBits>(serial_stop));
    mSerialPort->setParity(static_cast<QSerialPort::Parity>(serial_parity));
    if(!mSerialPort->open(QIODevice::ReadWrite)){
        emit serialOperaRet(true,false);
        return ;
    }
    emit serialOperaRet();
    connect(mSerialPort,&QSerialPort::readyRead,this,&SerialWorker::onRecvSerialData);
}

void SerialWorker::sendSerialData(const char *data,int len)
{
    if(data == nullptr || len <= 0)return;
    QMutexLocker locker(&serialWriteMutex);
    if(mSerialPort && mSerialPort->isOpen()){
        mSerialPort->clear();
        mSerialPort->write(data,len);
        mSerialPort->flush();
        emit serialBytesCount(len,0);
        mSerialPort->waitForBytesWritten(s32SerialTimeout);
    }
}

void SerialWorker::onRecvSerialData()
{
    if(mSerialPort && mSerialPort->isOpen() && m_SerialDataQue){
        QByteArray readData;
        readData.append(mSerialPort->readAll());
        if(readData.isEmpty()){
            LOG(ERROR)<<"readdata error";
            return;
        }
        int len = readData.length();
        emit serialBytesCount(0,len);
        m_SerialDataQue->PushData(readData.data(),len);
    }
}

主进程头文件里申明了如下三个成员:

    SerialWorker     *mSerialWorker;
    QThread          *mSerialWorkThread;

主线程实现中在需要打开的时候添加以下程序:

mSerialWorker = new SerialWorker(&serialDataQue);
mSerialWorkThread = new QThread();
mSerialWorker->moveToThread(mSerialWorkThread);
connect(mSerialWorker, &SerialWorker::serialBytesCount, this, &MainWindow::onRecvSerialBytesCount);
connect(mSerialWorker, &SerialWorker::serialOperaRet, this,&MainWindow::onRecvSerialOperaRet);
connect(mSerialWorkThread, &QThread::finished,mSerialWorker, &QObject::deleteLater);
connect(mSerialWorkThread, &QThread::started,mSerialWorker, &SerialWorker::serialInit);
connect(this, &MainWindow::writeToSerial,mSerialWorker, &SerialWorker::sendSerialData);
QString name = “com10”;
int baudrate = 9600;
int data = 8;
int stop = 1;
int parity = 0;
mSerialWorker->setSerialParams(name,baudrate,parity,data,stop);
mSerialWorkThread->start();

主线程里需要关闭时候执行以下程序:

if(mSerialWorkThread){
     mSerialWorkThread->quit();
     mSerialWorkThread->wait();
     mSerialWorkThread->deleteLater();
     return;
 }

新建继承于QThread的发送线程,添加信号:

signals:
    void sendQueryData(const char *data,int len);

在主线程里添加connect:

connect(pSendThread,&sendQueryThread::sendQueryData,this,&MainWindow::writeToSerial);
sendQueryThread::Instance()->start();

经测试符合需求,正常接收,发送数据不再提示不同线程警告,继续验证这样持续打开关闭串口会不会出内存泄漏问题;
非常感谢各位在网络上无私分享的同仁,在此将自己这几天的调试过程记录下来,以供大家参考,水平有限,欢迎指正。

后记

最终因为个人能力有限还是无法很好的驾驭qt下串口编程,可能是我个人问题,我发现虽然我串口多线程了,但是串口读取的QThreadID还是和主线程ID一致,这与我所设想不合,我设想每个串口收发在应用层面完全独立,互不干扰,所以最终还是回归到windows下使用CnComm,linux下使用termios+select串口编程。

你可能感兴趣的:(QT,qt,开发语言)