STM32遥控小车上位机部分
介绍本文实现的控制小车程序用于控制自制的遥控小车,控制方式为点击窗口中的按钮实现控制或者通过键盘的方向键来控制。为方便Qt初学者能从本文中学到自己想要的东西,我将一些重要的流程环节都以单独的**形式发布,初学者可从本文给予的链接跳转至自己想要的部分,同时每一部分我会尽量给出我的学习路程以及参考资料。
注:
我也是一个初学者,有一点C语言基础,对C++不是特别懂,所以**中有误的地方或者不明白的地方请各位大神指出,我会尽量及时回复。
关于Qt的教程,我参考的是:QT基础教程 | QT入门 | 信号与槽(Bilibili),只需看前4节就差不多能实现大部分的基础的窗口设计了。
本文的程序代码在这里,百度网盘(提取码:adm9 ),共两个文件夹,一个是可直接运行的程序,另一个是程序源码。
本文涉及的遥控小车硬件实现部分见此:STM32遥控小车下位机及硬件连接部分(CSDN)
一、界面效果
上图:
说明:
Qt将窗口程序设计分为UI设计和逻辑设计,窗口的UI设计只需要在Qt Creater中拖动各个控件到你想要的位置,然后使整个界面好看即可。此过程不涉及任何C++编程,但需要对Windows程序设计有一定的了解,明白自己的需求,才能设计出满意的界面。
我设计的界面分为几个部分,各部分分别实现什么功能,同时程序低层的核心部分为串口实现,其他的界面交互都是为了调用相关函数实现串口通信的。接下来几节将详细阐述具体实现过程及代码。
二、各部分的实现
1. 核心部分:串口实现
这个部分的实现主要靠调用Qt库里的串口类函数从而实现各种功能。如果想详细了解Qt的串口实现,可以转这里:Qt串口的实现(Bilibili),跟着这个老师做一遍就对串口的实现能大概了解了。
本文主要使用到的串口相关函数如下: //设置串口号,波特率,校验位等
setPortName()
setBaudRate()
setParity()
setDataBits()
setStopBits()
//打开串口以及读写串口
open()
write()
read()
2. 连接组
如图
说明:
该部分有4个控件,两个标签(Label),两个按钮(Button)。两个标签用来提示当前的串口连接状态,两个按钮用来控制连接和测试连接。
两个标签:标签1为一个静态的Label,程序运行的整个过程中该控件不发生任何改变。标签2为一个动态的Label,当连接成功时,该Label上的字显示为“已连接”,当未连接时,Label上的字显示为“未连接”。使用函数QLabel.setText()对Label上的文字进行更改。
两个按钮:按钮1为串口配置按钮(BtnSerialPortConfig),点击该按钮时,程序会打开一个新的窗口界面从而进行串口配置。按钮2为串口连接按钮(BtnConnectPort),点击该按钮时,程序根据配置文件对串口进行配置和启动,连接成功的话该按钮上的文字会变为断开,以及Label2上的文字会变为“已连接”,再次点击,程序会将串口断开,文字又会变回来。
a. 实现点击配置按钮时打开新的窗口
功能描述:点击配置按钮时程序作出响应,打开一个新的子窗口。具体原理为点击按钮时会发出信号,通过将该信号与一个自定义的函数绑定在一起,就能使信号触发时,函数就开始执行,这个函数就叫这个信号的槽函数。 代码解释:
编写槽函数之前需要先在当前工程下添加一个新的窗口类,即设计师界面类,为其命名为newWindow。
将newWindow的头文件加到主窗口头文件中,在槽函数中添加如下代码
void MainWindow::on_BtnSerialPortConfig_clicked()
{
newWindow *configWindow = new newWindow;
//setWindowModality:保证子窗口弹出时禁用主窗口
configWindow->setWindowModality(Qt::ApplicationModal);
configWindow->show();
}
说明:
QT中的信号与槽原理与计算机的中断原理类似,当某个特定信号发生后,程序就会调用并执行槽函数中的代码。
槽函数与信号直接的绑定关系有两种:一是通过connect函数,将信号与自定义的函数关联在一起;二是UI设计界面中,右击某个交互控件,选择转到槽,选择特定的动作,之后程序会将QT库中自带的空槽函数写入主窗口函数中,用户只需在此函数中补充自己想要实现的功能即可。 . 实现点击连接按钮时打开串口
功能描述:点击连接按钮时,串口能够打开与关闭,并且窗口中的控件文字会发生相应的变化。要实现该功能需要一个标志变量mIsOpen用来表明当前的串口状态,每次点击连接按钮执行槽函数时会判断当前mIsOpen的值,如果当前串口连接,则槽函数执行断开串口的操作,如果当前串口断开,则槽函数执行打开串口的操作。
部分代码:
//===================槽函数===================//
void MainWindow::on_BtnConnectPort_clicked()
{
if(mIsOpen)
{
//如果当前串口状态为打开,则这里执行关闭串口时的动作:
mSerialPort.close(); //关闭串口
ui->BtnConnectPort->setText("连接");//更改按钮2文字为“连接“”
ui->LabPortState->setText("未连接");//更改标签2文字为“未连接”
ui->BtnSerialPortConfig->setEnabled(true);//允许按钮1使能
mIsOpen = false;
}
else
{
//如果当前串口状态为关闭,则这里执行打开串口时的动作:
//1.读取配置文件2.配置并开启串口3.判断串口是否打开成功
readConfigFile();
if(startSerialPort())
{
//如果成功打开串口,则执行这里的代码
mIsOpen = true;
ui->BtnConnectPort->setText("断开");
ui->LabPortState->setText("已连接");
ui->BtnSerialPortConfig->setEnabled(false);//禁用按钮1
}
else
{
//如果打开串口失败,则执行这里的代码
QMessageBox::warning(this,"警告","打开串口出错,请检查串口连接或参数设置");
}
}
}
//===============读取配置文件的函数=================//
void MainWindow::readConfigFile()
{
QSettings iniConfigFile(mFileName,QSettings::IniFormat);//新建QSettings类实例,mFileName为配置文件所在地址。
mPortName = iniConfigFile.value("serialport/portname").toString();//读取ini文件中的数据
...
}
//===============配置并开启串口的函数=================//
bool MainWindow::startSerialPort()
{
mSerialPort.setPortName(mPortName);//配置端口
...
return mSerialPort.open(QSerialPort::ReadWrite);//打开串口,打开成功则会返回true,反之则返回false
}
代码说明:
在槽函数中首先判断标志变量mIsOpen的状态(true/false),如果为true,则点击按钮2后,槽函数执行关闭串口的动作;如果为false,则槽函数执行打开串口的动作。
关闭串口只需要执行QSerialPort类的.close()函数,然后对相关按钮标签文字做更改,以及使能按钮1即可。
打开串口需要分三步执行代码:第一步读取配置文件(readConfigFile()),第二步配置串口并打开串口(startSerialPort()),第三步根据startSerialPort()函数的返回值判断串口是否打开成功。若打开成功,则更改按钮标签文字,禁用按钮1;反之若打开失败,则弹出警告窗口(这里使用QT自带的QMessageBox类即可)。
对于readConfigFile()函数,我这里使用QSettings类来读取ini文件,关于ini文件的介绍可以见这里:CSDN:ini配置文件格式。QSettings读取ini文件时只需要先创建一个QSettings类实例iniConfigFile,然后调用iniConfigFile.value()函数对想要的数据读取即可,读取到的数据存储到类的成员变量(如mPortName)中以供后面的函数使用。具体可查看QT官方的帮助文档。
对于startSerialPort()函数,根据成员变量(如mPortName)的值配置串口并打开,配置时使用对应的.set***()函数即可。打开串口时调用.open()函数即可。注:这里配置串口时除了.setPortName()函数以外,其他的配置函数都不能将QString类型数据直接当做参数传入函数中,故我的源代码中使用了switch-case语句,详情请参考源代码。
关于配置文件config.ini,我将其放在了当前程序所在的文件夹下,每次打开程序时都会检测是否存在这个配置文件,如果不存在则会生成一个config.ini文件,并写入默认的配置。主程序的检测文件以及生成文件的代码如下:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
mIsOpen = false;//第一次打开时将标志变量mIsOpen置为false
//第一次打开程序时会创建配置文件
//mFileName
mFileName = QCoreApplication::applicationDirPath() + "/config.ini";
QFile file(mFileName);
if(!file.exists())//判断是否有配置文件,如果没有则在这里执行新建配置文件的动作
{
file.open(QIODevice::WriteOnly);
file.close(); //file.open()打开一个不存在的文件时会新建这个文件
makeConfigFile();//在这个函数中对ini文件写入数据
}
}
//=================对ini文件写入数据
void MainWindow::makeConfigFile()
{
QSettings iniConfigFile(mFileName,QSettings::IniFormat);
iniConfigFile.setValue("serialport/portname","COM1");
...
}
在makeConfigFile()函数中,QT对ini文件的写入与读取类似,也是先要创建一个QSettings类实例iniConfigFile,然后调用内部的iniConfigFile.setValue()函数即可。
3. 显示组
显示组为主窗口的第二个部分,UI界面如图
说明:
该组共三个控件,分别为一个显示窗口,两个按钮。两个按钮分别为测试按钮和清空按钮。
显示窗口用来显示串口接收到的数据,测试按钮用来测试是否与遥控小车连接成功,清空按钮用来清空显示窗口的内容 实现显示窗口显示接收到的数据
功能描述:开启串口后,当串口中接收到数据时,显示窗口(BrosMessage)会将接收到的数据显示在窗口中。该功能主要用来接受遥控小车发出的验证信息,即监控小车的当前运动状态。
代码片段:
// MainWindow初始化函数中添加的代码
connect(&mSerialPort, SIGNAL(readyRead()), this, SLOT(on_readyRead()));//当串口有数据传来时,读取串口数据,显示在textbrowser上。
//on_readyRead()函数
void MainWindow::on_readyRead()
{
if(mIsOpen)
{
//当前串口已打开,这里执行接收文字并显示的动作
QByteArray recvData = mSerialPort.readLine();//readLine():按行读取串口数据
ui->BrosMessage->append(QString(recvData));//append():将数据显示到显示窗口中
}
}
代码说明:
MainWindow初始化函数中添加的代码:该代码片段使用connect函数将串口信号与对应的槽函数关联起来。connect函数需要指明四个参数,分别为谁发出信号,发出什么信号,谁接收信号,怎么处理信号。对于该代码,mSerialPort实例发出信号,发出的是该实例具有的readyRead()信号,该信号有效则意味着串口接收到了数据。然后是this,也就是该MainWindow来接收信号,并使用on_readyRead()函数处理信号。
on_readyRead()函数:这个函数用来执行接收到串口信号后程序的动作,分为两步:第一步是读取串口中的数据,按行读取;第二步是将数据显示到显示窗口BrosMessage中。第一步中使用QSerialPort类自带的readLine()函数,第二步中使用QTextBrowser类自带的append()函数。注意在声明on_readyRead()函数时需要将其声明为slot函数类型。 实现点击测试按钮时发出测试指令
功能描述:点击测试按钮(BtnConnectTest)时,程序立刻通过串口发出测试指令。
代码片段:void MainWindow::on_BtnConnectTest_clicked()
{
mSerialPort.write(QByteArray::fromHex("1F"));//发送测试代码
}
代码说明:
on_BtnConnectTest_clicked()为一个槽函数,在该函数中使用QSerialPort类自带的write()即可通过串口发送指定数据。
我设计的遥控车指令代码都是两个十六进制数,为正确发送十六进制数,使用了fromHex()函数,该函数可以将字符串形式的十六进制数转换为十六进制字节码流。 实现点击清空按钮时清空显示窗口的内容
功能描述:点击清空按钮(BtnClearMessage)时,程序将显示窗口(BrosMessage)中的所有文字清空。 代码片段:
void MainWindow::on_BtnClearMessage_clicked()
{
ui->BrosMessage->clear();
}
代码说明:
这里同上一步类似写一个槽函数命名为on_BtnClearMessage_clicked(),在该函数中,调用QTextBrowser类的clear()函数,实现清空窗口的功能。