本章节将会从建立一个Qt工程开始,一步步演示如何通过代码实现QtSerial模块与STM32单片机通信,之后将数据报解包并通过QtCharts模块显示在屏幕上。此外,本章节还将简单介绍如何通过串口向示波器发送指令,利用上位机改变示波器运行状态。

 

Part 1:工程建立与基础配置

本节将会介绍如何建立一个Qt工程,并介绍如何添加Qt所需资源,对Qt工程配置文件做一个简单介绍。

Step 1: 建立工程

  • 点击菜单栏“File->New File or Project”,打开新建窗口
  • 选择“Application”窗口下的“Qt Widget Application”模板,建立一个空白的图形化窗口工程。

  • 点击“Choose” 后,会提示输入工程名和存储位置,储存位置尽量选择全英文无空格目录,避免出现奇怪问题。

Tips:虽然Qt会为用户创建一个工程名命名的空白文件夹,但是Qt会在同级目录下产生编译文件,故建议用户为Qt工程建立独立的开发目录,不要直接放在桌面上。

  • 点击“Next”,进入主界面类配置,此页面决定了生成的界面模板中主界面对应C++类的名称,可以保持类名默认。

  • 点击“Next”后,其余选项保持默认,即可自动生成一个空白界面工程。各文件作用如下:

 *.pro       Qt配置文件

Header   C++头文件文件夹

        mainwindow.h      主界面对应类的头文件

Sources  C++源代码文件夹

        main.c                  整个程序的程序入口文件

        mainwindow.c      主界面对应类的源代码

Forms     存放界面文件

        Mainwindow.ui     主界面对应的界面配置文件(代码)

  • 点击界面左下角的绿色运行三角(无debug标志),即可编译生成第一个空白界面工程。

Step 2: 配置工程所需资源

打开*.pro文件,可以看到自动生成的Qt配置文件,各部分功能如下图所示:

在对应代码段之后加入下列语句(加号“+”不可省略),为Qt引入今后开发所需的各个组件。

QT       += charts
QT       += serialport
QT       += network

 Tips:如果需要引入外部组件/库,可以使用下列形式添加:

LIBS  += -lwiringPi
LIBS  += -LC:/QtCv_Libs/install/x64/mingw/lib/ -llibopencv_world460.dll

 Part 2:串口基础功能开发

本小节将主要介绍串口的基础操作:搜索、打开、读取三步操作,写入操作将从后文控制单片机角度进行介绍。

串口等设备在Linux中主要映射为文件的形式,我们所用的USB串口模块,一般会被映射为

/dev/ttyUSBx

如果需要手动枚举串口设备,可以在终端中输入:

ls /dev/ttyUSB*

Step 1: 编辑界面

串口的基础设置项有:串口设备名、串口速率(波特率)、数据位、停止位、校验位、DTSRTS等数据,本文所用示波器的数据位、停止位等数据均固定不变,故串口仅需要设置串口设备名、串口速度两个信息。

由于串口速度一般采取固定的几种数值,串口设备也多为几个特定可选项,故采用ComboBox(下拉框)实现串口设备和速度选择。

具体操作步骤如下:

  • 从左侧拖拽“Combo Box”,进入右侧编辑器,选好位置后释放:

  • 双击下拉框或右键选择“Edit Items

  • 点击左下角的“+”可以添加速率选项,可以添加如下常用波特率:

1200/2400/4800/9600/14400/19200/38400/57600/76800/115200/12800/230400/25600/460800/921600

  • 按照下图排布一个串口界面,并按照右侧名称更改控件名:

Step 2:编辑搜索串口槽函数

Qt中界面操作,例如点击、鼠标移动等操作,都是通过“信号-槽”机制实现,信号-槽与MFC编程中的消息-消息处理函数相似,简单来说“信号”是某种事件的触发信号,而“槽”是触发这个信号后,所对应进入的处理函数(回调函数)。

我们首先为搜索按钮添加槽函数,具体步骤如下:

  • 右键按钮,选择“Go to Slot”选项

  • 在打开的窗口中选择“click()”类型的槽函数(点击按钮触发的槽函数)

  • 此时Qt会跳转至c代码编辑页面,在刚刚添加的槽函数中加入以下代码:
