一、问题
以前串口编程使用第三方的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 <QObject>
#include <QThread>
#include <QMutex>
#include "QtCommon.h"
#include "dataqueue.h"
#include <QSerialPort>
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串口编程。