本章节将会从建立一个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: 编辑界面
串口的基础设置项有:串口设备名、串口速率(波特率)、数据位、停止位、校验位、DTS、RTS等数据,本文所用示波器的数据位、停止位等数据均固定不变,故串口仅需要设置串口设备名、串口速度两个信息。
由于串口速度一般采取固定的几种数值,串口设备也多为几个特定可选项,故采用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
评论(0)
您还未登录,请登录后发表或查看评论