文章目录
- 一. 关于数据传输的方式
- 1.1 基本数据类型传输
- 1.2 结构体传输
- 二. STM32与QT实现串口传输结构体实例
- 2.1 下位机的实现
- 2.2 上位机的实现
- 2.3 演示Demo
- 三. 注意事项
- 3.1 关于字节对齐问题
- 3.2 关于大小端问题
一. 关于数据传输的方式
在日常开发过程中,我们时常需要用到串口来传输数据,无论是MCU与MCU通信,还是上位机与下位机之间的通信,串口传输数据都非常普遍。对于传输方式,可以直接分别将单个数据类型进行拆分后发送;也可以将数据封装成结构体后再进行拆分发送。
1.1 基本数据类型传输
如果是传输少量数据,可以根据数据类型,把数据拆分成多个字节后通过串口发送。另一端串口接受后,根据拆分的方式对数据进行合并。下面以STM32的板间通信为例:
发送端:
uint8_t msg_send[10] = {0};
uint16_t meg_uint16;
float meg_float;;
/*帧头*/
msg_send[0] = 0x0A;
msg_send[1] = 0x0B;
/*数据段*/
msg_send[2] = (uint8_t)(meg_uint16>> 8);//高8位
msg_send[3] = (uint8_t)(meg_uint16); //低8位
/*数据段*/
msg_send[4] = (uint8_t)(meg_float>> 24);//同上
msg_send[5] = (uint8_t)(meg_float>> 16);
msg_send[6] = (uint8_t)(meg_float>> 8);
msg_send[7] = (uint8_t)(meg_float);
/*帧尾*/
msg_send[8] = 0x0C;
msg_send[9] = 0x0D;
HAL_UART_Transmit(&huart1,msg_send,6,0xff);//串口发送
接收端:
uint8_t msg_receive[10] = {0};
HAL_UART_Receive_IT(&huart1, msg_receive, 10);//串口接受
if(msg_receive[0]==0x0A&&msg_receive[1]==0x0B&&msg_receive[8]==0x0C&&msg_receive[9]==0x0D)//验证帧头帧尾
{
uint16_t meg_uint16 = (uint16_t)(msg_receive[2]<<8|msg_receive[3]);
float meg_float = (float)(msg_receive[4]<<24|msg_receive[5]<<16|msg_receive[6]<<8|msg_receive[7]);
}
1.2 结构体传输
上面的方式虽然简单,但是我们需要对每个数据进行单独的拆分与合并。发送和接受时,就需要计算出数据格式中每个单元所对应的位置,即数组中第i个元素对应的内容。这样显然是很麻烦的,效率很低,这就相当于先织了一个大网,捕捉到一网鱼,还得过下称,才能按照重量分类开来一样。
那么如果我们能提前根据接收的数据格式来做一个容器,直接把接收的数据复制到这个容器内,就可以剩下很多的操作,这就是通过结构体的方式来传输,如上图所示。
二. STM32与QT实现串口传输结构体实例
下面以STM32与QT通信为例,讲述具体的代码实现:
开发平台:STM32F030F4P6 / PC (window10)
开发环境:Keil-v5.31/ Qt Creator-v4.8.2
开发语言:C/C++
注意,由于STM32串口输出的是TTL电平,因此还需要加多一个CH340芯片转成USB信号,再与PC串口通信。
假设我们要传输的结构体如下,#pragma pack(1)
代表结构体中变量会在内存中按照一字节对齐的方式存储,所以结构体的所占的字节数就是所有成员类型所占字节数之和,即14个字节。关于字节对齐的问题,可以学习这篇文章(链接)。
#pragma pack(1)//按照1字节对齐
typedef struct Data_Structure
{
uint8_t Info_u8;
uint16_t Info_u16;
float Info_float;
}DataS;
typedef struct CSInfoStrcutre
{
uint8_t data_u8;
uint16_t data_u16;
float data_float;
DataS data_Structure;
} CSInfoS;
#pragma pack()//恢复默认字节对齐规则
typedef struct CSInfoStrcutre* ptrCSInfo;
2.1 下位机的实现
主要实现步骤如下:
uint8_t infoArray[14]={0};
uint8_t infoPackage[18]={0};
CSInfo_2Array_uint8(ptrCSInfo,infoArray);
CSInfo_Pack(infoPackage,infoArray,sizeof(infoArray));
HAL_UART_Transmit(&huart1,infoPackage,18,0xFF);
通过CSInfo_2Array_uint8()
将结构体拆分为单个字节的数组。再通过CSInfo_Pack()
给数组加入帧头与帧尾构成完整的数据包。最后通过STM32 HAL库自带的串口发送函数HAL_UART_Transmit()
,将数据发送去上位机。本文数据处理部分参考的是这篇博客(链接)。
/**
* @brief 将数据段(CSInfoS)重组为uint8类型的数组
* @param infoSeg 指向一个CSInfoS的指针
* @param infoArray 由数据段重组的uint8类型数组
* @retval 无
*/
void CSInfo_2Array_uint8(ptrCSInfo infoSeg,uint8_t* infoArray)
{
int ptr=0;
uint8_t* infoElem=(uint8_t*)infoSeg;
for(ptr=0;ptr<sizeof(CSInfoS);ptr++){
infoArray[ptr] = (*(infoElem+ptr));
}
}
/**
@Protocol
----------------------------------------------
头 | 信息 | 尾 |
----------------------------------------------
0x0A|0x0B| CSInfoStrcutre | 0x0C|0x0D |
----------------------------------------------
2Byte | 14Byte | 2Byte |
----------------------------------------------
* @brief 按协议打包
* @param infopackage 打包结果,按协议结果为2+14+2=18字节
* @param infoArray 由数据段重组的uint8类型数组
* @param infoSize 数据段的大小
* @retval 无
*/
void CSInfo_Pack(uint8_t* infopackage,uint8_t* infoArray,uint8_t infoSize)
{
uint8_t ptr=0;
infopackage[0] = 0x0A;
infopackage[1] = 0X0B;
/* 将信息封如入数据包中 */
for(;ptr<infoSize;ptr++){
infopackage[ptr+2] = infoArray[ptr];
}
infopackage[ptr+2] = 0X0C;
infopackage[ptr+3] = 0X0D;
}
2.2 上位机的实现
上位机的大致处理流程如下,这里只讲解串口数据处理部分,关于其他内容,可以去看源码。
在MainWindow的初始函数中加入connect(m_serialport,SIGNAL(readyRead()),this,SLOT(receive_data()))
,目的在于当串口接收到数据时,则会进入到数据处理函数receive_data()
中。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_serialport = new QSerialPort();
serialport_init();
m_serialport->setReadBufferSize(90);//设置串口缓存区大小为90字节
connect(m_serialport,SIGNAL(readyRead()),this,SLOT(receive_data()));//串口数据接受
connect(ui->pushButton_1,SIGNAL(clicked()),this,SLOT(serialport_init()));//刷新串口
connect(ui->pushButton_2,SIGNAL(clicked()),this,SLOT(open_serialport()));//打开与关闭串口
QTimer *timer1 = new QTimer(this); //初始化一个定时器,用于定时刷新UI上的Qlabel
connect(timer1, SIGNAL(timeout()), this, SLOT(refresh_data())); //定时更新Qlaber
timer1->start(400); //刷新频率。
}
receive_data()
中对串口中的数据进行处理,具体流程如下:
/**
@Protocol
----------------------------------------------
头 | 信息 | 尾 |
----------------------------------------------
0x0A|0x0B| CSInfoStrcutre | 0x0C |
----------------------------------------------
2Byte| 57Byte | 1Byte |
----------------------------------------------
* @brief 数据到来时触发数据接收
*/
void MainWindow::receive_data()
{
if(m_serialport->bytesAvailable()>=18)//查看串口缓存区有多少数据
{
uint8_t package_serial[36];//串口提取出的原始数据
uint8_t infoArray[14]; //提取出一个数据段
/* 读取数据 */
int numHasRead = readInfoFromSerialport(package_serial); //从串口读取数据至数组
/* 提取数据段 */
bool readable = CSInfo_GetInfoArrayInpackages(infoArray,package_serial,numHasRead);//根据帧头寻找数据段
/* 数据段解包 */
if(readable)
{CSInfo_InfoArray2CSInfoS(infoArray,this->ptrCSInfo);}//把数据传入结构体中
}
}
/**
* @brief 把当前serialport缓冲区的数据全部读取到一个uint8类型的数组中
* @param packages 从串口读取到的包含数据包的数据
* @retval numHasRead 从缓冲区读取到的字节数
*/
int MainWindow::readInfoFromSerialport(uint8_t* packages)
{
int numHasRead = 0;
/* 没有可用的串口设备则中止读取操作 退出函数 */
if(m_serialport->isOpen())
{
/* 读取串口缓冲区数据 */
QByteArray dataArray = m_serialport->read(36);
/*计算数据长度*/
numHasRead = dataArray.size();
/*将读出来的数据迁移至dataArray中*/
if(numHasRead>=18){
for(int i=0;i<numHasRead;i++){
*(packages+i) = (uint8_t)dataArray[i];
}
}
return numHasRead;
}
else {return 0;}
}
/**
* @brief 在串口读取到的数据中根据帧头提取出数据段
* @param infoArray 串口缓冲区读出的数据
* @param packages 从串口读取到的包含数据包的数据
* @param sizepackages 从串口读取到的字节数(packages的大小)
* @retval readable 读取成功为true,失败为false
*/
bool MainWindow::CSInfo_GetInfoArrayInpackages(uint8_t* infoArray,uint8_t* packages,int sizepackages)
{
int ptr;
if(sizepackages<18){
return false;
}
else
{
/*检测出帧头所在的位置*/
for(ptr=0;ptr<sizepackages;ptr++){
if((packages[ptr]==0x0A)&&(packages[ptr+1]==0x0B)&&(packages[ptr+16]==0x0C)&&(packages[ptr+17]==0x0D))
{
/*把帧头后的14个字节读取出来*/
ptr += 2;
for(int i=0;i<14;i++){
infoArray[i] = packages[ptr+i];
}
return true;
}
}
}
return false;
}
/**
* @brief 把存有一个数据段的数组解析为一个CSInfoStructure,结果存到参数2对应的地址
* @param infoArray 存有一个数据段的uint8类型的数组
* @param infoStrc 从串口读取到的字节数(packages的大小)
* @retval 无
*/
void MainWindow::CSInfo_InfoArray2CSInfoS(uint8_t* const infoArray,ptrCSInfo infoStrc)
{
uint8_t* u8PtrOStrc = (uint8_t*)infoStrc;
for(int i=0;i<14;i++)
*(u8PtrOStrc+i) = infoArray[i];
}
2.3 演示Demo
这里根据上面所写的代码简单的做了个Demo,左边为QT的页面,右边则是Keil的调试页面。
功能主要是:将STM32中的结构体数据传输到QT上,QT接受到后把数据打印在页面上。每个数据都递增处理,方便查看变量的变化。
关于这个Demo的源码,已经上传到CSDN上了,有需要的自取(下载链接)。
三. 注意事项
3.1 关于字节对齐问题
关于字节对齐的问题,可以学习这篇文章(链接)。
结构体的所占的字节数并不一定是所有成员类型所占字节数的总和,这涉及到字节对齐的问题。STM32默认的字节对齐数是4,如果我们希望实现结构体的所占的字节数是所有成员类型所占字节数的总和,我们就需要规定结构体按照1字节对齐来存储,但是这也一定上增加总线的访问次数,降低了访问效率。
#pragma pack(1)//按照1字节对齐
typedef struct Data_Structure
{
uint8_t Info_u8;
uint16_t Info_u16;
float Info_float;
}DataS;
#pragma pack(0)
上位机与下位机传输结构体的时候,传递双方的对于传输对象的字节对齐规则需要一致。
3.2 关于大小端问题
大小端指的是字节序,就是内存中存储数据的字节顺序
大端模式: 高字节存于内存低地址,低字节存于内存高地址。
小端模式: 低字节存于内存低地址,高字节存于内存高地址。
STM32使用的是小段模式,QT我测了也是使用的小段模式,因此在源码里,我没有针对大小端做处理。但是,对于大小端模式不同的平台,串口通信时还需要做数据的转换。