void MainWindow::on_pushButtonSearch_clicked()
{
    ui->comboBoxPort->clear();
    auto Infos=QSerialPortInfo::availablePorts();
    QList<QSerialPortInfo>::iterator info=Infos.begin();
    for(;info!=Infos.end();info++)
    {
        ui->comboBoxPort->addItem(info->portName());
    }
}

此代码的主要作用是:

        清空界面上的串口下拉框

        调用函数枚举可用串口

        遍历串口列表(QList),将串口名更新到界面上的串口下拉框

  • 此时会出现诸多变量类型未定义,需要在h中添加QtSerialPort相关头文件
#include <QDebug> 
#include <QMessageBox>
#include "QtSerialPort/QSerialPort"
#include "QtSerialPort/QSerialPortInfo"
  • 运行验证,点击搜索后可以看到,最上方的串口选择下拉框中出现了几个可选的串口。

Step 2:编辑打开串口槽函数

搜索到串口后需要打开串口,使用完毕后我们需要关闭串口,为了界面简洁,打开和关闭串口操作我们将之封装进同一个按钮操作。

具体代码功能如下步骤添加

  • h中的类私有变量列表中添加:
QSerialPort *myserial;
  • c中的构造函数中添加:
myserial = new QSerialPort();
  • 修改c中的在构析函数:
MainWindow::~MainWindow()
{
    delete ui;
    if(myserial)
    {
        if(myserial->isOpen()) myserial->close();
        delete myserial;
    }
}
  • 为“Open”按钮添加槽函数,并修改为如下内容
void MainWindow::on_pushButtonOpen_clicked()
{
    if(!myserial->isOpen())
    {
        myserial->setPortName(ui->comboBoxPort->currentText()); //Set PortName
        myserial->setBaudRate(ui->comboBoxSpeed->currentText().toInt()); //Set Port Speed

        QSerialPort::DataBits DataBitMap[4]={QSerialPort::Data5,QSerialPort::Data6,QSerialPort::Data7,QSerialPort::Data8};
        QSerialPort::Parity parityMap[5]={QSerialPort::NoParity, QSerialPort::EvenParity, QSerialPort::OddParity, QSerialPort::SpaceParity, QSerialPort::MarkParity};
        QSerialPort::StopBits stopbMap[3]={QSerialPort::OneStop, QSerialPort::OneAndHalfStop, QSerialPort::TwoStop};
        QSerialPort::FlowControl flowctlMap[3]={QSerialPort::NoFlowControl, QSerialPort::SoftwareControl, QSerialPort::HardwareControl};

        //Set Port Setting
        myserial->setDataBits(DataBitMap[3]);
        myserial->setParity(parityMap[0]);
        myserial->setStopBits(stopbMap[0]);
        myserial->setFlowControl(flowctlMap[0]);

        if(myserial->open(QIODevice::ReadWrite))
        {
            qDebug()<<"Opened serial port "+ui->comboBoxPort->currentText()+".";
            ui->comboBoxPort->setDisabled(true);
            ui->comboBoxSpeed->setDisabled(true);
            ui->pushButtonSearch->setDisabled(true);
            ui->pushButtonOpen->setText("Close");
        }
        else
        {
            qDebug()<<"Open serial port "+ui->comboBoxPort->currentText()+" failed.";
            QMessageBox::warning(this,tr("Cannot Open Port!"),tr("Cannot open this serial port, it may be due to a device malfunction or another program is using this serial port."),QMessageBox::Close);
        }
    }
    else
    {
        qDebug()<<"Close serial port "+ui->comboBoxPort->currentText()+".";
        myserial->close();
        ui->comboBoxPort->setEnabled(true);
        ui->comboBoxSpeed->setEnabled(true);
        ui->pushButtonSearch->setEnabled(true);
        ui->pushButtonOpen->setText("Open");
    }
}

本段代码主要分为2部分,由“!myserial->isOpen()”判断当前串口是否打开,决定开启串口还是关闭串口。

