实现波形显示的QT串口数据接收程序

**使用QT在串口调试助手基础上实现波形显示

一、前言

背景:使用ADS1255对模拟信号进行采样,并将转换的数据通过串口发送给电脑,使用QT编写上位机软件接收串口数据并实现采样波形的显示。因为没有具体的需求,只是进行简单测试,程序不尽完善,简单记录一下过程,方便刚接触的同伴一起学习。

二、测试效果

界面是在串口助手基础上改的,具有串口调试助手的基本功能,加了一个折线图显示,但是重新整理了上次串口的程序,显示效果如下:

20230302_104834

采样的板子设计的不太好,模拟输入端开路时本底噪声基本在0.6mV左右,设计输入电压是±2.5V。采用的串口输入,波特率为1500000,输入的数据具有固定的格式,数据输入形式如下:


三、实现过程遇到的问题

使用QT的serialport 和 charts库,简单过程不再说明,源程序在文末给出,下面简述一下我在实现中遇到的问题:

1.串口的定时扫描和串口名更新

原本只在程序开始时进行串口扫描,但随后发现如果设备在程序运行后就检测不到串口,串口如果被占用也得不到更新。通过定时器和关联槽函数来定时(500ms)扫描串口,但是串口禁用那行代码还没整明白是怎么回事(有知道的欢迎在评论区告诉我),具体实现看源代码。

for(int i = 0;i<portStringList.size();i++)
{
    serial->setPortName(portStringList[i]);
    if(serial->open(QIODevice::ReadWrite))
        ui->comboSerialPort->addItem(portStringList.at(i));
    else
    {
        ui->comboSerialPort->addItem(portStringList.at(i) + "(不可用)");
        ui->comboSerialPort->setItemData(i,(QVariant)0,Qt::UserRole-1);     //串口禁用??
    }
    serial->close();
}

2.图表显示的内容需要移动,类似示波器的显示波形

对于横坐标,我是通过固定横坐标时间的宽度,改变横坐标的坐标范围来实现的,比如数据输出速率为10,固定横坐标只显示50个点,则设置横坐标宽度为5。

    t += 0.1;
    qreal value = valueStr.toDouble();
    serices0->append(t,value);
    if(t>50)
        axisX->setRange(t-50,t);

对于纵坐标,通过一个链表把图表显示的50个数据存储起来,再用类似队列的方式先入先出的方式更新队列,找出队列的最大值和最小值更新纵坐标的坐标范围

    if(listvalue.size()<=500)
        listvalue.push_front(value);
    else
    {
        listvalue.pop_back();
        listvalue.push_front(value);
    }
    qreal minvalue = *std::min_element(listvalue.begin(),listvalue.end());
    qreal maxvalue = *std::max_element(listvalue.begin(),listvalue.end());
    axisY->setRange(minvalue-0.00001,maxvalue+0.00001);

3.把数据从发送的字符串中截取出来显示

没想到太好的办法,目前是需要注意发送字符串的形式,按着字符串形式更改程序,比如下位机发送的类型是value:0.0000000V,可以利用字符串截取函数把中间的数据单独拿出来,因为我下位机发送的数据宽度固定,所以我是使用mid()函数直接截取,如果长度 不一致,也可以用split()函数将数据与文本割裂开。

    receiveBuff = serial->readAll();
    receiveBytes += receiveBuff.length();
    QByteArray valueStr;
    valueStr = receiveBuff.mid(QString("value:").size(),QString("-0.000000").size());

4.设置坐标轴的问题

给图表中序列赋坐标轴的时候常用到这样一段代码

chart->setAxisX(axisX,serices0);
chart->setAxisY(axisY,serices0);

但一般都会警告该代码已过时,推荐使用addAxis()函数替代,于是改成

chart->addAxis((QAbstractAxis*)axisX,Qt::AlignBottom);
chart->addAxis((QAbstractAxis*)axisY,Qt::AlignLeft);

黄色警告消失了,编译也没有错误,但是运行起来坐标轴有些许问题,还没明白怎么回事。

四、程序代码

