STM32串口通信 链表接收不定长数据帧
数据帧说明
STM32数据寄存器为USARTx->DR寄存器
可以看到DR寄存器只有[8:0]位可以使用,第8位用于奇偶校验,也就是DR寄存器一次只能接受8bit既1字节的数据。
不太恰当的比方
打个比方就是一个篮子 (DR寄存器) 只能装8 (bit) 个物品,
我们用这个篮子把水果放到我们的仓库 (MCU) 中,
别人把物品一个一个放入篮子里,装满8个我们就把这篮子东西放到仓库里。
但是我们觉得这样放太乱了,就在仓库里面划了一片地,在上面贴了一个苹果标签,以后接收到的东西就放这里。
但是仓库里又不只是放苹果,而且我们也不能确定装进我篮子里的是不是苹果,也就是说我们单次接受数据的时候基本没有办法判断数据的可靠性,而且数据也是单一的。
那这样吧,我们定个规矩,你往我篮子里放了3个苹果5个橘子就代表你要开始发送有用数据了,
第二篮子你就放苹果,
第三篮子你就放橘子,
第四篮子如果放的是3个橘子5个苹果,那这次接收就结束了,
然后我也不用检查哪一个篮子是苹果哪一个是橘子,
直接就可以把第二篮放到苹果的位置,第三篮放到橘子的位置。
打的比方有些不太恰当,但基本就是这么个意思,这样我们就可以一帧接收多样的数据,通过确定开始和结束的协议也提高数据的可靠性。
但是问题又来了,我们只有一个篮子,一下子接收不了那么多篮子的数据,
那怎么办,我们在仓库立划个缓存区用于存储别人发送的数据,也就是我们把这次的数据接收完再做处理,但是这个缓冲区一般都是使用数组定义,也就是要事先规定好,你发给我8个篮子的东西,我就划8个篮子的地方,一旦开始接收数据,我事先划的地放大小就不能改了,因为数组定义的时候要事先给定长度。
呐有没有可以边接收边划分空间的方法呢,最近也在看链表的相关知识于是便想到将链表用于数据缓冲区,这样就可以边接受数据边开辟空间了。
数据缓冲链表结构
struct Frame
{
u8 data; //数据域
struct Frame *next; //指针域指向下一个节点
};
我使用的链表比较简单,一是用于节省空间,另一方面自己会的也不多。
数据域就用于存储串口传来的8bit数据,
指针域就用于指向下一个节点,把两个节点联系起来,最后一个节点指向NULL代表链表结尾。
基本结构就是这样:
头结点的作用是用于指向下一个数据,data存储一帧的节点数量。
比如一帧的数据为 :A5 34 56 5A
那么:Head->data=4,也就是除去头结点一共接收了4个节点
一定要注意 !!不用的指针一定要指向NULL,防止产生野指针造成内存泄漏。
那么我们定义一个全局的头指针就可以在程序了任何地方使用了
struct Frame *Head=NULL; //头指针
extern struct Frame *Head;//声明全局变量
这个头指针现在是没有存储空间的因为它现在只是个地址信息
/**
**********************************
* 函数名:Head_Init
* 描述 :初始化头结点
* 输入 :无
* 输出 : 无
* 注 :
**********************************
*/
void Head_Init(void)
{
Head=(struct Frame *)malloc(sizeof(struct Frame));
if(Head==NULL)
exit(1);
Head->data=0;
Head->next=NULL;
}
我们给头指针分配完内存,头指针就是头结点了,
头节点的data就可以存储节点长度了。
然后我们就可以在串口中断里写我们规定的协议了
#define Frame_Head 0xA5 //帧头
#define Frame_END 0x5A //帧尾
/**********帧头帧尾标志位**********/
bool Frame_Head_sta=0;
bool Frame_End_sta=0;
/**
**********************************
* 函数名:USART1_IRQHandler
* 描述 :接受数据格式为:数据长度->帧头->数据->数据...->帧尾
* 例:4 A5 12 34 56 5A
* 输入 :无
* 输出 : 无
* 注 :
**********************************
*/
void USART1_IRQHandler(void)
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
//判断是否接受到帧头
if(Res==Frame_Head)
{
Frame_Head_sta=1;
}//判断是否接受到帧尾->若还没有接受到帧头不接受帧尾
else if(Res==Frame_END)
{
Frame_End_sta=1;
}
//已经接受到帧头或者帧尾
if((Frame_Head_sta==1)||(Frame_End_sta==1))
{
//数据长度加一
Head->data+=1;
//添加数据进入链表->尾插法
Head=Add_Data(&Head,Res);
}
}
}
协议定的比较简单,看一下注释基本就明白了
链表的插入方法采用的尾插法,其实头插法更快一些,不需要轮训数据,但是我感觉这样处理数据不太舒服,于是便写成了尾插法。
/**
**********************************
* 函数名:Add_Data
* 描述 :尾插法插入节点
* 输入 :(指向头结点的指针 需要插入的数据)
* 输出 : 无
* 注 :
**********************************
*/
struct Frame* Add_Data(struct Frame **head,u8 Res)
{
struct Frame *data,*temp;
//分配内存
data=(struct Frame *)malloc(sizeof(struct Frame));
//数据域存储串口数据
data->data=Res;
//指针域指向NULL
data->next=NULL;
//指向头指针指向的位置不是NULL ->头结点后面已经连接了其他节点
if(*head!=NULL)
{
//指向头节点的指针传给temp ->保证*head不发生变化
temp=*head;
//一个节点一个节点往后查询直到temp->next=NULL,也就是最后一个节点的位置
while(temp->next)
{
temp=temp->next;
}
//把最后一个节点指向的位置改成data也就是在最后一个节点插入新的节点
temp->next=data;
}
else//头节点后还未插入节点
{
*head=data;
}
return *head;
}
一帧数据接受完之后一定要及时处理释放内存空间,不然会造成内存溢出,程序跑飞。
写到这里我又遇到一个问题,我接受的数据是不定长的,那么我存储起来也要是不定长的
但是我当时还没有好的思路,于是写了下面这坨代码
/**
/**
**********************************
* 函数名:Frame_Manage
* 描述 :链表数据处理
* 输入 :无
* 输出 : 无
* 注 :
**********************************
*/
void Frame_Manage( u8 *data0,u8 *data1,u8 *data2,
u8 *data3,u8 *data4,u8 *data5,
u8 *data6,u8 *data7,u8 *data8)
{
u8 i;
//接受到帧头和帧尾
if(Frame_Head_sta&&Frame_End_sta)
{
//标志位清零
Frame_Head_sta=0;
Frame_End_sta=0;
//遍历链表并存储数据
while(Head!=NULL)
{
switch(i)
{
case 0: *data0=Head->data;break;
case 1: *data1=Head->data;break;
case 2: *data2=Head->data;break;
case 3: *data3=Head->data;break;
case 4: *data4=Head->data;break;
case 5: *data5=Head->data;break;
case 6: *data6=Head->data;break;
case 7: *data7=Head->data;break;
case 8: *data8=Head->data;break;
}
//释放节点内存
free(Head);
//地址往下走
Head=Head->next;
i++;
}
//再次分配内存给头结点 因为已经释放了投机点的内存
Head_Init();
}
}
这个代码真是相当难受,明明是不定长的接受,后来又变成固定长度的存储
后来我才想到头结点里存放的数据长度,
可以直接利用这个数据长度,使用malloc开辟一个相同长度的空间存储帧数据,
使用u8类型的指针指向开辟的内存空间,用于存放一帧的数据,
记录好首地址的位置,经过 Frame_Manage() 函数后,
我们的帧数据就存储在 *Frame_data 所指向的空间
下次帧数据来了以后再释放空间就可以了,重新更新数据长度就可以了
修改后的代码如下:
u8 *Frame_data=NULL; //帧数据缓冲区
u8 *Frame_data_Head=NULL;//只用于存放缓冲区首地址
extern u8 *Frame_data; //帧数据存储
extern u8 *Frame_data_Head;//只用于存放缓冲区首地址
/**
**********************************
* 函数名:Frame_Manage
* 描述 :链表数据处理
* 输入 :无
* 输出 : 无
* 注 :
**********************************
*/
void Frame_Manage(void)
{
//如果接收到帧头 帧尾
if(Frame_Head_sta&&Frame_End_sta)
{
//释放上次缓冲区空间->放入Frame_data_Head
free(Frame_data_Head);
//指向NULL->防止乱指
Frame_data=NULL;
//开辟一帧的大小(可变)
Frame_data=(u8 *)malloc(sizeof(u8)*(Head->data));
//存放空间首地址->用于free和读取数据
Frame_data_Head=Frame_data;
//清除标志位
Frame_Head_sta=0;
Frame_End_sta=0;
while(Head!=NULL)
{
//存放帧数据
*Frame_data=(Head->data);
//地址->后移
Frame_data++;
free(Head);
Head=Head->next;
}
Head_Init();
//归还空间首地址
Frame_data=Frame_data_Head;
}
}
这片空间的用法和指向数组的指针的用法相同,
通过下面这个代码就可以读取数据帧任意一点的数据了
/**
**********************************
* 函数名:Find_Frame
* 描述 :读取固定位置数据
* 输入 :位置,起始地址
* 输出 : u8 数据
* 帧格式 :4 A5 34 56 5A
* 位置 : 0 1 2 3 4
**********************************
*/
u8 Find_Frame(u8 team,u8 *head)
{
head+=team;
return *head;
}
很显然8bit无符号型明显是不够用的
就写了俩u8融合成u16的,其他类型的融合思路基本相似
/**
**********************************
* 函数名:Fusion
* 描述 :u8融合u16
* 输入 :team0 高8位位置 team1 低8位位置
* 输出 : 无
* 注 :
**********************************
*/
u16 Fusion(u8 team0,u8 team1)
{
team0=Find_Frame(team0,Frame_data);
team1=Find_Frame(team1,Frame_data);
return ((team0<<8)+team1);
}
效果展示
我们直接用串口发送16进制且包含帧头 (A5) 帧尾 (5A) 的数据帧STM32就可以接受到数据帧并打印出来。
可以看出我们发送数据帧长度变化的时候,STM32同样可以接收该长度的数据帧,完全不用修改程序,非常的方便,而且也是用多少拿多少,非常的人性化。
文件放在下面了,芯片类型为F103VCT6
工程文件
提取码:qqy7