【Qt原创开源项目】多功能串口调试助手3.0(增加数据帧解析功能,适用于PSINS导航开发板)

概述

由于数据更新频率是100Hz(即每10ms更新一次),而直接以100Hz的频率刷新UI(特别是频繁更新Label)可能会带来不必要的CPU负担,并且人眼也无法分辨如此快的更新。因此,我们可以通过以下方法来降低Label的刷新频率:

方法一:使用定时器进行限流

我们可以设置一个定时器,每隔一定时间(比如100ms)去更新一次Label,而在这段时间内,我们只保存最新的数据,并不立即刷新UI。

步骤:

  1. 创建一个成员变量来存储最新的数据值。

  2. 在串口数据解析后,更新这个成员变量(而不是直接更新Label)。

  3. 设置一个定时器,每隔一定时间(如100ms)读取这个成员变量的值,并更新Label。

示例代码

  • 头文件(例如mainwindow.h)中声明

private:

double m_latestValue;  // 存储最新的数据值

QTimer *m_displayTimer; // 用于定时刷新显示的定时器

  • 源文件(例如mainwindow.cpp)中实现

在构造函数中初始化定时器:


MainWindow::MainWindow(QWidget *parent)

: QMainWindow(parent)

{

// ... 其他初始化代码

m_latestValue = 0.0;

// 创建定时器,每100毫秒触发一次

m_displayTimer = new QTimer(this);

connect(m_displayTimer, &QTimer::timeout, this, &MainWindow::updateLabelDisplay);

m_displayTimer->start(100); // 100毫秒,即10Hz

}

// 在解析串口数据的槽函数中更新数据

void MainWindow::handleSerialData(const QByteArray &data)

{

// 解析数据,得到value

double value = parseData(data); // 假设parseData是你的解析函数

// 更新成员变量,但不更新界面

m_latestValue = value;

}

// 定时器触发的槽函数,用于更新Label显示

void MainWindow::updateLabelDisplay()

{

// 将最新的值显示到Label上

ui->labelValue->setText(QString::number(m_latestValue, 'f', 2)); // 假设显示两位小数

}

方法二:使用时间戳记录上一次更新时间

另一种方法是在每次更新Label时记录时间,只有距离上一次更新时间超过一定间隔才更新。这种方法适用于不想用定时器的情况,但逻辑稍微复杂。

示例代码

在头文件中声明一个成员变量记录上次更新时间:


private:

qint64 m_lastUpdateTime; // 记录上一次更新的时间(毫秒)

在构造函数中初始化:


MainWindow::MainWindow(QWidget *parent)

: QMainWindow(parent)

{

// ...

m_lastUpdateTime = 0;

}

在接收到数据并解析后:


void MainWindow::handleSerialData(const QByteArray &data)

{

double value = parseData(data);

qint64 currentTime = QDateTime::currentMSecsSinceEpoch();

// 如果距离上次更新时间已经超过100毫秒,则更新

if (currentTime - m_lastUpdateTime >= 100) {

ui->labelValue->setText(QString::number(value, 'f', 2));

m_lastUpdateTime = currentTime;

}

}

但是,这种方法有一个缺点:如果数据更新非常快,而我们在100ms内可能收到多个数据,那么只有第一个数据会触发更新,后面的数据都不会触发更新,直到下一个100ms间隔。这样就会显示第一个数据,而丢失后面的数据。因此,我们通常结合方法一,使用一个成员变量保存最新的值,然后在这个条件更新时显示最新的值。

改进后的代码:


void MainWindow::handleSerialData(const QByteArray &data)

{

double value = parseData(data);

m_latestValue = value; // 始终保存最新的值

qint64 currentTime = QDateTime::currentMSecsSinceEpoch();

if (currentTime - m_lastUpdateTime >= 100) {

ui->labelValue->setText(QString::number(m_latestValue, 'f', 2));

m_lastUpdateTime = currentTime;

}

}

总结

推荐使用方法一,因为它更清晰,并且定时器可以保证定期刷新。方法二虽然可以避免使用定时器,但需要每次接收数据时都检查时间,如果数据量很大,可能效率不如方法一。

