QT串口动态实时显示大量数据波形曲线(一)========“串口设置与ui界面添加(灯与按钮)”

序言    

    项目背景:底层硬件每10ms按照通讯协议通过串口上传40个8位数据,上位机制作软件接收数据并实时绘图。

    项目参数:(1)每10ms传输40个8位数据;(2)每1s将接收数据按照通讯协议(分类:电压,电流和频率)绘图。

    软件编写软件:QT。

    初步估计难点:(1)QT串口接收函数readyRead()函数为不定时触发槽函数;(2)绘图个数太大,影响串口接收函数。

    文章注意点:(1)记录按照学习顺序;(2)适合初级学者学习;(3)程序逻辑有不严谨之处。

    作者思维:主攻基于单片机,DSP(TI公司6748),ARM(STM32F103,F407,H750)的逻辑编程。

    软件编写时间:学习QT+编写程序+验证,共消耗2周。

    以上为文章背景


第一部分:

    软件制作前后,初步弄清楚了“面向过程”和“面向对象”编程的底层区别。

    面向过程:程序从main函数第一行程序执行。程序前级影响后继变量,同时程序中包含中断问题,执行顺序执行的程序中插入中断程序。中断程序中变量的改变影响主循环中程序变量(全局变量和某些局部变量)。

    面向对象:举个栗子:按钮按下,立刻改变内部变量参数,改变的变量立刻影响其他程序中参数的使用,可以立刻改变其他部分逻辑执行。当然面对对象编程中也包含面向过程的编程。

    注意:只关注了底层的执行问题,至于new对象问题属于编程语言。仅仅使用QT,可以不关注语言差别。


第二部分:串口

    这部分直接通过程序说明:

    (1)串口号:

    QList<QSerialPortInfo> serialPortinfo = QSerialPortInfo::availablePorts();
    int count = serialPortinfo.count();
    for(int i = 0; i<count; i++)
    {
        ui->SPUPORT->addItem(serialPortinfo.at(i).portName());
    }

    程序说明:读取电脑外置接口中有多少串口,并将其显示到ui->SPUPORT中,其中ui->SPUPORT为ComboBox模块。读取之后获取串口序号字符串

    (2)波特率:

    依旧采用ComboBox模块,同时加入编辑组合框(双击即可加入,基本操作,余下不再介绍),填入基本波特率:9600,19200和115200。同样程序中获取波特率字符串

    停止位,校验位与其他和上述一致,不作介绍。

    (3)开始按钮:

    QT的精髓应该就在按钮的设置,信号和槽函数。点击和触发函数。这点与ARM中触发中断一样,只是这个是手动触发,更直观。触发之后也是顺序执行。

bool MainWindow::mGetPortinfoma()
{
    mPortname = ui->SPUPORT->currentText();
    mBaudrate = ui->SPUBAUD->currentText();

    mSerial.setPortName(mPortname);

    if(mBaudrate == "9600")
    {
        mSerial.setBaudRate(QSerialPort::Baud9600);
    }
    else if(mBaudrate == "19200")
    {
        mSerial.setBaudRate(QSerialPort::Baud19200);
    }
    else
    {
        mSerial.setBaudRate(QSerialPort::Baud115200);
    }
    return mSerial.open(QSerialPort::ReadWrite);
}

    此函数在“开始按钮”中执行。主要功能:(1)取串口号,(2)取波特率,(3)将串口号和波特率放入串口初始化函数(QT固定函数),(4)打开串口。需要注意点:从ComboBox模块中获得的参数都是字符串。然后根据获取的字符串进行判断。

    注意:QT精髓点在于以下“开始按钮”的槽函数。按钮的一部分函数相当于单片机,ARM或者DSP中的参数初始化。另一部分函数相当于程序开始程序。这点感觉就是面向对象程序的好处,感觉很好。

