STM32串口通信 链表接收不定长数据帧

数据帧说明

STM32数据寄存器为USARTx->DR寄存器
STM32串口通信 链表接收不定长数据帧
可以看到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代表链表结尾。

基本结构就是这样:
STM32串口通信 链表接收不定长数据帧
头结点的作用是用于指向下一个数据,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串口通信 链表接收不定长数据帧
可以看出我们发送数据帧长度变化的时候,STM32同样可以接收该长度的数据帧,完全不用修改程序,非常的方便,而且也是用多少拿多少,非常的人性化。

文件放在下面了,芯片类型为F103VCT6

工程文件

提取码:qqy7

上一篇:OpenCV从摄像头读取视频


下一篇:分布式事务之servicecomb-pack