环形缓冲区 之 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;//取出一个字节数据后,将数据长度减1ringBuff->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.rhkb.cn/news/474589.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Matplotlib | 理解直方图中bins表示的数据含义

引出问题 hist作图中 bins 发生变换 y轴的数据也变化 想不通 不是说y轴计算的是频率吗 频率既然是定值 为什么y轴的数据就还会变化&#xff1f;那我确定了bins的数值 我想获得bins内各个柱子&#xff08;bin&#xff09;中数据点的数量如何获得 bins的变化 先看一组数据 da…

【Linux】基础02

Linux编译和调试 VI编辑文件 vi : 进入文件编辑 是命令行模式 i &#xff1a;从光标处进入插入模式 dd : 删除光标所在行 n dd 删除指定行数 Esc &#xff1a; 退出插入模式 &#xff1a; 冒号进入末行模式 :wq : 保存退出 :q &#xff1a; 未修改文件可以退出 :q! …

Linux网络——套接字编程

1. 网络通信基本脉络 基本脉络图如上&#xff0c;其中数据在不同层的叫法不一样&#xff0c;比如在传输层时称为数据段&#xff0c;而在网络层时称为数据报。我们可以在 Linux 中使用 ifconfig 查看网络的配置&#xff0c;如图 其中&#xff0c;inet 表示的是 IPv4&#xff0c;…

深度学习的实践层面

深度学习的实践层面 设计机器学习应用 在训练神经网络时&#xff0c;超参数选择是一个高度迭代的过程。我们通常从一个初步的模型框架开始&#xff0c;进行编码、运行和测试&#xff0c;通过不断调整优化模型。 数据集一般划分为三部分&#xff1a;训练集、验证集和测试集。常…

Jmeter 如何导入证书并调用https请求

Jmeter 如何导入证书并调用https请求 通过SSL管理器添加证书文件 支持添加的文件为.p12&#xff0c;.pfx&#xff0c;.jks 如何将pem文件转换为pfx文件&#xff1f; 在公司内部通常会提供3个pem文件。 ca.pem&#xff1a;可以理解为是根证书&#xff0c;用于验证颁发的证…

LabVIEW 温湿度测试与监控系统

煤炭自燃是煤矿和煤炭储存领域面临的重大安全隐患&#xff0c;尤其是在煤炭堆积和运输过程中&#xff0c;温湿度变化会直接影响煤体的氧化速率和自燃倾向。传统的监测手段通常存在实时性差、数据处理复杂等问题&#xff0c;难以准确评估煤自燃的风险。因此&#xff0c;设计了一…

IDEA 开发工具常用快捷键有哪些?

‌在IDEA中&#xff0c;输出System.out.println()的快捷键是sout&#xff0c;输入后按回车&#xff08;或Tab键&#xff09;即可自动补全为System.out.println()‌‌。 此外&#xff0c;IDEA中还有一些其他常用的快捷键&#xff1a; 创建main方法的快捷键是psvm&#xff0c;代…

KF UKF

我需要Kalman 现在&#xff0c;主要是用来处理检测问题情况里的漏检&#xff0c;因为模拟了一段2D&#xff0c; &#xff08;x&#xff0c;y&#xff09;的数据&#xff0c;为了看效果&#xff0c;画的线尽量简单一点&#xff1a; import numpy as np import matplotlib.pyplo…

多品牌摄像机视频平台EasyCVR视频融合平台+应急布控球:打造城市安全监控新体系

在当今快速发展的智慧城市和数字化转型浪潮中&#xff0c;视频监控技术已成为提升公共安全、优化城市管理、增强应急响应能力的重要工具。EasyCVR视频监控平台以其强大的多协议接入能力和多样化的视频流格式分发功能&#xff0c;为用户提供了一个全面、灵活、高效的视频监控解决…

第8章硬件维护-8.2 可维护性和可靠性验收

8.2 可维护性和可靠性验收 可维护性和可靠性验收非常重要&#xff0c;硬件维护工程师在后端发现问题后&#xff0c;总结成可维护性和可靠性需求&#xff0c;在产品立项的时候与新特性一起进行需求分析&#xff0c;然后经过设计、开发和测试环节&#xff0c;在产品中落地。这些需…