1.pro项目文件,主要是添加两行核心库和资源文件

QT       += core gui
QT       += serialport
QT       += charts

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

RESOURCES += \
    res.qrc

2.ui设计文件,按照自己需求布局,给控件命名

3.h文件,一些变量和函数声明

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtCharts>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QStringList>
#include <QMessageBox>
#include <QFileDialog>
#include <QList>

using namespace QtCharts;

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;

private:
    qreal t;
    QChart *chart;
    QLineSeries *serices0;
    QValueAxis *axisX,*axisY;
    QSerialPort *serial;                        //串口端口
    QStringList portStringList;                 //端口链表
    QTimer *timer;                              //定时器
    QByteArray sendBuff,receiveBuff;            //发送、接收缓存区
    long int sendBytes,receiveBytes;            //发送、接收字节数
    QList <qreal>  listvalue;

    void InitSerialPort();
    void InitChart();

private slots:
    void serialPort_readyRead();
    void portTimerEvent();

    void on_btnOpenSerial_clicked();
    void on_btnSend_clicked();
    void on_btnClearRevBuff_clicked();
    void on_btnSaveFile_clicked();
    void on_btnOpenFile_clicked();
    void on_btnClearSendBuff_clicked();
    void on_btnResetCount_clicked();
    void on_chkFixedSend_clicked();
    void on_lineEditTime_editingFinished();
    void on_textEditRev_textChanged();
};
#endif // MAINWINDOW_H

4.c文件,函数功能实现

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    t = 0;
    sendBytes = 0;
    receiveBytes = 0;

    chart = new QChart();
    serices0 = new QLineSeries();
    axisX = new QValueAxis();
    axisY = new QValueAxis();
    timer = new QTimer();
    serial = new QSerialPort(this);
    QTimer *portTimer = new QTimer(this);
    connect(portTimer,SIGNAL(timeout()),this,SLOT(portTimerEvent()));
    connect(serial,SIGNAL(readyRead()),this,SLOT(serialPort_readyRead()));

    InitSerialPort();
    InitChart();

    portTimer->start(500);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::InitSerialPort()
{
    ui->comboSerialPort->clear();
    portStringList.clear();
    foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
        portStringList += info.portName();
    for(int i = 0;i<portStringList.size();i++)
    {
        serial->setPortName(portStringList[i]);
        if(serial->open(QIODevice::ReadWrite))
            ui->comboSerialPort->addItem(portStringList.at(i));
        else
        {
            ui->comboSerialPort->addItem(portStringList.at(i) + "(不可用)");
            ui->comboSerialPort->setItemData(i,(QVariant)0,Qt::UserRole-1);     //串口禁用??
        }
        serial->close();
    }
    ui->comboBaudRate->setCurrentIndex(5);
    ui->comboDataBits->setCurrentIndex(3);
    ui->comboParity->setCurrentIndex(2);
    ui->comboStop->setCurrentIndex(0);

    ui->btnSend->setEnabled(false);
    ui->chkFixedSend->setEnabled(false);
    ui->lineEditTime->setEnabled(false);

    ui->lineEditTime->setText("1000");
    ui->radioTextReceive->setChecked(Qt::Checked);
    ui->radioTextSend->setChecked(Qt::Checked);
}

void MainWindow::InitChart()
{
    ui->chartView->setChart(chart);
    QMargins mgs(5,5,5,5);
    chart->setMargins(mgs);
    chart->setTitle("数据曲线");

    //创建折线序列
    serices0->setName("时间-电压曲线");
    chart->addSeries(serices0);

    //创建坐标轴
    axisX->setRange(0,5);
    axisX->setTitleText("time(secs)");

    axisY->setRange(-2,2);
    axisY->setTitleText("value");

    chart->setAxisX(axisX,serices0);
    chart->setAxisY(axisY,serices0);

   //chart->addAxis((QAbstractAxis*)axisX,Qt::AlignBottom);
   //chart->addAxis((QAbstractAxis*)axisY,Qt::AlignLeft);
}