选择方法一,我们只需要在定时器超时时更新界面,而串口数据接收只负责更新一个成员变量,这样界面更新和串口接收线程就解耦了(注意:串口接收通常是在一个单独的线程,但更新成员变量是简单的赋值操作,所以是线程安全的,只要数据类型是基本类型(如double)并且是原子操作。如果是在多线程环境下,且数据类型复杂,可能需要加锁。但在这个场景下,double的赋值在大多数平台上是原子的)。

注意:Qt的界面操作必须在主线程中,所以定时器的槽函数是在主线程中执行的,因此是安全的。

刷新频率选择

100Hz的数据更新,刷新界面降低到10Hz(100ms一次)是一个合理的选择,当然你也可以根据需要调整定时器的时间间隔。

解决方案(方法一)

根据之前的讨论,我们计划通过以下方式降低Label的刷新频率:

  1. 在接收到数据时,不立即更新UI,而是将数据存储起来。

  2. 使用一个定时器,以较低的频率(例如20Hz)从存储的数据中取出最新值并更新UI。

在3.0代码中,解析后的数据通过frameDisplay()函数更新到各个Label上。这个函数是在onFrameProcessed()槽中调用的,而onFrameProcessed()是在数据帧解析完成后发出的信号触发的。

因此,我们需要修改数据更新的机制:

  • 不再在每解析一帧数据后就更新UI,而是将解析后的数据存储在一个结构体中(只保留最新值)。

  • 使用定时器以固定间隔(如50ms,即20Hz)调用一个更新函数,该函数将存储的最新数据更新到UI上。

具体步骤:

  1. 在MainWindow类中添加一个成员变量来存储最新的数据帧(例如latestFrame)。

  2. 修改onFrameProcessed()槽函数,不再调用frameDisplay(),而是将解析得到的数据帧赋值给latestFrame

  3. 添加一个定时器,定时触发一个槽函数(例如updateDisplay()),在这个槽函数中调用frameDisplay()来显示latestFrame中的数据。

  4. frameDisplay()函数中,改为显示latestFrame的数据(注意:这里我们不再使用serialProcessor->currentFrame(),而是使用我们存储的latestFrame)。

注意:由于数据更新频率高,而显示更新频率低,我们需要确保在更新latestFrame时不会与定时器读取latestFrame发生冲突,因此使用互斥锁进行保护。

修改代码:

首先,在mainwindow.h中:

添加私有成员变量:

  • DataFrame latestFrame; // 存储最新的数据帧

  • QMutex frameMutex; // 用于保护latestFrame的互斥锁

  • QTimer *displayTimer; // 用于定时更新显示的定时器

添加私有槽函数:

  • void updateDisplay(); // 定时器触发的更新显示函数

然后,在mainwindow.cpp中:

在构造函数中初始化定时器:


MainWindow::MainWindow(QWidget *parent)

: QMainWindow(parent)

, ui(new Ui::MainWindow)

{

// ... 其他代码

// 初始化定时器

displayTimer = new QTimer(this);

connect(displayTimer, &QTimer::timeout, this, &MainWindow::updateDisplay);

// 设置刷新频率为20Hz(50ms)

displayTimer->start(50); // 50ms对应20Hz

// ... 其他代码

}

修改onFrameProcessed()槽函数:


void MainWindow::onFrameProcessed()

{

// 不再直接调用frameDisplay(),而是更新latestFrame

DataFrame frame = serialProcessor->currentFrame();

QMutexLocker locker(&frameMutex); // 加锁保护

latestFrame = frame; // 存储最新帧

}

修改frameDisplay()函数,改为从latestFrame中取数据:


void MainWindow::frameDisplay()

{

// 使用互斥锁保护

QMutexLocker locker(&frameMutex);

// 将latestFrame的数据显示到UI

// 注意:这里的数据已经是小端字节序转换后的,因为之前存储的是从serialProcessor->currentFrame()得到的

// 但是注意:在onFrameProcessed()中存储的frame已经经过qFromLittleEndian转换了吗?实际上在frameDisplay()中我们才进行转换,所以这里需要重新审视。

// 重新审视:在原来的frameDisplay()函数中,我们是从serialProcessor->currentFrame()获取数据,然后进行字节序转换和组合。

// 现在,我们将这个数据存储到latestFrame,所以latestFrame中存储的是未经转换的原始数据(因为serialProcessor->currentFrame()返回的是原始数据帧)。

// 因此,我们需要在frameDisplay()中仍然进行字节序转换和组合。

// 所以,这里我们使用latestFrame,但转换代码和原来一样。

double insLat = serialProcessor->combinePosition(qFromLittleEndian(latestFrame.Lat1), qFromLittleEndian(latestFrame.Lat2));

double insLon = serialProcessor->combinePosition(qFromLittleEndian(latestFrame.Lon1), qFromLittleEndian(latestFrame.Lon2));

double gpsLat = serialProcessor->combinePosition(qFromLittleEndian(latestFrame.GpsLat1), qFromLittleEndian(latestFrame.GpsLat2));

double gpsLon = serialProcessor->combinePosition(qFromLittleEndian(latestFrame.GpsLon1), qFromLittleEndian(latestFrame.GpsLon2));

// 然后更新各个Label,代码和原来一样...

ui->label_Gx->setText(QString::number(qFromLittleEndian(latestFrame.Gx), 'f', 4));

// ... 其他Label类似

}