void MainWindow::on_BEGINBUTTON_clicked()
{
    gnBeginFlag = !gnBeginFlag;

    if(gnBeginFlag == 0)
    {
        ui->BEGINBUTTON->setText("开始");
        mSerial.close();
        ui->SPUPORT->setEnabled(true);
        ui->SPUBAUD->setEnabled(true);
    }
    else
    {
        ui->BEGINBUTTON->setText("停止");

        mGetPortinfoma();
        ui->SPUPORT->setEnabled(false);
        ui->SPUBAUD->setEnabled(false);
    }
}

    其基本逻辑:(1)设置自身显示字符:开始和停止;(2)改变标志位:gnBeginFlag,以便在其他程序中作判断使用;(3)打开和关闭串口,使能和禁止参数设置。

    (4)槽函数定义:

connect(&mSerial,SIGNAL(readyRead()),this,SLOT(SerialPort_Readyread()));

    函数放在程序开始,链接串口接收数据,接收数据后的触发函数。

    (5)槽函数(串口有数据后的中断函数):

void MainWindow::SerialPort_Readyread()
{
    if(gnBeginFlag == true)                                       //按钮按下
    {

        if(mSerial.bytesAvailable()>=40)                          //项目要求40个8位数据
        { 
            LEDControl();                                         //有数据时指示灯控制函数
            QByteArray recvData = mSerial.readAll();              //取串口数据(固定函数)

            for(int i=0;i<40;i++)
            {
                gnReceiveBuffer[i] = gnReceiveBuffer[i+40];
            }
            for(int j=40;j<80;j++)
            {
                gnReceiveBuffer[j] = recvData.at(j-40);
            }                                                      //2包,80个数据
            for(gnDataScanCnt=0;gnDataScanCnt<77;gnDataScanCnt++)  //遍历80个数据
            {
                if((gnReceiveBuffer[gnDataScanCnt] == 128)
                 &&(gnReceiveBuffer[gnDataScanCnt+1] == 0)
                 &&(gnReceiveBuffer[gnDataScanCnt+2] == 127)
                 &&(gnReceiveBuffer[gnDataScanCnt+3] == 255))      //判断数据报头
                {
                    gnDataHeadBegin = gnDataHeadEnd;
                    gnDataHeadEnd = gnDataScanCnt;
                    gnDataHeadCnt ++;                              //检测到报头个数
                }
            }
            gnDataHeadNum = gnDataHeadCnt;
            gnDataHeadCnt = 0;

            if(gnDataHeadNum ==2)                                  //2包数据,必须有2个报头
            {
                if((gnDataHeadEnd - gnDataHeadBegin) == 40)        //报头中间数据必须为40个
                {
                    for(int k=gnDataHeadBegin;k<gnDataHeadEnd;k++)
                    {
                        gnReceive[k-gnDataHeadBegin] = gnReceiveBuffer[k];
                    }                       //将完成的一帧放入gnReceive数组中

                    DATAExplain();                                 //数据处理函数
                }
                else                                    //以下为数据灯的控制
                {
                    ui->DATALED->setFixedSize(20,20);
                    ui->DATALED->setStyleSheet("QPushButton{border-style:solid;"
                                              "            border-width:1px;"
                                              "            border-color:black;"
                                              "            border-radius:10px;"
                                              "            background-color:red}");
                }
            }
            else
            {
                ui->DATALED->setFixedSize(20,20);
                ui->DATALED->setStyleSheet("QPushButton{border-style:solid;"
                                          "            border-width:1px;"
                                          "            border-color:black;"
                                          "            border-radius:10px;"
                                          "            background-color:red}");
            }
        }
    }
}

    函数作用:串口接收到不等数据后,触发此函数。(疑惑:不知道接收多少会触发,这个才是整个程序中最不定的因素,找了网上很多说明,都没说明白这点)。当接收的数据大于40时(正好一帧数据),开始处理接收到的数据,将mSerial.readAll()中数据放入定义的数组中。因为不知道具体什么时候开始串口接收,也不确定何时出发此函数,所以必须将2包数据缓存,并且下位机发送的数据中必须有报头和报尾(可以不要),接收到2帧后放入开辟的数组gnReceiveBuffer中,一共80个8位数据。然后遍历所有数据找出报头(下位机报头为80007FFF,没办法,报头就是这样定的),2帧数据如果都正确的话必定有2个报头,将第一个报头和后面的数据取出,放入gnReceive数组中,就可处理完整的一帧数据了。

    数据组织简单形式介绍:80007FFF+数据。如果是完整2帧的话,应该是80007FFF+第1帧数据+80007FFF+第2帧数据。但是没有办法判断何时开始和何时结束,接收到的数据应该是下面的形式:前次数据+80007FFF+第1帧数据+80007FFF+第2帧部分数据。然后下次数据的形式(程序中有对应处理函数):第1帧部分数据+80007FFF+第2帧数据+80007FFF+部分第3帧数据。所以每次程序都能得到1帧完整数据,但是处理时间晚数据接收10ms(两帧数据间隔时间)。

    程序遗漏点:(1)数据包数据不完整,丢失报头或者报文,(2)接收到不只是40包数据,接收41包或者更多的时候,数据会丢失。程序中没有处理这两点,以后的绘图中也可以看出确实有的点消失了,一部分原因归咎于此点。

    程序中加入了数据灯控制,当数据产生错误时,数据灯会变红,这段程序应该是常规操作,和“开始按钮”的逻辑一样。

    (6)头文件函数

