环形缓冲区 之 STM32 串口接收的实现

STM32串口数据接收环形缓冲区接收实例说明     ...... 矜辰所致

前言

关于环形缓冲区,网上有大量的理论说明文章,在有些操作系统中,会有实现环形缓冲区的代码,比如 RT-Thread 的 ringbuffer.cringbuffer.h 文件,Linux 内核中的 kfifo.ckfifo.h 文件。

环形缓冲区使用于多种场景,对于单片机领域典型的场合就是串口通讯的实现。在我们使用单片机的时候,如果没有用到操作系统,我们如何使用环形缓冲区来实现串口接收呢?

那么本文我们以 STM32 为例,来说明一下如何使用环形缓冲区实现 STM32 的串口数据接收和数据处理。

我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

目录

  • 前言
  • 一、 基础介绍
  • 二、 实现代码
  • 三、 实际使用
  • 3.1 标准库
  • 3.2 HAL库
  • 3.3 数据处理的细节说明
  • 结语
  • 一、 基础介绍

    虽然本文并不会深入的解析环形缓冲区的原理(大家可以自行查看网上理论文章),但是我们有必要简要说明一下环形缓冲区的工作模型,以及工作流程。

    因为芯片的内存空间是线性连续的,不可能构成实际上的环形。所谓环形缓冲区,是开辟一段连续的内存空间,通过特别的设计逻辑,使得这段内存空间使用起来像是环形的。

    我们来设计的时候,会开辟一段内存空间,比如单片机上定义一个数组。然后定义一个写指针,和一个读指针。

    如果有数据写入,把数据放到写指针指向的地址,写指针递增。当开辟的最后一个地址写完以后,写指针指向起始位置。

    读取的时候,先判断缓冲区内确实有数据存在,然后从读指针的地址开始读取数据,读指针递增。

    简单的流程图如下:

    上面是一个简单的示意图,我们在实现的时候,有几个问题需要考虑一下:

  • 合理的环形缓冲区大小,太小的话,容易造成数据丢失或者数据阻塞,太大的话需要占用更多的内存空间,在一些 Flash 比较小的 MCU 上,需要合理控制;
  • 上图中最后文字提到的,数据满了以后,是覆盖,还是阻塞,这要看自己的应用场景通过自己的程序控制;
  • 我们还需要能够随时判断缓冲区是否为空,是否已满,这样才能有利于我们的数据处理 。
  • 简单介绍这么多,下面会出实现代码,然后进行实际使用说明。

    二、 实现代码

    不墨迹,这里直接给出一个可以使用的环形缓冲区的驱动代码,方便以后用到时候直接复制,这也是很早以前网上下载的,都忘了从哪里弄下来的= =! 当然环形缓冲区网上的代码也有不同版本,但是大体上的本质都大差不差。

    首先是 .c 文件,里面实现了 环形缓冲区初始化,判断缓冲区是否是空/满,写环形缓冲区,读取环形缓冲区 等函数。

    ringbuff.c

    里面实现了,环形缓冲区的初始化,

    #include "ringbuff.h"
    #include "stdio.h"
    #include <string.h>
    
    
    void RingBuff_Init(RingBuff_t *rb) //初始化函数
    {
      rb->Head = 0; //头指针置于起始位
      rb->Tail = 0; //尾指针置于起始位
      rb->Length = 0; //计录当前数据长度 判断是否存有数据
    	//   HAL_UART_Receive_IT(&hlpuart1, &data_tmp, 1); // 开启串口接收中断
    }
    
    /**
     * @brief  判断队列是否为空
     * @note   
     * @param  *rb: 结构体指针
     * @retval 返回0和1,1代表空,0代表非空
     */
    te_cicrleQueueStatus_t RingBuff_IsEmpty(RingBuff_t *rb)
    {
      return (rb->Head == rb->Tail) ? CQ_STATUS_IS_EMPTY : CQ_STATUS_OK;
    }
    /**
     * @brief  判断队列是否为满
     * @note   
     * @param  *rb: 结构体指针
     * @retval 返回0和1,1代表满,0代表非满
     */
    te_cicrleQueueStatus_t RingBuff_IsFull(RingBuff_t *rb)
    {
      return ((rb->Tail + 1) % BUFFER_SIZE == rb->Head) ? CQ_STATUS_IS_FULL : CQ_STATUS_OK;
    }
    
    /**
      *功能:数据写入环形缓冲区
      *入参1:要写入的数据
      *入参2:buffer指针
      *返回值:buffer是否已满
      */
    uint8_t Write_RingBuff(RingBuff_t *ringBuff , uint8_t data)
    {
    	if(ringBuff->Length >= BUFFER_SIZE) //判断缓冲区是否已满
    	{
    		//如果buffer爆掉了,清空buffer,进行重新初始化   不初始化,会复位死机
    		// memset(ringBuff, 0, BUFFER_SIZE);
    		// RingBuff_Init(&ringBuff);
    		return 1;
    	}
     
        //将单字节数据存入到环形buffer的tail尾部
    	ringBuff->Ring_Buff[ringBuff->Tail]=data;    
        //重新指定环形buffer的尾部地址,防止越界非法访问
    	ringBuff->Tail = ( ringBuff->Tail + 1 ) % BUFFER_SIZE;
        //存入一个字节数据成功,len加1 
    	ringBuff->Length++;    
    	
    	return 0;
    }
    
    /**
      *功能:读取缓存区整帧数据-单字节读取
      *入参1:存放提取数据的指针
      *入参2:环形区buffer指针
      *返回值:是否成功提取数据
      */
    uint8_t Read_RingBuff_Byte(RingBuff_t *ringBuff , uint8_t *rData)
    {
    	if(ringBuff->Length == 0)//判断非空
    	{
    		return 1;
    	}
    		
        //先进先出FIFO,从缓冲区头出,将头位置数据取出
    	*rData = ringBuff->Ring_Buff[ringBuff->Head];
        //将取出数据的位置,数据清零
    	ringBuff->Ring_Buff[ringBuff->Head] = 0;
    				
    	//重新指定buffer头的位置,防止越界非法访问
    	ringBuff->Head = (ringBuff->Head + 1) % BUFFER_SIZE;
        //取出一个字节数据后,将数据长度减1
    	ringBuff->Length--;
    	
    	return 0;
    }
    
    
    /*
    从环形缓冲区读多个字节
    */
    te_cicrleQueueStatus_t RingBuff_ReadNByte(RingBuff_t *pRingBuff, uint8_t *pData, int size)
    {
    	int i = 0;
    	if(NULL == pRingBuff || NULL == pData)
    		return CQ_STATUS_ERR;
    
    	for( i = 0; i < size; i++)
    	{
    		Read_RingBuff_Byte(pRingBuff, pData+i);
    	}
    	return CQ_STATUS_OK;
    }
    
    //向环形缓冲区写多个字节
    te_cicrleQueueStatus_t RingBuff_WriteNByte(RingBuff_t *pRingBuff, uint8_t *pData, int size)
    {
    	int i = 0;
    	if(NULL == pRingBuff || NULL == pData)
    		return CQ_STATUS_ERR;
    
    	for(i = 0; i < size; i++)
    	{
    		Write_RingBuff(pRingBuff, *(pData+i));
    	}
    	return CQ_STATUS_OK;
    }
    
    
    //获取当前环形缓冲区中数据长度
    int RingBuff_GetLen(RingBuff_t *pRingBuff)
    {
    	if(NULL == pRingBuff)
    		return 0;
    
    	if(pRingBuff->Tail >= pRingBuff->Head)
    	{
    		return pRingBuff->Tail - pRingBuff->Head;
    	}
    	
    	return pRingBuff->Tail + BUFFER_SIZE - pRingBuff->Head;
    }
    
    uint16_t RQBuff_GetBuffLenth(RingBuff_t* RQ_Buff) {
    	return RQ_Buff->Length;
    }
    
    //获取当前头部数据
    unsigned char RingBuff_GetHeadItem(RingBuff_t *pRingBuff)
    {
    	if(NULL == pRingBuff)
    		return CQ_STATUS_ERR;
    	
    	return pRingBuff->Ring_Buff[pRingBuff->Head];
    }
    
    //获取指定下标数据
    unsigned char RingBuff_GetIndexItem(RingBuff_t *pRingBuff, int index)
    {
    	if(NULL == pRingBuff || index > BUFFER_SIZE-1)
    		return CQ_STATUS_ERR;
    
    	return pRingBuff->Ring_Buff[index%BUFFER_SIZE];
    }
    
    

    ringbuff.h

    #ifndef _RINGBUFF_H_INCLUDED
    #define _RINGBUFF_H_INCLUDED
    
    #include "main.h"
    #include "Datadef.h"
    #include "stdio.h"
    #include <string.h>
    #include "usart.h"
    
    #define USART_BUFF_MAX 1024 
    #define BUFFER_SIZE 1024        /* 环形缓冲区的大小 */
    
    typedef enum {
      CQ_STATUS_OK = 0,
      CQ_STATUS_IS_FULL,
      CQ_STATUS_IS_EMPTY,
      CQ_STATUS_ERR    // 出错
    } te_cicrleQueueStatus_t;
    
    
    typedef struct
    {
    	uint32_t Head;
    	uint32_t Tail;
    	uint32_t Length;
    	uint8_t Ring_Buff[BUFFER_SIZE];
    } RingBuff_t;
    
    
    extern RingBuff_t  enoceanbuff;
    
    
    te_cicrleQueueStatus_t RingBuff_IsEmpty(RingBuff_t *rb);
    
    te_cicrleQueueStatus_t RingBuff_IsFull(RingBuff_t *rb);
    
    te_cicrleQueueStatus_t RingBuff_ReadNByte(RingBuff_t *pRingBuff, uint8_t *pData, int size);
    
    te_cicrleQueueStatus_t RingBuff_WriteNByte(RingBuff_t *pRingBuff, uint8_t *pData, int size);
    
    int RingBuff_GetLen(RingBuff_t *pRingBuff);
    
    unsigned char RingBuff_GetIndexItem(RingBuff_t *pRingBuff, int index);
    
    uint8_t Write_RingBuff(RingBuff_t *ringBuff , volatile uint8_t data);
    uint8_t Read_RingBuff_Byte(RingBuff_t *ringBuff , uint8_t *rData);
    void RingBuff_Init(RingBuff_t *rb);
    
    uint16_t RQBuff_GetBuffLenth(RingBuff_t* RQ_Buff);
    
    #endif //_MOD_BUTTON_H_INCLUDED
    
    

    这里两个文件使用的时候可以直接放到工程里面,比如下面两个工程:

    三、 实际使用

    上面我们给出了程序源码,然后根据自己的工程框架放到自己工程下面,接下来我们就来实际说明一下怎么使用。

    我们这里把标准库 和 HAL 库的串口数据处理都说明一下。

    3.1 标准库

    首先我们需要定义一个环形缓冲区,在我们的 STM32 上也就是顶一个结构体变量。 在我们上面的 ringbuff.h 文件中有一个名为 RingBuff_t 的结构体,我们需要定义一下:

    我们可以通过定义 BUFFER_SIZE 来定义自己的缓冲区大小。

    #define BUFFER_SIZE 1024        /* 环形缓冲区的大小 */
    

    然后在程序初始化阶段,使用 RingBuff_Init 初始化一下这个变量,如下图:

    数据写入:

    好,对于标准库而言,我们串口接收数据一般在串口中断中实现,我们实现的方式如下:

    以前我们每次收到数据或许也会放到自己定义的缓冲区中,使用了环形缓冲区,我们直接使用Write_RingBuff 函数即可。

    数据读取:

    那么怎么读取数据呢?

    我们在环形缓冲区实现代码里面有Read_RingBuff_Byte 读取数据的函数,读取的关键在于什么时候读!

    有一种通用的方式,就是隔一段时间检查一下环形缓冲区是否为空,如果不为空,就进行读取。这个可以放在 while 循环中进行,我这里给个例子:

    当然,除了这种等待一段时间让数据接收完成的方式,对于 STM32 而言,还有利用空闲中断(IDLE)的方式,具体实现如下:

    当然,在标准库中使用 IDLE 中断,记得使能一下中断,如下图:

    在程序中,如果需要清空缓存,也是直接使用 RingBuff_Init 即可。实际上都不用清空缓冲区内的数据,即便其他位置有数据也没有关系,因为我们判断是否有数据都是先通过指针位置来判断,才进行读写操作,所以内存中即便有数据,也会被覆盖。

    3.2 HAL库

    接下来说说 HAL 库,其实操作逻辑也是一样的,只不过 HAL 库开启接收中断的时候需要使用 HAL_UART_Receive_IT 函数 。

    第一步,当然是定义 RingBuff_t 结构体变量,初始化,流程和标准库一样,只不过初始化函数需要多加上一句话,使用 HAL_UART_Receive_IT 开启接收中断,而且还需要额外定义一个变量,用来配合这个函数 ,具体的如下图:

    使用上面 RingBuff_Init 完成初始化。

    数据写入:

    数据的接收也是在串口中断的时候进行,在 HAL 库中操作如下:

    因为定义了一个变量 data_tmp 用来存放接收到的串口数据,而且每次接收完以后,接收到的数据又存放于 data_tmp

    数据读取:

    至于数据读取, HAL 的方式完全和 标准库一样,好像没有什么特别需要注意的,可以循环中判断缓冲区非空,进行读取数据,也可以通过 IDLE 中断,进行读取数据然后处理。

    3.3 数据处理的细节说明

    上面我们已经掌握了如何存环形缓冲区,什么时候取数据,怎么取数据,这里再补充一下数据处理中的一个问题。

    因为我们处理数据都是一帧一帧的,我们不管接受到正确的数据,还是不需要的数据,我们都需要把一帧数据读取完。 否者会影响下一帧数据的处理。 如果不用环形缓冲区,用最简单粗暴的方法就是一旦检测到错误的数据,直接清空缓冲区,但是这在数据量大的时候很容易会导致丢包的情况。我们采用环形缓冲区就是为了尽可能的避免丢包。

    所以呢即便我们收到不需要的数据,我们也需要把这个错误的数据读取完毕。

    我们接受的数据协议,一般在包头后面都会跟上本条数据的长度,所以,我们可以根据读到的长度信息,使用RingBuff_ReadNByte 把剩余的数据读取完毕。

    比如:

    知道如何处理不需要的数据,基本上环形缓冲区的使用也没有什么问题了。

    结语

    本文详细的把 STM32 标准库和 HAL 库如何使用环形缓冲区说明了一遍,相信大家在实际应用中都能够知道怎么使用。

    当然上面的都是使用裸机的示例,对于使用 RTOS 该怎么处理,如果有机会用到,我也会来记录说明。

    好了,本文就到这里,谢谢大家!

    作者:矜辰所致

    物联沃分享整理
    物联沃-IOTWORD物联网 » 环形缓冲区 之 STM32 串口接收的实现

    发表回复