void MainWindow::serialPort_readyRead()
{
    QByteArray lastStr;
    if(!ui->radioStopReceive->isChecked())
    {
        lastStr = ui->textEditRev->toPlainText().toUtf8();
        receiveBuff = serial->readAll();
        receiveBytes += receiveBuff.length();

        QByteArray valueStr;
        valueStr = receiveBuff.mid(QString("value:").size(),QString("-0.000000").size());

        t += 0.1;
        qreal value = valueStr.toDouble();
        serices0->append(t,value);

        if(t>50)
            axisX->setRange(t-50,t);

        if(listvalue.size()<=500)
            listvalue.push_front(value);
        else
        {
            listvalue.pop_back();
            listvalue.push_front(value);
        }
        qreal minvalue = *std::min_element(listvalue.begin(),listvalue.end());
        qreal maxvalue = *std::max_element(listvalue.begin(),listvalue.end());
        axisY->setRange(minvalue-0.00001,maxvalue+0.00001);


        ui->labRevBytesCount->setText(QString::number(receiveBytes));

        if(ui->radioHexReceive->isChecked())
        {
            receiveBuff = receiveBuff.toHex().toUpper();
            int length = receiveBuff.length();
            for(int i = 0;i<=length/2;i++)
                receiveBuff.insert((2+3*i), QByteArray(" "));
        }
        lastStr = lastStr.append(receiveBuff);
        ui->textEditRev->setText(lastStr);
    }
    else
        serial->clear(QSerialPort::Input);
}

void MainWindow::portTimerEvent()
{
    QStringList newPortStringList;
    newPortStringList.clear();
    foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
        newPortStringList += info.portName();
    if(newPortStringList.size() != portStringList.size())
    {
        portStringList = newPortStringList;
        ui->comboSerialPort->clear();
        ui->comboSerialPort->addItems(portStringList);
    }
}


void MainWindow::on_btnOpenSerial_clicked()
{
    if(ui->btnOpenSerial->text() == QString("打开串口"))
    {
        //串口设置
        serial->setPortName(ui->comboSerialPort->currentText());
        serial->setBaudRate(ui->comboBaudRate->currentText().toInt());
        switch(ui->comboDataBits->currentText().toInt())
        {
        case 5: serial->setDataBits(QSerialPort::Data5);break;
        case 6: serial->setDataBits(QSerialPort::Data6);break;
        case 7: serial->setDataBits(QSerialPort::Data7);break;
        case 8: serial->setDataBits(QSerialPort::Data8);break;
        default: serial->setDataBits(QSerialPort::UnknownDataBits);
        }
        switch(ui->comboParity->currentIndex())
        {
        case 0: serial->setParity(QSerialPort::EvenParity);break;
        case 1: serial->setParity(QSerialPort::MarkParity);break;
        case 2: serial->setParity(QSerialPort::NoParity);break;
        case 3: serial->setParity(QSerialPort::OddParity);break;
        default: serial->setParity(QSerialPort::UnknownParity);
        }
        switch (ui->comboStop->currentIndex())
        {
        case 0: serial->setStopBits(QSerialPort::OneStop);break;
        case 1: serial->setStopBits(QSerialPort::OneAndHalfStop);break;
        case 2: serial->setStopBits(QSerialPort::TwoStop);break;
        default: serial->setStopBits(QSerialPort::UnknownStopBits);
        }

        serial->setFlowControl(QSerialPort::NoFlowControl);

        if(!serial->open(QIODevice::ReadWrite))
        {
            QMessageBox::warning(this,"提示","无法打开串口",QMessageBox::Ok);
            return;
        }

        ui->comboSerialPort->setEnabled(false);
        ui->comboBaudRate->setEnabled(false);
        ui->comboDataBits->setEnabled(false);
        ui->comboParity->setEnabled(false);
        ui->comboStop->setEnabled(false);

        ui->btnSend->setEnabled(true);
        ui->chkFixedSend->setEnabled(true);
        ui->lineEditTime->setEnabled(true);
        ui->btnOpenSerial->setText("关闭串口");
    }
    else
    {
        serial->close();

        ui->comboSerialPort->setEnabled(true);
        ui->comboBaudRate->setEnabled(true);
        ui->comboDataBits->setEnabled(true);
        ui->comboParity->setEnabled(true);
        ui->comboStop->setEnabled(true);

        ui->btnSend->setEnabled(false);
        ui->chkFixedSend->setEnabled(false);
        ui->lineEditTime->setEnabled(false);
        ui->btnOpenSerial->setText("打开串口");
    }
}