但是注意:原来的frameDisplay()函数中已经做了这些转换,我们可以把转换部分保留,只是将数据来源改为latestFrame。因此,我们可以将原来的函数中frame替换为latestFrame,并加上互斥锁保护。

然而,我们也可以重构一下:将原来的frameDisplay()函数中关于数据转换和显示的部分提取出来,接受一个DataFrame参数。这样,我们可以在两个地方复用:一个是直接显示(如果以后需要),另一个是在定时器触发的显示中。

但为了简单起见,我们直接修改frameDisplay()函数,让它显示latestFrame,并加上互斥锁。

另外,我们添加的updateDisplay()槽函数非常简单:


void MainWindow::updateDisplay()

{

frameDisplay(); // 调用显示函数

}

这样,显示就会以20Hz的频率更新,而不是每次解析到数据帧就更新。

但是,请注意:在原来的设计中,onFrameProcessed()是在解析线程中调用的(假设SerialProcessor是在单独的线程中运行),那么我们需要确保线程安全。如果SerialProcessor不在单独的线程中,那么就不存在多线程问题,但为了健壮性,我们仍然使用互斥锁。

如果SerialProcessor是在主线程中运行的(即和UI同一个线程),那么就不需要互斥锁。但考虑到未来可能将解析放到子线程,建议保留互斥锁。

最后,别忘了在mainwindow.h中声明latestFrameframeMutexdisplayTimer

由于DataFrame是一个结构体,我们需要包含其定义。

另外,在mainwindow.h中,我们可能需要前置声明SerialProcessor,因为我们在mainwindow.cpp中使用了它。

总结修改步骤:

  1. mainwindow.h中添加:

#include "serialprocessor.h" // 如果还没有包含

#include <QMutex>

#include <QTimer>

class MainWindow : public QMainWindow

{

// ... 其他代码

private:

// ... 其他成员

DataFrame latestFrame;  // 存储最新的数据帧

QMutex frameMutex;      // 保护latestFrame

QTimer *displayTimer;   // 显示更新定时器

private slots:

void updateDisplay();   // 定时更新UI

};

  1. mainwindow.cpp中:
  • 构造函数初始化定时器并启动

  • 修改onFrameProcessed():存储最新帧(加锁)

  • 修改frameDisplay():显示latestFrame(加锁,并注意数据转换)

  • 实现updateDisplay()槽:调用frameDisplay()

注意:如果DataFrame结构体很大,赋值操作可能会耗费一定时间。如果担心性能,可以考虑使用指针或引用,但要注意生命周期和线程安全。这里我们使用简单的拷贝,因为数据量不大(估计100字节左右),且20Hz的更新频率不会造成负担。

这样修改后,UI的更新频率就降低到了20Hz,从而减轻了UI线程的负担。

根据实际情况可调整定时器的间隔(50ms是20Hz,也可以设置成其他值,比如100ms即10Hz)。

另外,如果数据更新非常快,而显示更新慢,那么中间的数据会被覆盖,只显示最新的数据,这是符合预期的。

完整代码(方法一)

修改步骤

  • 1 在mainwindow.h中添加成员变量
// mainwindow.h
private:
    // ... 其他成员变量 ...
    
    // 数据缓存
    DataFrame latestFrame;
    QMutex frameMutex;  // 用于保护latestFrame的互斥锁
    
    // 显示控制
    QTimer *displayTimer;  // 显示刷新定时器
    int refreshRate = 20;  // 刷新频率(Hz),默认为20Hz
    bool displayUpdatePending = false; // 标记是否有待更新的显示
  • 2 在构造函数中初始化定时器
// mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    UiInit();

    /* 数据接收信号槽链接 */
    connect(COM, SIGNAL(readyRead()), this, SLOT(COM_DataReceive()));

    serialProcessor = new SerialProcessor(this);
    connect(serialProcessor, &SerialProcessor::frameProcessed, this, &MainWindow::onFrameProcessed);
    connect(serialProcessor, &SerialProcessor::checksumError, this, &MainWindow::onChecksumError);
    
    // 添加显示刷新定时器
    displayTimer = new QTimer(this);
    connect(displayTimer, &QTimer::timeout, this, &MainWindow::updateDisplay);
    
    // 设置刷新频率(20Hz = 50ms)
    int interval = 1000 / refreshRate; // 毫秒
    displayTimer->start(interval);
}
  • 3 修改onFrameProcessed()函数
void MainWindow::onFrameProcessed()
{
    // 不再直接调用frameDisplay()
    // 而是将最新数据存储到缓存中
    DataFrame frame = serialProcessor->currentFrame();
    
    QMutexLocker locker(&frameMutex);
    latestFrame = frame;
    displayUpdatePending = true; // 标记有新的数据待显示
}
  • 4 添加新的显示更新函数
void MainWindow::updateDisplay()
{
    if (!displayUpdatePending) {
        return; // 没有新的数据更新
    }
    
    DataFrame frame;
    {
        QMutexLocker locker(&frameMutex);
        frame = latestFrame;
        displayUpdatePending = false; // 重置标记
    }
    
    // 调用显示函数,传入当前帧
    frameDisplay(frame);
}

// 修改frameDisplay函数,接受DataFrame参数
void MainWindow::frameDisplay(const DataFrame &frame)
{
    // 字节序转换
    double insLat = serialProcessor->combinePosition(qFromLittleEndian(frame.Lat1), qFromLittleEndian(frame.Lat2));
    double insLon = serialProcessor->combinePosition(qFromLittleEndian(frame.Lon1), qFromLittleEndian(frame.Lon2));
    double gpsLat = serialProcessor->combinePosition(qFromLittleEndian(frame.GpsLat1), qFromLittleEndian(frame.GpsLat2));
    double gpsLon = serialProcessor->combinePosition(qFromLittleEndian(frame.GpsLon1), qFromLittleEndian(frame.GpsLon2));

    // 更新UI显示
    ui->label_Gx->setText(QString::number(qFromLittleEndian(frame.Gx), 'f', 4));
    ui->label_Gy->setText(QString::number(qFromLittleEndian(frame.Gy), 'f', 4));
    ui->label_Gz->setText(QString::number(qFromLittleEndian(frame.Gz), 'f', 4));
    ui->label_Ax->setText(QString::number(qFromLittleEndian(frame.Ax), 'f', 4));
    ui->label_Ay->setText(QString::number(qFromLittleEndian(frame.Ay), 'f', 4));
    ui->label_Az->setText(QString::number(qFromLittleEndian(frame.Az), 'f', 4));
    ui->label_Mx->setText(QString::number(qFromLittleEndian(frame.Mx), 'f', 4));
    ui->label_My->setText(QString::number(qFromLittleEndian(frame.My), 'f', 4));
    ui->label_Mz->setText(QString::number(qFromLittleEndian(frame.Mz), 'f', 4));
    ui->label_InsLon->setText(QString::number(insLon, 'f', 6));
    ui->label_InsLat->setText(QString::number(insLat, 'f', 6));
    ui->label_InsHgt->setText(QString::number(qFromLittleEndian(frame.Hgt), 'f', 6));
    ui->label_VE->setText(QString::number(qFromLittleEndian(frame.VE), 'f', 4));
    ui->label_VN->setText(QString::number(qFromLittleEndian(frame.VN), 'f', 4));
    ui->label_VU->setText(QString::number(qFromLittleEndian(frame.VU), 'f', 4));
    ui->label_GpsLon->setText(QString::number(gpsLon, 'f', 6));
    ui->label_GpsLat->setText(QString::number(gpsLat, 'f', 6));
    ui->label_GpsHgt->setText(QString::number(qFromLittleEndian(frame.GpsHgt), 'f', 6));
    ui->label_GpsVE->setText(QString::number(qFromLittleEndian(frame.GpsVE), 'f', 4));
    ui->label_GpsVN->setText(QString::number(qFromLittleEndian(frame.GpsVN), 'f', 4));
    ui->label_GpsVU->setText(QString::number(qFromLittleEndian(frame.GpsVU), 'f', 4));
    ui->label_Pitch->setText(QString::number(qFromLittleEndian(frame.Pitch), 'f', 4));
    ui->label_Roll->setText(QString::number(qFromLittleEndian(frame.Roll), 'f', 4));
    ui->label_Yaw->setText(QString::number(qFromLittleEndian(frame.Yaw), 'f', 4));
    ui->label_BaroHgt->setText(QString::number(qFromLittleEndian(frame.BaroHgt), 'f', 2));
    ui->label_Temp->setText(QString::number(qFromLittleEndian(frame.Temp), 'f', 1));
}
  • 5 添加刷新率控制功能(可选)
