基于Qt的多功能串口通信工具分享:实时数据收发与波形绘制

20 篇文章 0 订阅

需要工程源码请私信

基于 Qt 框架开发的多功能串口通信工具,旨在为用户提供稳定、流畅的串口数据收发体验。该工具不仅支持基本的串口通信功能,还集成了定时发送、多线程数据处理、粘包问题解决、实时波形绘制等多种高级功能。通过使用 QSerialPort 进行串口操作,并结合 QSettings 进行配置文件管理,用户可以灵活地配置通信参数,实现对外部设备的数据交互和监控。此外,软件通过使用多线程技术确保串口通信的平稳性,避免因大量数据传输导致界面卡顿。其粘包拆解机制和波形绘制功能,帮助用户更直观地观察通信数据的变化,为硬件调试和通信测试提供了强有力的支持。
在这里插入图片描述

1. 多线程串口通信模块

多线程的串口通信可以避免主线程卡顿,这里我们在 SerialWorker::run() 函数中实现串口的接收和发送操作。

void SerialWorker::run() {
    serialPort = new QSerialPort();

    // 设置串口参数
    serialPort->setPortName(portName);
    serialPort->setBaudRate(baudRate);
    serialPort->setDataBits(dataBits);
    serialPort->setStopBits(stopBits);
    serialPort->setParity(parity);

    if (!serialPort->open(QIODevice::ReadWrite)) {
        emit errors(0, serialPort->errorString());
        emit connected(0, false);
        delete serialPort;
        serialPort = nullptr;
        return;
    }

    emit connected(0, true);  // 连接成功,发送信号
    emit informations(0, tc("串口已打开"));

    // 事件循环,保证串口的读写操作不会阻塞主线程
    QEventLoop eventLoop;
    QTimer timer;
    timer.setInterval(10);  // 设置定时器的间隔为10ms
    connect(&timer, &QTimer::timeout, &eventLoop, &QEventLoop::quit);
    timer.start();

    while (running) {
        QByteArray data;
        {
            // 从队列中取出数据并发送
            QMutexLocker locker(&mutex);
            if (!sendQueue.isEmpty()) {
                data = sendQueue.dequeue();
            }
        }

        if (!data.isEmpty()) {
            serialPort->write(data);  // 向串口写入数据

            if (!serialPort->waitForBytesWritten()) {
                emit errors(0, serialPort->errorString());
            } else {
                emit informations(0, tc("数据已发送: %1").arg(QString::fromUtf8(data)));
            }
        }

        // 读取串口数据
        if (serialPort->waitForReadyRead(10)) {
            QByteArray receivedData = serialPort->readAll();
            emit dataReceived(receivedData);  // 发射信号,传递接收到的数据

            // 粘包数据处理
            if (receivedData.contains("#") && receivedData.contains("$")) {
                m_buf += receivedData;  // 将数据加入缓存
                for (auto val : parseData(m_buf)) {
                    emit dataLine(val);  // 发送提取出来的数值型数据
                }
            }
        }

        // 处理事件循环,避免阻塞信号槽机制
        eventLoop.exec();
    }

    serialPort->close();
    emit informations(0, tc("串口已关闭"));
    emit connected(0, false);

    delete serialPort;
    serialPort = nullptr;
}
关键点:
  • 事件循环:通过 QEventLoop 实现了一个持续运行的事件循环,让串口的读写操作可以实时进行。
  • 发送队列:通过 QMutexLocker 锁定互斥体,保证在多线程环境下操作安全。
  • 信号槽:使用信号 emit 向外部通知连接状态、接收到的数据和错误信息。

2. 粘包拆解模块

粘包问题在串口通信中很常见。我们通过正则表达式匹配接收到的数据,将粘包数据拆解出来。

