STM32与上位机通信协议——UART协议:
串行通讯需要有通信协议
通信协议:规定发送与接收方,通信的方式与要求,数据的格式
由RXD和TXD两条线,由于没有时钟线,所以需要规定波特率
数据传输速率就是波特率
UART(串行异步全双工)
采用的是串行通信,也就是一条传输线,一位一位的顺序发送(可以远距离传输,传输较慢)
异步通信是以一个字符为传输单位,每个字符为10位(1个起始位,7个数据位,1个校验位,1个停止位)
通信中两个字符之间的时间间隔不固定,但是同一个字符相邻位之间时间间隔是固定的:
数据通信格式
1、起始位
2、数据位
3、校验位
4、停止位
5、空闲位
STM32需要通过串口向上位机完成数据上传,然后完成实时显示。
所以需要先完成上位机的一个开发,上位机开发工具选择QT
创建QT工程:
选择QT Widgets 修改名称为:serialdisplay,路径自己选择
直接点击下一步,到这个界面:
修改一下类名为:SerialDisplay
直接点击下一步,直到点击完成 创建工程就成功了,创建完成可以运行一下;
接下来就是添加空间,找到设计界面,点击ui
就转到这个界面,可以开始添加控件,左边就是控件栏:
添加完控件如下:
修改右边对应控件的名字,便于代码里使用;
然后就是代码部分:
右击你要实现功能的控件,比如说打开串口按钮如下:
点击转到槽函数:
选择点击触发,点击ok,就会跳转到以下界面:
对应实现该函数即可。其他我就不一一介绍了:
直接上源码:
serialdisplay.cpp文件
#include "serialdisplay.h"
#include "ui_serialdisplay.h"
#include <QDebug>
#include <QMessageBox>
#include <QFontDialog>
#include <QJsonObject>
#include <QJsonDocument>
SerialDisplay::SerialDisplay(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::SerialDisplay)
{
ui->setupUi(this);
serialflag = 0;//表示串口关闭
setWindowFlags(windowFlags()&~Qt::WindowMaximizeButtonHint);// 禁止最大化按钮
serialport = new QSerialPort();//分配内存
ui->dateTimeEdit->setDisabled(true);//
timerid = startTimer(1000);//开启定时器,每隔1秒
}
SerialDisplay::~SerialDisplay()
{
killTimer(timerid);//关闭
delete ui;
delete serialport;
}
//串口获取
void SerialGetData::Serial_read()
{
int temp = 0;//
int humi = 0;
QByteArray recvData;
try {
recvData = SerialGetData::serial_recv_Data();//从串口接收数据
} catch (MyExcption &err) {
QMessageBox::warning(NULL , "提示", err.what());
return;
}
QString receive = QString::fromLocal8Bit(recvData.constData());
//json格式数据解析 比如:{"温度":"10℃","湿度":"20%"}
QJsonParseError err;
QByteArray arr ;
arr.append(receive);
QJsonDocument doc = QJsonDocument::fromJson(arr,&err);
if(err.error != QJsonParseError::NoError){
qDebug() << "转换失败";
return;
}
QJsonObject obj = doc.object();
temp = SerialGetData::getNumsFromStr(obj.value("温度").toString());
humi = SerialGetData::getNumsFromStr(obj.value("湿度").toString());
// qDebug() << receive;
/*分割字符串的方法*/
/*//自定义数据格式(温度:10-湿度:20或者10℃-20%)
QStringList list = receive.split("-");//以:分割字符串
if(list[0].contains("温度") || list[0].contains("℃")){
temp = SerialGetData::getNumsFromStr(list[0]);
humi = SerialGetData::getNumsFromStr(list[1]);
}
else if(list[0].contains("湿度") || list[0].contains("%")){
temp = SerialGetData::getNumsFromStr(list[1]);
humi = SerialGetData::getNumsFromStr(list[0]);
}
else{
QMessageBox::information(NULL,"提示","数据格式错误",QMessageBox::Ok);
return;
}
*/
ui->lcdTemp->display(temp);
ui->lcdHuim->display(humi);
}
//串口接收
QByteArray SerialGetData::serial_recv_Data() throw(MyExcption)
{
//读取串口收到的数据
QByteArray buffer = serialport->readAll();
//为空的话
if(buffer.isEmpty()){
throw("收到数据为空");
return NULL;
}
return buffer;
}
//获取字符串中数字部分
int SerialGetData::getNumsFromStr(QString data)
{
QString num;
int j = 0;
for(int i = 0 ; i < data.length();i++){
if(data[i] >= '0' && data[i] <= '9'){
num[j] = data[i];
j++;
}
}
return num.toInt();
}
//扫描串口
void SerialGetData::on_scanSerialBtn_clicked()
{
// 清除当前显示的端口号
ui->serialPortBox->clear();
//检索端口号
foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
{
ui->serialPortBox->addItem(info.portName());
}
}
//打开串口
void SerialGetData::on_openSerialBtn_clicked()
{
if(ui->openSerialBtn->text() == QString("打开串口")) //串口未打开
{
//设置端口号
//qDebug() << ui->serialPortBox->currentText();
serialport->setPortName(ui->serialPortBox->currentText());
//设置波特率
serialport->setBaudRate(ui->baudRateBox->currentText().toInt());
//设置数据位
switch (ui->dataBitBox->currentText().toInt())
{
case 8: serialport->setDataBits(QSerialPort::Data8); break;
case 7: serialport->setDataBits(QSerialPort::Data7); break;
case 6: serialport->setDataBits(QSerialPort::Data6); break;
case 5: serialport->setDataBits(QSerialPort::Data5); break;
default: break;
}
//设置停止位
switch (ui->stopBitBox->currentText().toInt())
{
case 1: serialport->setStopBits(QSerialPort::OneStop);break;
case 2: serialport->setStopBits(QSerialPort::TwoStop);break;
default:break;
}
//设置校验方式
switch (ui->chekBitBox->currentIndex())
{
case 0: serialport->setParity(QSerialPort::NoParity);break;
default:break;
}
//设置流控制模式
serialport->setFlowControl(QSerialPort::NoFlowControl);
//打开串口
if(serialport->open(QIODevice::ReadWrite)==false)
{
QMessageBox::warning(NULL , "提示", "串口打开失败!");
return;
}
// 失能串口设置控件
ui->serialPortBox->setEnabled(false);
ui->chekBitBox->setEnabled(false);
ui->baudRateBox->setEnabled(false);
ui->dataBitBox->setEnabled(false);
ui->stopBitBox->setEnabled(false);
ui->scanSerialBtn->setEnabled(false);
//调整串口控制按钮的文字提示
ui->openSerialBtn->setText(QString("关闭串口"));
ui->openSerialBtn->setStyleSheet("background-color: rgb(255, 255, 255);\
color: rgb(255,0,0);\
border-color: rgb(255, 0, 0);");
serialflag = 1;//串口打开
}
else //串口已经打开
{
serialport->close();
// 使能串口设置控件
ui->serialPortBox->setEnabled(true);
ui->chekBitBox->setEnabled(true);
ui->baudRateBox->setEnabled(true);
ui->dataBitBox->setEnabled(true);
ui->stopBitBox->setEnabled(true);
ui->scanSerialBtn->setEnabled(true);
//调整串口控制按钮的文字提示
ui->openSerialBtn->setText(QString("打开串口"));
ui->openSerialBtn->setStyleSheet("background-color: rgb(0, 255, 255);\
color: rgb(0,0,255);\
border-color: rgb(255, 0, 0);");
serialflag = 0;//串口关闭
}
}
//建立连接
void SerialGetData::on_eConnectBtn_clicked()
{
if(serialflag == 0){
QMessageBox::information(NULL,"提示","串口未打开",QMessageBox::Ok);
return;
}
ui->eConnectBtn->setEnabled(false);
ui->eDisconnectBtn->setEnabled(true);
ui->eConnectBtn->setStyleSheet("background-color: rgb(255, 255, 255);\
color: rgb(255, 0, 0);\
border-color: rgb(255, 0, 0);");
ui->eDisconnectBtn->setStyleSheet("background-color: rgb(0, 255, 255);\
color: rgb(0, 0, 255);\
border-color: rgb(255, 0, 0);");
/*
if(connectflag == 1){
QMessageBox::information(NULL,"提示","已建立连接",QMessageBox::Ok);
return;
}*/
connect(serialport,&QSerialPort::readyRead,this,&SerialGetData::Serial_read);
//connectflag = 1;//已为信号绑定槽函数
}
//关闭连接
void SerialGetData::on_eDisconnectBtn_clicked()
{
if(serialflag == 0){
QMessageBox::information(NULL,"提示","串口未打开",QMessageBox::Ok);
return;
}
ui->eDisconnectBtn->setEnabled(false);
ui->eConnectBtn->setEnabled(true);
ui->eConnectBtn->setStyleSheet("background-color: rgb(0, 255, 255);\
color: rgb(0, 0, 255);\
border-color: rgb(255, 0, 0);");
ui->eDisconnectBtn->setStyleSheet("background-color: rgb(255, 255, 255);\
color: rgb(255, 0, 0);\
border-color: rgb(255, 0, 0);");
/*
if(connectflag == 0){
QMessageBox::information(NULL,"提示","未建立连接",QMessageBox::Ok);
return;
}*/
disconnect(serialport,&QSerialPort::readyRead,this,&SerialGetData::Serial_read);
//connectflag = 0;//已解除绑定
ui->lcdTemp->display(0);
ui->lcdHuim->display(0);
}
//定时器,定时获取时间展示
void SerialGetData::timerEvent(QTimerEvent *event)
{
//获取当前时间
QDate currentdate = dateTime.currentDateTime().date();
QTime currenttime = dateTime.currentDateTime().time();
if(event->timerId() == timerid){
ui->dateTimeEdit->setDate(currentdate);
ui->dateTimeEdit->setTime(currenttime);
}
}
serialdisplay.h文件:
#ifndef SERIALDISPLAY_H
#define SERIALDISPLAY_H
#include <QMainWindow>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
#include <QDateTime>
#include "TOOL/myexcption.h"
QT_BEGIN_NAMESPACE
namespace Ui { class SerialDisplay; }
QT_END_NAMESPACE
class SerialDisplay : public QMainWindow
{
Q_OBJECT
public:
SerialGetData(QWidget *parent = nullptr);
~SerialGetData();
QByteArray serial_recv_Data() throw(MyExcption);
bool serial_sendData(QString data);
int getNumsFromStr(QString data);//获取字符串中数字部分
private slots:
void on_scanSerialBtn_clicked();
void Serial_read();
void on_openSerialBtn_clicked();
void on_eConnectBtn_clicked();
void on_eDisconnectBtn_clicked();
void timerEvent(QTimerEvent *event)override;
private:
Ui::SerialGetData *ui;
QSerialPort *serialport;
int timerid;
int serialflag;
QDateTime dateTime;
};
#endif // SERIALDISPLAY_H
上面两个文件用到了,自定义异常处理:
myexcption.cpp文件:
#include "myexcption.h"
MyExcption::MyExcption(const char *err)
{
this->error = err;//
}
myexcption.h文件:
#ifndef MYEXCPTION_H
#define MYEXCPTION_H
class MyExcption
{
public:
MyExcption(const char *err);
const char* what() throw(){
return this->error;
}
private:
const char *error;
};
#endif // MYEXCPTION_H
代码中完成了实时时间的显示,和温湿度显示,温湿度部分用到了json格式数据的处理,要求下位机传上来的数据必须是json格式的数据,接下来说说json格式数据:
json格式大概分为三种:
第一种,也是最简单的(JSON对象):
{
"name":"smith",
"age":30,
"sex":男
}
第二种 (对象的属性也可以是JSON对象):
{
"name":"smith",
"age":18
"sex":男
"school":{
"sname":"南京大学",
"address":"南京市鼓楼区汉口路22号"
}
}
第三种(对象数组):
[
{
"title":"Java实战经典开发",
"edition":3,
"author":["smith","尼古拉斯","斯巴达"]
},
{
"title":"Oracle实战经典开发",
"edition":3,
"author":["smith","尼古拉斯","斯巴达"]
},
{
"title":"Vue实战经典开发",
"edition":5,
"author":["smith","尼古拉斯","斯巴达"]
}
]
其实JSON数据就是一段字符串而已,只不过有不同意义的分隔符将其分割开来而已,我们看上面的符号,里面有[] ,{}等符号,其中
1 []中括号代表的是一个数组;
2 {}大括号代表的是一个对象
3 双引号“”表示的是属性值
4 冒号:代表的是前后之间的关系,冒号前面是属性的名称,后面是属性的值,这个值可以是基本数据类型,也可以是引用数据类型。
而我们只是简单的上传温湿度,所以只需要用到第一种:
可以规定数据格式为:
{
"温度":"18℃",
"湿度":"70%"
}
而下位机只需要按照这种格式发数据到串口即可。
实现效果图:
对之前的控件进行了颜色修改和美化。
问题
2024-4-8修改:
针对大家评论区或者私信提出的一直转换失败的原因:由于串口是按流接收,会在长数据发送时出现一帧数据不完整的情况,比如发送:1dabnxaiuhnsxius这个字符串,会在第一帧数据发送:1dabnxaiu第二帧发送:hnsxius,而我那里没有做完整性判断,所以会出问题,只需要将串口读取部分做一个约定,以回车换行结尾即可解决该问题,在接收到回车换行之前的数据保存,每一帧数据拼接起来,直到获取到回车换行为止,修改部分函数如下:
完整工程链接:
请根据资源链接下载