// 在UI中添加滑块或控件来调整刷新率
void MainWindow::on_refreshRateSlider_valueChanged(int value)
{
    refreshRate = qBound(1, value, 100); // 限制在1-100Hz之间
    int interval = 1000 / refreshRate;
    displayTimer->setInterval(interval);
    
    // 更新状态栏显示
    ui->statusbar->showMessage(QString("显示刷新率: %1 Hz").arg(refreshRate), 2000);
}
  • 6 在UI中添加刷新率控制(可选)
    在界面中添加一个滑块和标签来调整刷新率:
// 在UiInit()函数中添加
void MainWindow::UiInit()
{
    // ... 其他初始化代码 ...
    
    // 添加刷新率控制滑块
    QSlider *refreshSlider = new QSlider(Qt::Horizontal, this);
    refreshSlider->setRange(1, 100); // 1-100Hz
    refreshSlider->setValue(refreshRate);
    refreshSlider->setFixedWidth(150);
    connect(refreshSlider, &QSlider::valueChanged, this, &MainWindow::on_refreshRateSlider_valueChanged);
    
    QLabel *refreshLabel = new QLabel(QString("刷新率: %1 Hz").arg(refreshRate), this);
    
    // 添加到状态栏
    ui->statusbar->addPermanentWidget(new QLabel("显示刷新率:", this));
    ui->statusbar->addPermanentWidget(refreshSlider);
    ui->statusbar->addPermanentWidget(refreshLabel);
}

实现原理说明

1 数据缓存机制:

  • 使用latestFrame变量存储最新的解析数据

  • 使用QMutex保护数据访问,确保线程安全

  • 设置displayUpdatePending标志表示有新数据待显示

2 定时刷新控制:

  • QTimer定时触发显示更新(默认20Hz)

  • 仅当有新数据时才执行UI更新操作

  • 避免无效的UI刷新操作

3 性能优化:

  • 将UI更新频率从100Hz降低到20Hz(可配置)

  • 减少80%的UI更新操作

  • 避免UI线程过载导致的卡顿

4 数据完整性:

  • 每次显示时使用最新的数据帧

  • 确保用户看到的是最近的数据

额外建议

1 添加平滑处理(可选):

// 在frameDisplay函数中添加数据平滑处理
double smoothedValue = 0.0;
double smoothingFactor = 0.2; // 平滑因子

// 在更新每个Label前添加平滑处理
double rawValue = qFromLittleEndian(frame.Gx);
smoothedValue = smoothingFactor * rawValue + (1 - smoothingFactor) * smoothedValue;
ui->label_Gx->setText(QString::number(smoothedValue, 'f', 4));

2 添加性能监视

// 在updateDisplay函数中添加
static QElapsedTimer perfTimer;
static int frameCount = 0;

if (frameCount == 0) {
    perfTimer.start();
}

frameCount++;

if (perfTimer.elapsed() > 1000) {
    double actualFPS = frameCount / (perfTimer.elapsed() / 1000.0);
    ui->statusbar->showMessage(QString("实际刷新率: %1 FPS").arg(actualFPS, 0, 'f', 1), 2000);
    frameCount = 0;
    perfTimer.restart();
}

这个解决方案将UI刷新频率从100Hz降低到可配置的频率(默认20Hz),显著减少了UI线程的工作量,同时确保用户看到的是最新的数据。通过互斥锁保护数据访问,保证了线程安全,并且添加了平滑处理选项以改善显示效果。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