public:
    void LEDControl();
    void DATAExplain();
public:
//开始按钮
    bool gnBeginFlag;

//串口定义变量
    bool mGetPortinfoma();
    QSerialPort mSerial;
    QString mPortname;
    QString mBaudrate;

    uint8_t gnReceiveBuffer[80];
    uint16_t gnDataScanCnt;
    uint8_t gnDataHeadCnt;
    uint8_t gnDataHeadBegin;
    uint8_t gnDataHeadEnd;
    uint8_t gnDataHeadNum;

    uint8_t gnReceive[50];


public slots:
    void SerialPort_Readyread();
private slots:
    void on_BEGINBUTTON_clicked();

    里面包含所有的定义参数。

    (7)运行灯控制函数

void MainWindow::LEDControl()
{
    gnRunLedCnt++;
    if(gnRunLedCnt>1)
    {
        gnRunLedCnt = 0;
    }
    if(gnRunLedCnt > 0)
    {
        ui->RUNLED->setStyleSheet("QPushButton{border-style:solid;"
                                  "            border-width:1px;"
                                  "            border-color:black;"
                                  "            border-radius:10px;"
                                  "            background-color:green}");
    }
    else
    {
        ui->RUNLED->setStyleSheet("QPushButton{border-style:solid;"
                                  "            border-width:1px;"
                                  "            border-color:black;"
                                  "            border-radius:10px;"
                                  "            background-color:white}");
    }

}

    计数器0和1跳变,改变LED灯颜色的变化:绿和白变化。其实这个灯是按钮。只要设置弧度和大小就能把矩形的按钮变成圆形的按钮。同时改变其背景颜色,就可以当做一个LED灯去使用了。

第三部分:UI界面的设计与显示

                                    图1                                                                     图2                                                                 图3 

    图1:ui设计界面,其中运行灯为按钮;图2:运行界面时,灯为黄色,串口与波特率可选;图3:点击开始按钮后,其显示变为停止,灯闪烁,串口号与波特率不可选。

