高速串口的数据接收(一)
针对高速串口接收数据的做了一些优化,下面是最开始的代码,存在数据接收时掉帧,数据延时等问题,现在我们将下面代码逐步优化和讲解。
void MyQSerialPort::run()
{
if(!rev_ser->isOpen()) { return; }
Fixed_data_back m_data;
QByteArray buffer,temp;
int bufferlen = sizeof(Fixed_data_back);
while(!stop)
{
if(runningFlag) {
if(rev_ser->bytesAvailable())
{
uint16_t trycount = 5;
uint16_t lastlen = 0;
rev_ser->waitForReadyRead(50);
buffer = rev_ser->read(bufferlen);
lastlen = bufferlen - buffer.length();
while(lastlen) {
QThread::usleep(100);
rev_ser->waitForReadyRead(50);
temp = rev_ser->read(lastlen);
buffer += temp;
lastlen = bufferlen - buffer.length();
trycount--;
if((lastlen != 0) && trycount == 0) { break; }
}
if(buffer.length() == bufferlen) {
memcpy((void*)(&m_data),(void*)(buffer.data()),bufferlen);
if(CheckSumGuideData((uint8_t*)(&m_data))) {
//qDebug()<<QStringLiteral("接收正确");
}else {
qDebug()<<QStringLiteral("接收错误");
}
buffer.clear();
}
}
}
}
上述代码优化如下:
1、设用信号和槽-Qt框架的信号和槽机制是专门为了事件驱动的编程设计的,在“QSerialPort”中,有一个“readyRead()”信号,每当有新的数据可读时会发射这个信号,这比轮询方式更加高效,也更符合Qt的设计哲学。
2、移除不必要的循环等待-使用“QSerialPort”的“waitForReadyRead()”函数通常不推荐用于生产环境,因为它会阻塞事件循环,我们可以利用信号和槽来异步处理数据。
3、减少对“usleep()”的依赖-在GUI应用程序中,长时间的睡眠是一个坏习惯,因为它会阻塞执行线程。即使在工作线程中,使用“usleep()”也可能导致对串口响应不够灵敏。我们可以通过Qt的事件循环来优雅地处理这个问题。
4、异常处理-应该检查每个串口操作是否成功,并在必要时进行错误处理。
5、线程安全-如果“stop”和“runningFlag”被多个线程访问,需要确保它们的访问是线程安全的,比如使用互斥锁或者Qt的原子操作。
class MyQSerialPort : public QObject {
Q_OBJECT
public:
MyQSerialPort(QSerialPort *serialPort, QObject *parent = nullptr) :
QObject(parent), serial(serialPort) {
// 连接信号和槽
connect(serial, &QSerialPort::readyRead, this, &MyQSerialPort::handleReadyRead);
}
~MyQSerialPort() {
serial->close();
}
public slots:
void startRead() {
runningFlag = true;
}
void stopRead() {
runningFlag = false;
}
private slots:
void handleReadyRead() {
if (!runningFlag) return;
QByteArray data = serial->readAll();
processData(data);
}
private:
void processData(const QByteArray &data) {
// 处理数据
if (data.size() < sizeof(Fixed_data_back)) {
// 数据不完整,可以存储起来等待下次读取更多
buffer.append(data);
if (buffer.size() >= sizeof(Fixed_data_back)) {
// 现在我们有足够的数据进行处理
Fixed_data_back m_data;
memcpy(&m_data, buffer.constData(), sizeof(Fixed_data_back));
buffer.clear();
// 校验和处理
if(CheckSumGuideData(reinterpret_cast<uint8_t*>(&m_data))) {
// 数据校验成功
} else {
qDebug()<<QStringLiteral("接收错误");
}
}
} else {
// 直接处理数据
}
}
QSerialPort *serial;
QByteArray buffer;
bool runningFlag = false;
};
上面代码进行了如下优化:
- 使用信号和槽来代替手动循环和等待数据。
- 不再需要"usleep()"或者“waitForReadyRead()”。
- 使用成员函数“handleReadyRead()”来响应串口的"readyRead()"信号。
- 如果接收到的数据不完整,会被存储在“buffer”中,直到有足够的数据可以处理。
这段代码更加简洁,遵循了Qt的编程范式,应该提供更稳定和高效的串口读取操作。
在Qt中,通常不需要直接创建并使用"QThread"的实例来处理串口读写。因为“QSerialPort”已经在内部处理好了异步读写的问题,你只需要关心如何设置串口参数,以及如何连接信号与槽来处理数据即可。
如果你需要从UI线程之外操作串口(比如,UI线程需要保持响应用户操作,而串口操作可能比较耗时),可以将串口处理的逻辑放在一个单独的类中(比如你的“MyQSerialPort”类),然后在主线程中实例化这个类,并将它移动到一个新的“QThread”中。
以下是一个简单的示例说明如何创建一个线程并在其中运行“MyQSerialPort”类的实例:
#include <QCoreApplication>
#include <QSerialPort>
#include <QThread>
#include "MyQSerialPort.h" // 假设这是你的MyQSerialPort类的头文件
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
QSerialPort *serialPort = new QSerialPort(); // 创建串口对象
// 设置串口参数(端口名、波特率等)
MyQSerialPort *mySerialPortWorker = new MyQSerialPort(serialPort); // 创建你的串口处理对象
QThread *thread = new QThread(); // 创建一个新线程
mySerialPortWorker->moveToThread(thread); // 将串口处理对象移动到新线程
// 当线程启动时,开始串口读取操作
QObject::connect(thread, &QThread::started, mySerialPortWorker, &MyQSerialPort::startRead);
// 当线程结束时,进行清理
QObject::connect(thread, &QThread::finished, mySerialPortWorker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
// 开始线程
thread->start();
// 退出程序时,确保线程正确结束
int ret = a.exec();
thread->quit();
thread->wait();
delete serialPort; // 释放串口资源
return ret;
}
如果需要保证读取到完整的数据包,并且在处理后还能继续读取剩下的数据,可以在处理数据之后调整"buffer",而不是完全清空它。你需要确保"buffer"中的数据始终是完整的数据包。当处理完一个完整的数据包之后,应当只移除已经处理过的那部分数据。
下面是如何调整“processData”函数来处理这种情况的实例:
void processData(const QByteArray &data) {
// 将新接收的数据添加到buffer中
buffer.append(data);
// 当buffer中的数据足够组成至少一个完整的数据包时,进行处理
while (buffer.size() >= sizeof(Fixed_data_back)) {
// 从buffer的开始部分复制一个数据包大小的数据
Fixed_data_back m_data;
memcpy(&m_data, buffer.constData(), sizeof(Fixed_data_back));
// 检查这个数据包
if(CheckSumGuideData(reinterpret_cast<uint8_t*>(&m_data))) {
// 数据校验成功,处理数据
// ...
// 从buffer中移除已经处理过的数据
buffer = buffer.mid(sizeof(Fixed_data_back));
} else {
qDebug() << QStringLiteral("接收错误");
// 如果校验失败,决定如何处理错误。一种做法是移除错误的数据包,
// 另一种是保持buffer不变,等待更多数据。
// 例如,如果你想要丢弃错误的数据包:
buffer = buffer.mid(sizeof(Fixed_data_back));
// 或者,如果你想要等待更多数据,可能是因为数据包的一部分尚未到达:
break;
}
}
}
在上述代码中,我们不再简单地清除整个“buffer”,而是在处理完一个“Fixed_data_back”结构后,使用“QByteArray::mid()”函数来移除那部分已经处理过的数据。这样,“buffer”中剩余的数据仍然保留,可以用来检测更多的完整数据包。
此外,如果检测到错误的数据包(即校验失败),需要你决定是丢弃它还是保留在“buffer”中,等待可能的额外数据。在某些情况下,如果你怀疑接收到的数据只是暂时不完整,你可能希望等待更多数据的到来,而不是立即丢弃它。
上述processData函数可能会出现被调用传入数据时由于processData函数中的while还没执行完成导致数据未及时接收,即如果处理数据(比如校验和处理)的时间过长,可能会导致在处理期间到达的新数据没有被及时读取,从而可能导致数据缓冲区溢出或数据丢失。
为了解决这个问题,有几个策略可以考虑:
1、减少数据处理时间
优化“processData”函数以减少数据处理所需的时间。例如,可以优化“CheckSumGuidData”函数,或者减少在循环中执行的操作数量,这有助于减少数据处理的时间,从而减少对新数据处理的延迟。
2、使用单独的处理线程
将数据处理逻辑放在单独的线程中执行。这样、主线程可以专注于串口读取数据,而处理数据的工作在另一个线程中进行,两者之间通过线程安全的队列或其他机制进行通信。
void processData(const QByteArray &data) {
buffer.append(data);
// 检查是否有足够的数据
while (buffer.size() >= sizeof(Fixed_data_back)) {
// 将数据传送到处理线程
QByteArray package = buffer.left(sizeof(Fixed_data_back));
QMetaObject::invokeMethod(processingThread, "handleData", Q_ARG(QByteArray, package));
// 移除已经传送的数据
buffer.remove(0, sizeof(Fixed_data_back));
}
}
在这个策略中,“processingThread”是负责数据处理的线程,而“handleData”是在该线程中执行数据处理的方法。
3、数据分段处理
如果可能,尝试分段处理缓冲区的数据,而不是一次处理完所有数据。例如,可以在每次事件循环中只处理一定数量的数据包,然后返回时间循环,以便及时处理新到达的数据。
void processData(const QByteArray &data) {
buffer.append(data);
int packetsProcessed = 0;
while (buffer.size() >= sizeof(Fixed_data_back) && packetsProcessed < MAX_PACKETS_PER_LOOP) {
// 处理数据...
packetsProcessed++;
}
}
在这里,“MAX_PACKETS_PER_LOOP”是你定义的每次事件循环中可以处理的最大数据包数量。