项目背景:底层硬件每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灯去使用了。
图1 图2 图3
图1:ui设计界面,其中运行灯为按钮;图2:运行界面时,灯为黄色,串口与波特率可选;图3:点击开始按钮后,其显示变为停止,灯闪烁,串口号与波特率不可选。
(1)能用ui界面就用ui界面,代码写着还是麻烦。有开发人员强调直接写代码牛b,我不这样认为。只要能攒出来,做出需要的逻辑和效果,哪种方式都是可以的,都是很牛b的。
(2)能借鉴代码就借鉴代码,可以提高效率。能不自己写的函数,能直接调用库函数,或者别人封装好的库函数,就用已经弄好的库函数。不否认自己写一遍可以提高,但是项目那么紧,先把东西搞出来,然后自己留时间再去一点一点自己写之前想自己写的代码,毕竟手中有粮,心中不慌。
(3)程序需要一个星期,得报两个星期的时间吧。不是偷懒,而是(1)你需要调试,试着试着BUG就出来了,写的再快,还是有BUG的,留调试时间;(2)留自己学习时间,这个就不说了,真是想休息也可以的。
注:由于小伙伴需要源代码的时间不同,登录邮箱界面太多麻烦,所以建立了一个订阅号,如果有问题或者需要源码,可添加订阅号,留言后会发送源代码或者有任何问题可留言,将积极解决提出的问题。