void MainWindow::on_btnSend_clicked()
{
    sendBuff = ui->textEditSend->toPlainText().toUtf8();
    if(ui->radioHexSend->isChecked())
        sendBuff = QByteArray::fromHex(sendBuff);
    if(ui->chkLineFeed->isChecked())
        sendBuff += '\n';
    serial->write(sendBuff);
    sendBytes += sendBuff.length();
    ui->labSendBytesCount->setText(QString::number(sendBytes));
    ui->textEditSend->moveCursor(QTextCursor::End);
}


void MainWindow::on_btnClearRevBuff_clicked()
{
    ui->textEditRev->clear();
}


void MainWindow::on_btnSaveFile_clicked()
{
    QString curPath = QDir::currentPath();
    QString dlgTilte = "保存文件";
    QString filter = "文本文件(*.txt);;所有文件(*.*)";
    QString fileName = QFileDialog::getSaveFileName(this,dlgTilte,curPath,filter);
    if(fileName.isEmpty())
        return;
    QFile file(fileName);
    if(!file.open(QIODevice::ReadWrite | QIODevice::Text))
        QMessageBox::warning(this,"文档编辑器",tr("无法写入文件 %1:\n%2").arg(fileName,file.errorString()));
    QTextStream stream(&file);
    stream.setAutoDetectUnicode(true);
    stream<<ui->textEditRev->toPlainText().toUtf8();
    file.close();
}


void MainWindow::on_btnOpenFile_clicked()
{
    QString curPath = QDir::currentPath();
    QString dlgTilte = "打开文件";
    QString filter = "文本文件(*.txt);;所有文件(*.*)";
    QString fileName = QFileDialog::getOpenFileName(this,dlgTilte,curPath,filter);
    if(fileName.isEmpty())
        return;
    QFile file(fileName);
    if(!file.open(QIODevice::ReadWrite | QIODevice::Text))
        QMessageBox::warning(this,"文档编辑器",tr("无法读取文件 %1:\n%2").arg(fileName,file.errorString()));
    ui->textEditSend->setText(file.readAll());
    file.close();
}


void MainWindow::on_btnClearSendBuff_clicked()
{
    ui->textEditSend->clear();
}


void MainWindow::on_btnResetCount_clicked()
{
    receiveBytes = 0;
    sendBytes = 0;
    ui->labRevBytesCount->setText(QString::number(receiveBytes));
    ui->labSendBytesCount->setText(QString::number(sendBytes));
}


void MainWindow::on_chkFixedSend_clicked()
{
    if(ui->chkFixedSend->isChecked())
    {
        int fixedTime = ui->lineEditTime->text().toInt();
        timer->start(fixedTime);
        connect(timer,SIGNAL(timeout()),this,SLOT(on_btnSend_clicked()));
    }
    else
    {
        timer->stop();
    }
}


void MainWindow::on_lineEditTime_editingFinished()
{
    on_chkFixedSend_clicked();
}


void MainWindow::on_textEditRev_textChanged()
{
    ui->textEditRev->moveCursor(QTextCursor::End);
}

程序基本框架和上一篇发文的基于QT5实现串口调试助手没太大区别,只是修改了一下控件的命名便于理解,把上次冗余的部分代码变简洁一下,加入了图表。程序需要自行理解修改一下才能运行,否则下位机发来的数据与此次字符串格式不一致会使程序发生错误或强制退出。刚入门还存在诸多问题,请各位见谅。

物联沃分享整理
物联沃-IOTWORD物联网 » 实现波形显示的QT串口数据接收程序

发表评论