环形缓冲区 之 STM32 串口接收的实现
STM32串口数据接收环形缓冲区接收实例说明 ...... 矜辰所致
前言
关于环形缓冲区,网上有大量的理论说明文章,在有些操作系统中,会有实现环形缓冲区的代码,比如 RT-Thread 的 ringbuffer.c
和 ringbuffer.h
文件,Linux 内核中的 kfifo.c
和 kfifo.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 该怎么处理,如果有机会用到,我也会来记录说明。
好了,本文就到这里,谢谢大家!