医学图像语义分割:前列腺肿瘤、颅脑肿瘤、腹部多脏器 MRI、肝脏 CT、3D肝脏、心室

医学图像语义分割&#xff1a;前列腺肿瘤、颅脑肿瘤、腹部多脏器 MRI、肝脏 CT、3D肝脏、心室 语义分割网络FCN&#xff1a;通过将全连接层替换为卷积层并使用反卷积上采样&#xff0c;实现了第一个端到端的像素级分割网络U-Net&#xff1a;采用对称的U形编解码器结构&#xff…

如何解决多系统数据重复与冲突问题?

多系统并行运作已成为现代企业的常态。企业通常同时使用ERP、CRM、HR等多个业务系统来管理不同的功能模块。然而&#xff0c;这种多系统环境也带来了一个常见且棘手的问题&#xff1a;数据重复与矛盾。由于各系统独立运行且缺乏有效的集成机制&#xff0c;不同系统间的数据容易…

麒麟时间同步搭建chrony服务器

搭建chrony服务器 在本例中&#xff0c;kyserver01&#xff08;172.16.200.10&#xff09;作为客户端&#xff0c;同步服务端时间&#xff1b;kyserver02&#xff08;172.16.200.11&#xff09;作为服务端&#xff0c;提供时间同步服务。 配置服务端&#xff0c;修改以下内容…

【GPTs】Ai-Ming:AI命理助手,个人运势与未来发展剖析

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | GPTs应用实例 文章目录 &#x1f4af;GPTs指令&#x1f4af;前言&#x1f4af;Ai-Ming主要功能适用场景优点缺点 &#x1f4af;小结 &#x1f4af;GPTs指令 中文翻译&#xff1a; defcomplete_sexagenary&#xff08;年&a…

Chainlit快速实现AI对话应用将聊天记录的持久化到MySql关系数据库中

概述 默认情况下&#xff0c;Chainlit 应用不会保留其生成的聊天和元素。即网页一刷新&#xff0c;所有的聊天记录&#xff0c;页面上的所有聊天记录都会消失。但是&#xff0c;存储和利用这些数据的能力可能是您的项目或组织的重要组成部分。 之前写过一篇文章《Chainlit快速…

【动手学深度学习Pytorch】6. LeNet实现代码

LeNet&#xff08;LeNet-5&#xff09;由两个部分组成&#xff1a;卷积编码器和全连接层密集块 x.view(): 对tensor进行reshape import torch from torch import nn from d2l import torch as d2lclass Reshape(torch.nn.Module):def forward(self, x):return x.view(-1, 1, 28…

AI工具百宝箱|任意选择与Chatgpt、gemini、Claude等主流模型聊天的Anychat,等你来体验!

文章推荐 AI工具百宝箱&#xff5c;使用Deep Live Cam&#xff0c;上传一张照片就可以实现实时视频换脸...简直太逆天&#xff01; Anychat 这是一款可以与任何模型聊天 &#xff08;chatgpt、gemini、perplexity、claude、metal llama、grok 等&#xff09;的应用。 在页面…

Excel数据动态获取与映射

处理代码 动态映射 动态读取 excel 中的数据&#xff0c;并通过 json 配置 指定对应列的值映射到模板中的什么字段上 private void GetFreightFeeByExcel(string filePath) {// 文件名需要以快递公司命名 便于映射查询string fileName Path.GetFileNameWithoutExtension(fi…

SRP 实现 Cook-Torrance BRDF

写的很乱&#xff01; BRDF&#xff08;Bidirectional Reflectance Distribution Function&#xff09;全称双向反射分布函数。辐射量单位非常多&#xff0c;这里为方便直观理解&#xff0c;会用非常不严谨的光照强度来解释说明。 BRDF光照模型&#xff0c;上反射率公式&#…

[代码随想录Day16打卡] 找树左下角的值 路径总和 从中序与后序遍历序列构造二叉树

找树左下角的值 定义&#xff1a;二叉树中最后一行最靠左侧的值。 前序&#xff0c;中序&#xff0c;后序遍历都是先遍历左然后遍历右。 因为优先遍历左节点&#xff0c;所以递归中因为深度增加更新result的时候&#xff0c;更新的值是当前深度最左侧的值&#xff0c;到最后就…