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 serialPortinfo = QSerialPortInfo::availablePorts();
    int count = serialPortinfo.count();
    for(int i = 0; iSPUPORT->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;kDATALED->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界面的设计与显示

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

                                    图1                                                                     图2                                                                 图3 

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

第四部分:总结

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

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

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

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

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

你可能感兴趣的:(qt,串口通信,c++,图像处理,面向对象编程)