第四部分:总结

    (1)能用ui界面就用ui界面,代码写着还是麻烦。有开发人员强调直接写代码牛b,我不这样认为。只要能攒出来,做出需要的逻辑和效果,哪种方式都是可以的,都是很牛b的。

    (2)能借鉴代码就借鉴代码,可以提高效率。能不自己写的函数,能直接调用库函数,或者别人封装好的库函数,就用已经弄好的库函数。不否认自己写一遍可以提高,但是项目那么紧,先把东西搞出来,然后自己留时间再去一点一点自己写之前想自己写的代码,毕竟手中有粮,心中不慌。

   (3)程序需要一个星期,得报两个星期的时间吧。不是偷懒,而是(1)你需要调试,试着试着BUG就出来了,写的再快,还是有BUG的,留调试时间;(2)留自己学习时间,这个就不说了,真是想休息也可以的。

    注:由于小伙伴需要源代码的时间不同,登录邮箱界面太多麻烦,所以建立了一个订阅号,如果有问题或者需要源码,可添加订阅号,留言后会发送源代码或者有任何问题可留言,将积极解决提出的问题。

  • 19
    点赞
  • 122
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 在进行QT串口数据动态曲线显示时,我们首先要借助QT串口通信模块获取来自串口设备的数据。在此基础上,我们需要创建一个QT图形界面程序,来实现曲线动态显示。 具体而言,我们可以通过QT中的QCustomPlot等图形库来实现动态曲线显示,在动态显示曲线的同时,我们也需要处理来自串口设备的数据流,将其转化为相应的曲线显示。 在实现QT串口数据动态曲线显示过程中,我们还需要考虑诸如数据采集、数据存储和数据处理等问题,以使得此程序更加稳定和实用。 总之,在QT串口数据动态曲线显示中,需要借助QT串口通信模块和相应的图形显示库,同时需要加强对数据采集和处理等问题的考虑,来实现高质量的动态曲线显示功能。 ### 回答2: Qt是一种流行的跨平台GUI开发框架,它提供了许多用于数据可视化的工具。在这篇文章中,我们将讨论如何使用Qt来实现串口数据动态曲线显示。 首先,我们需要使用Qt串口类来打开串口。通过指定串口的名称、波特率、数据位数、校验位和停止位等参数,我们可以创建一个可以读取或写入串口数据的对象。 一旦这个串口对象被创建,我们就可以通过Qt的信号槽机制来处理接收到的串口数据。我们可以连接一个槽函数到读取串口数据的信号上,每当串口收到数据时,该槽函数就会自动被调用,并将串口数据存储到一个缓冲区中。 接下来,我们需要使用Qt的绘图类来将串口数据转换成一条曲线。通过创建一个QPainter对象,我们可以在一个QWidget窗口上实现实时数据曲线动态显示。我们可以使用QPainter的绘制函数,如drawLine、drawPoint或drawPath,来把每个新的数据添加曲线中。 最后,我们需要使用Qt的定时器类来控制实时数据曲线的更新速率。通过使用QTimer类的start和stop函数,我们可以启动和停止一个定时器对象,以定期调用更新显示函数,这样就可以实现实时数据曲线动态显示。 综上所述,使用Qt实现串口数据动态曲线显示并不复杂。我们只需要利用Qt串口、信号槽、绘图和定时器等类,就可以轻松地实现一个可靠的功能强大的实时数据曲线显示系统。 ### 回答3: 在Qt中进行串口数据动态曲线显示,可以利用Qt Charts模块以及Qt SerialPort模块。首先需要打开串口,读取串口数据并解析,将解析后的数据作为动态曲线的横纵坐标,并将数据实时显示曲线上。 具体步骤如下: 1. 引入Qt Charts和Qt SerialPort模块,包括头文件和库文件。 2. 在UI界面添加一个动态曲线控件,设置相关属性,如横纵坐标范围、坐标轴标签等。 3. 打开串口设置串口参数,如波特率、数据位、停止位等。 4. 读取串口数据,使用Qt的QSerialPort类中的read()函数读取数据,读取后进行解析。 5. 将解析后的数据作为动态曲线的横纵坐标,使用Qt Charts中的QChart类和QLineSeries类,将坐标点添加曲线中,并通过曲线的update()函数进行实时更新。 6. 将实时更新的曲线显示UI界面中,使用Qt的QChartView类将曲线添加到UI中的曲线控件中。 7. 完成后需要在程序中添加相应的错误处理和异常处理代码。当串口连接错误或数据解析错误时,应该及时给出提示信息。 以上是Qt串口数据动态曲线显示的基本步骤,根据实际需求可以进行一些扩展和优化。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值