当前位置: 首页 > article >正文

环形缓冲区 之 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 该怎么处理,如果有机会用到,我也会来记录说明。

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


http://www.kler.cn/a/405981.html

相关文章:

  • 实时数据研发 | Flink技术栈
  • 大模型在智能客服中心领域的应用思考
  • C#元组详解:创建、访问与解构
  • Android 网络通信(三)OkHttp实现登入
  • 游戏陪玩系统开发功能需求分析
  • 循环输出1~100之间的每个数
  • @WebService 详解
  • Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)
  • 学习笔记|MaxKB对接本地大模型时,选择Ollma还是vLLM?
  • js中new操作符具体都干了什么?
  • 为自动驾驶提供高分辨率卫星图像数据,实例级标注数据集OpenSatMap
  • 如何实现单片机的安全启动和安全固件更新
  • 达索系统亮相第三十一届中国汽车工程学会年会暨展览会
  • 【已完成】windows配置pytorch2.4.1深度学习环境
  • 商用密码应用安全性评估,密评整体方案,密评管理测评要求和指南,运维文档,软件项目安全设计相关文档合集(Word原件)
  • 玩转合宙Luat教程 基础篇④——程序基础(库、线程、定时器和订阅/发布)
  • c++ std::stack总结
  • 深入理解 prompt提示词 原理及使用技巧
  • ElasticSearch7.x入门教程之中文分词器 IK(二)
  • Python操作neo4j库py2neo使用之创建和查询(二)
  • ubuntu pytorch容器内安装gpu版本的ffmpeg
  • android studio无法下载,Could not GET xxx, Received status code 400
  • C++设计模式介绍
  • Bug:引入Feign后触发了2次、4次ContextRefreshedEvent
  • IDEA 下载源码很慢,Download Source使用阿里云镜像仓库
  • 算法编程题-排序