QList<float> SerialWorker::parseData(QByteArray &data) {
    QList<float> buf;  // 存储提取出的浮点数数据
    QRegularExpression regex("#(-?\\d*\\.?\\d+?)\\$");  // 正则表达式,匹配 #number$ 格式
    QRegularExpressionMatchIterator it = regex.globalMatch(data);

    int lastMatchEnd = 0;
    while (it.hasNext()) {
        QRegularExpressionMatch match = it.next();
        QString numberStr = match.captured(1);  // 提取匹配的数值字符串
        bool ok;
        float number = numberStr.toFloat(&ok);  // 将字符串转换为浮点数
        if (ok) {
            buf.append(number);  // 将解析出的数字添加到列表中
        }
        lastMatchEnd = match.capturedEnd(0);  // 记录最后一个匹配的结束位置
    }

    // 移除已处理的部分,保留未处理的部分以便后续处理
    if (lastMatchEnd > 0) {
        data.remove(0, lastMatchEnd);
    }

    return buf;  // 返回提取出的浮点数列表
}

在这里插入图片描述

关键点:
  • 正则表达式:通过正则表达式 #(-?\\d*\\.?\\d+?)\\$ 匹配粘包中的数值数据,#$ 是包裹数值的标志,支持匹配正负浮点数。
  • 移除已处理数据:每次处理后,将已解析的数据从缓存中移除,未处理的数据保留在缓存中等待下次处理。

3. 波形绘制模块

接收到的数值数据会显示在一个波形图中,以下是 WaveformWidget 的实现。

void WaveformWidget::addDataPoint(float value) {
    if (dataPoints.size() >= maxDataPoints) {
        dataPoints.pop_front();  // 如果数据点过多,则移除最旧的数据点
    }
    dataPoints.push_back(value);  // 添加新的数据点
    totalPointsReceived++;  // 统计接收到的总点数
    calculateStatistics();  // 计算最大值、最小值和平均值
    update();  // 触发界面重绘
}

void WaveformWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);  // 开启抗锯齿,使波形更平滑

    drawAxes(painter);  // 绘制坐标轴
    drawGrid(painter);  // 绘制背景网格
    drawData(painter);  // 绘制波形数据
    drawStatistics(painter);  // 显示最大、最小和平均值
}
关键点:
  • 实时绘制:当新的数据点被添加时,调用 update() 触发重绘,显示最新的波形。
  • 平滑显示:通过 QPainter::setRenderHint(QPainter::Antialiasing) 开启抗锯齿,使波形显示更加平滑。

4. 配置文件管理模块

软件通过 config.ini 文件存储配置项,QSettings 用于管理配置的读写。

void MainWindow::createConfigFile(const QString &fileName, const QStringList &values) {
    QFile file(fileName);
    if (file.exists()) return;  // 如果文件已经存在,则跳过

    QSettings settings(fileName, QSettings::IniFormat);
    settings.setIniCodec(QTextCodec::codecForName("UTF-8"));  // 设置编码为UTF-8

    int groupIndex = 1;
    for (const QString &value : values) {
        QString groupName = QString("%1").arg(groupIndex);
        settings.beginGroup(groupName);  // 创建新的组
        settings.setValue(QString("%1").arg(groupIndex), value);  // 写入键值对
        settings.endGroup();
        groupIndex++;
    }
}

QStringList MainWindow::readConfigFile(const QString &fileName) {
    QStringList iniinfors;
    QSettings settings(fileName, QSettings::IniFormat);
    settings.setIniCodec(QTextCodec::codecForName("UTF-8"));

    QStringList groups = settings.childGroups();  // 读取所有组
    groups.sort();

    for (const QString &group : groups) {
        settings.beginGroup(group);
        QStringList keys = settings.childKeys();  // 读取所有键
        for (const QString &key : keys) {
            iniinfors << settings.value(key).toString();  // 获取键值
        }
        settings.endGroup();
    }
    return iniinfors;
}
关键点:
  • 配置文件自动生成:如果配置文件不存在,会自动生成一个 config.ini 文件,并写入初始值。
  • 配置文件读取:软件启动时,通过 readConfigFile() 函数读取配置项,并将其加载到界面上。

总结

通过上述代码与注释,软件实现了以下核心功能:

  • 多线程串口通信:避免主线程阻塞,通过事件循环确保数据收发的实时性。
  • 粘包数据拆解:使用正则表达式解析粘包数据,确保接收的数据是正确的。
  • 实时波形绘制:接收到的数据点会动态绘制在波形图中,提供可视化的反馈。
  • 配置文件管理:通过 QSettings 管理配置项,支持自动生成和读取配置文件 。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

极客晨风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值