如果当前串口并未开启,则执行:

        从界面获取串口名和串口速度

        根据界面数据和查找表设置波特率、校验值等信息

        尝试开启串口:

                如果开启成功则把界面上的控件设置为“禁用”,并将按钮文本改为“Close

                如果开启失败则弹出错误提示框。

如果当前串口已经开启,则执行:

        关闭当前串口

        把界面上的控件设置为“启用”,并将按钮文本改为“Open

 

Step 3:编辑串口收槽函数

串口数据接收存在两种方式:轮询、槽函数,前者需要CPU不断查询是否接收到串口数据,会增大CPU负担;为此我们选择采用槽函数的形式,每次接收到数据自动执行槽函数,减弱CPU负担,降低程序耦合度。

具体代码功能如下步骤添加:

  • h中的类私有槽函数声明列表中添加:
void on_SerialRecv(void);
  • c中添加槽函数实现:
void MainWindow::on_SerialRecv(void)
{
    if(myserial->isOpen())
    {
        QByteArray QBArecv =myserial->readAll();
        QString Recv="";
        if(1)
        {
            QString Temp;
            foreach(QChar dat,QBArecv)
                Recv+=Temp.sprintf("%02X ",(unsigned int)dat.unicode());
        }
        else Recv+=QString::fromLocal8Bit(QBArecv);
        qDebug()<<Recv;
    }
}

此段代码主要通过“myserial->readAll();”函数,在每一次收到数据时,将串口缓冲区中的数据全部读出,存储至QByteArray中。

为了调试方便,我们遍历QByteArray中的每一个元素,并将之格式化为十六进制形式,通过qDebug函数打印到QDebug缓冲区,效果如下图所示:

Part 3:分割串口数据报

从上文图片中可以看出,每次进入槽函数,系统收到的数据长度并非固定,也不能一次性收入STM32示波器一次测量的所有数据。因此,我们需要将不同时间段内的数据报汇总进同一个存储容器,并通过一些特定的符号标记实现数据报分割与恢复。

 一般情况下,一个数据报文至少要包头、包尾、校验数据等诸多标志信息,由于示波器刷新速度很快,单一一次传输失败也不会影响显示效果,故实际使用中我们仅用了一个包尾来分割数据段。

由于示波器测量波形一般为周期性信号,很少出现低频小信号和高频信号瞬时转变,故我们针对示波器数据特征设置了一个不容易被误触发的同步序列(包尾),具体为{0x53,0x59,0x4E,0x43,0x00,0xFF,0x00,0xFF},如下图所示:

修改串口接收槽函数代码,使之能够拆分串口数据报:

void MainWindow::on_SerialRecv(void)
{
    static QByteArray RecvBuff;
    if(myserial->isOpen())
    {
        QByteArray QBArecv =myserial->readAll();
        RecvBuff.append(QBArecv);
        char SyncArray[]={0x53,0x59,0x4E,0x43,0x00,0xFF,0x00,0xFF};
        QByteArray Sync=QByteArray(SyncArray,8);
        if(RecvBuff.indexOf(Sync)!=-1)
        {
            QByteArray Packed=RecvBuff.left(RecvBuff.indexOf(Sync)+8);
            RecvBuff=RecvBuff.mid(RecvBuff.indexOf(Sync)+8);

            QBArecv=Packed;
            QString Recv="";
            if(1)
            {
                QString Temp;
                foreach(QChar dat,QBArecv)
                    Recv+=Temp.sprintf("%02X ",(unsigned int)dat.unicode());
            }
            else Recv+=QString::fromLocal8Bit(QBArecv);
            qDebug()<<Recv;
            qDebug()<<"Lenth="<<Packed.length();
        }
    }
}

 最终呈现效果如下图所示:

可以看到,每次分割出的结果已经是完整的数据报文,而且可以自动计算出报文长度1008Bits

之后对报文内容进行解析,即可得到如下图所示示波器功能(图表实现方式将在下一节介绍):

Part 4:源码下载

本章节中提到的代码可以从如下链接下载

链接:https://pan.baidu.com/s/10RgnbvdIogN_kPJLKAz_0Q?pwd=Yuki 
提取码:Yuki