# STM32 USART 链表接收不定长 **Repository Path**: mwh2035/STM32_USART ## Basic Information - **Project Name**: STM32 USART 链表接收不定长 - **Description**: 仅供参考 - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2021-10-31 - **Last Updated**: 2024-06-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # **STM32串口通信 链表接收不定长数据帧** # 数据帧说明 STM32数据寄存器为USARTx->DR寄存器 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2fcff08050ef4f9fa0288a29626db0bf.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ5OTc5MDUz,size_16,color_FFFFFF,t_70) 可以看到DR寄存器只有[8:0]位可以使用,第8位用于奇偶校验,也就是DR寄存器一次只能接受8bit既1字节的数据。 ## 不太恰当的比方 打个比方就是一个篮子 **(DR寄存器)** 只能装8 **(bit)** 个物品, 我们用这个篮子把水果放到我们的仓库 **(MCU)** 中, 别人把物品一个一个放入篮子里,装满8个我们就把这篮子东西放到仓库里。 但是我们觉得这样放太乱了,就在仓库里面划了一片地,在上面贴了一个苹果标签,以后接收到的东西就放这里。 但是仓库里又不只是放苹果,而且我们也不能确定装进我篮子里的是不是苹果,也就是说我们单次接受数据的时候基本没有办法判断数据的可靠性,而且数据也是单一的。 那这样吧,我们定个规矩,你往我篮子里放了3个苹果5个橘子就代表你要开始发送有用数据了, 第二篮子你就放苹果, 第三篮子你就放橘子, 第四篮子如果放的是3个橘子5个苹果,那这次接收就结束了, 然后我也不用检查哪一个篮子是苹果哪一个是橘子, 直接就可以把第二篮放到苹果的位置,第三篮放到橘子的位置。 打的比方有些不太恰当,但基本就是这么个意思,这样我们就可以一帧接收多样的数据,通过确定开始和结束的协议也提高数据的可靠性。 但是问题又来了,我们只有一个篮子,一下子接收不了那么多篮子的数据, 那怎么办,我们在仓库立划个缓存区用于存储别人发送的数据,也就是我们把这次的数据接收完再做处理,但是这个缓冲区一般都是使用数组定义,也就是要事先规定好,你发给我8个篮子的东西,我就划8个篮子的地方,一旦开始接收数据,我事先划的地放大小就不能改了,因为数组定义的时候要事先给定长度。 呐有没有可以边接收边划分空间的方法呢,最近也在看链表的相关知识于是便想到将链表用于数据缓冲区,这样就可以边接受数据边开辟空间了。 # 数据缓冲链表结构 ```c struct Frame { u8 data; //数据域 struct Frame *next; //指针域指向下一个节点 }; ``` 我使用的链表比较简单,一是用于节省空间,另一方面自己会的也不多。 数据域就用于存储串口传来的8bit数据, 指针域就用于指向下一个节点,把两个节点联系起来,最后一个节点指向NULL代表链表结尾。 基本结构就是这样: ![在这里插入图片描述](https://img-blog.csdnimg.cn/5add460a951f40329e46f01cd5760e6d.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ5OTc5MDUz,size_16,color_FFFFFF,t_70) 头结点的作用是用于指向下一个数据,data存储一帧的节点数量。 比如一帧的数据为 :A5 34 56 5A 那么:Head->data=4,也就是除去头结点一共接收了4个节点 **一定要注意 !!不用的指针一定要指向NULL,防止产生野指针造成内存泄漏**。 那么我们定义一个全局的头指针就可以在程序了任何地方使用了 ```c struct Frame *Head=NULL; //头指针 extern struct Frame *Head;//声明全局变量 ``` 这个头指针现在是没有存储空间的因为它现在只是个地址信息 ```c /** ********************************** * 函数名: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就可以存储节点长度了。 然后我们就可以在串口中断里写我们规定的协议了 ```c #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); } } } ``` 协议定的比较简单,看一下注释基本就明白了 链表的插入方法采用的尾插法,其实头插法更快一些,不需要轮训数据,但是我感觉这样处理数据不太舒服,于是便写成了尾插法。 ```c /** ********************************** * 函数名: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; } ``` 一帧数据接受完之后一定要及时处理释放内存空间,不然会造成内存溢出,程序跑飞。 写到这里我又遇到一个问题,我接受的数据是不定长的,那么我存储起来也要是不定长的 但是我当时还没有好的思路,于是写了下面这坨代码 ```c /** /** ********************************** * 函数名: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** 所指向的空间 下次帧数据来了以后再释放空间就可以了,重新更新数据长度就可以了 修改后的代码如下: ```c 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; } } ``` 这片空间的用法和指向数组的指针的用法相同, 通过下面这个代码就可以读取数据帧任意一点的数据了 ```c /** ********************************** * 函数名: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的,其他类型的融合思路基本相似 ```c /** ********************************** * 函数名: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就可以接受到数据帧并打印出来。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/81ad68a93dc24bd4aa8612a964cf3ab3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p-05oOg5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16) 可以看出我们发送数据帧长度变化的时候,STM32同样可以接收该长度的数据帧,完全不用修改程序,非常的方便,而且也是用多少拿多少,非常的人性化。 文件放在下面了,芯片类型为**F103VCT6** # [工程文件](https://pan.baidu.com/s/1qWEO2PMicaHgTbX2Nx7YGg) **提取码:qqy7**