STM32HAL 快速入门(十四):环形缓冲区 —— 解决数据丢失问题的高效存储结构
·
前言
大家好,这里是 Hello_Embed。在处理按键输入时,我们先后经历了 “轮询法” 和 “中断法”:轮询法在执行耗时操作(如 LCD 显示)时会丢失按键;中断法虽能及时响应,但全局变量存储按键值时,新值可能覆盖旧值,仍会导致丢失。本篇解决这一问题,我们需要一种专门专门在 “写入快、读取慢” 场景下可靠存储数据的结构 ——环形缓冲区(Ring Buffer)。本篇将详解环形缓冲区的原理与实现,为下一篇 “按键按键数据防丢失” 实战打基础。
一、为什么需要环形缓冲区?
先看两个典型问题:
- 轮询法的缺陷:当 CPU 执行耗时操作(如 LCD 刷新)时,若此时按下按键,轮询及时读取,导致按键丢失;
- 中断法的隐患:用全局变量存储按键值时,若短时间内多次按键,新值会覆盖旧值(如第一次处理完第一次按键,第二次按键被触发)。
核心需求:需要一块 “临时存储空间”,让中断程序快速写入按键数据,主程序再 “慢悠悠” 地读取处理,且数据不会因新写入而丢失 —— 环形缓冲区正是为此设计的。
二、环形缓冲区的基本概念
环形缓冲区本质是一块固定大小的一维数组,通过 “读写指针” 的循环移动模拟 “环形” 结构,适合 “一边写入、一边读取” 的场景(如中断写、主程序读)。
1. 结构示意图

- 数组:实际存储数据的空间(如
buf[8],容量为 8); - 读指针(r):标记下一次读取数据的位置;
- 写指针(w):标记下一次写入数据的位置;
- “环形” 特性:当指针到达数组末尾时,自动绕回开头(如
w=8时重置为0)。
2. 核心逻辑
- 空缓冲区:
r == w(读写指针位置相同,无数据); - 满缓冲区:下一次写指针位置
next_w == r(避免与 “空” 状态冲突,预留一个空位); - 写入数据:若未满,将数据存入
buf[w],w移至下一位(绕回); - 读取数据:若未空,从
buf[r]读取数据,r移至下一位(绕回)。
三、环形缓冲区的实现(库函数封装)
为提高复用性,我们将环形缓冲区封装为独立的库函数(circle_buffer.c和circle_buffer.h),包含初始化、读、写等操作。
1. 头文件(circle_buffer.h)
定义结构体和函数声明,结构体包含缓冲区核心信息:
#ifndef _CIRCLE_BUFFER_H
#define _CIRCLE_BUFFER_H
#include <stdint.h>
// 环形缓冲区结构体
typedef struct circle_buf{
uint32_t r; // 读指针(下一次读取位置)
uint32_t w; // 写指针(下一次写入位置)
uint32_t len; // 缓冲区长度(数组大小)
uint8_t *buf; // 指向实际存储数据的数组
}circle_buf, *p_circle_buf;
// 初始化环形缓冲区
void circle_buf_init(p_circle_buf pCircleBuf, uint32_t len, uint8_t *buf);
// 从缓冲区读取数据(成功返回0,失败返回-1)
int circle_buf_read(p_circle_buf pCircleBuf, uint8_t *pVal);
// 向缓冲区写入数据(成功返回0,失败返回-1)
int circle_buf_write(p_circle_buf pCircleBuf, uint8_t Val);
#endif
2. 源文件(circle_buffer.c)
实现初始化、读、写函数,核心是指针的循环移动和空满判断:
#include <stdint.h>
#include "circle_buffer.h"
// 初始化:设置读写指针为0,绑定数组和长度
void circle_buf_init(p_circle_buf pCircleBuf, uint32_t len, uint8_t *buf)
{
pCircleBuf->r = 0; // 读指针初始位置
pCircleBuf->w = 0; // 写指针初始位置
pCircleBuf->len = len; // 缓冲区长度(数组大小)
pCircleBuf->buf = buf; // 绑定存储数组
}
// 读取数据:若缓冲区非空,读取r位置数据,r指针后移(绕回)
int circle_buf_read(p_circle_buf pCircleBuf, uint8_t *pVal)
{
// 判断缓冲区是否为空(r == w)
if (pCircleBuf->r != pCircleBuf->w)
{
*pVal = pCircleBuf->buf[pCircleBuf->r]; // 读取数据
pCircleBuf->r++; // 读指针后移
// 若读指针到达末尾,绕回开头
if (pCircleBuf->r == pCircleBuf->len)
pCircleBuf->r = 0;
return 0; // 读取成功
}
else
{
return -1; // 缓冲区为空,读取失败
}
}
// 写入数据:若缓冲区未满,写入w位置数据,w指针后移(绕回)
int circle_buf_write(p_circle_buf pCircleBuf, uint8_t Val)
{
uint32_t next_w; // 下一次写指针位置
// 计算下一次写指针(若当前w在末尾,绕回0)
next_w = pCircleBuf->w + 1;
if (next_w == pCircleBuf->len)
next_w = 0;
// 判断缓冲区是否未满(next_w != r)
if (next_w != pCircleBuf->r)
{
pCircleBuf->buf[pCircleBuf->w] = Val; // 写入数据
pCircleBuf->w = next_w; // 写指针移至下一位
return 0; // 写入成功
}
else
{
return -1; // 缓冲区已满,写入失败
}
}
3. 关键逻辑解析
- 空满判断:
- 空:
r == w(无数据可读); - 满:
next_w == r(预留一个空位,避免与 “空” 状态混淆)。
- 空:
- 指针绕回:当
r或w等于len(数组长度)时,重置为0,模拟环形结构。
四、库函数的使用准备
- 文件组织:
在工程目录新建circle_buffer文件夹,放入circle_buffer.c和circle_buffer.h; - Keil 配置:
新建 “CircleBuffer” 组,添加circle_buffer.c;
在工程选项中,将circle_buffer文件夹路径添加至 “Include Paths”(确保头文件可被引用)。
总结
环形缓冲区通过固定大小的数组和循环指针,高效解决了 “一边写入、一边读取” 场景下的数据丢失问题。其核心优势在于:
- 无数据覆盖:写入时若未满,新数据不会覆盖旧数据;
- 高效循环:指针绕回操作无需移动数组元素,时间复杂度为 O (1);
- 易于复用:封装为库函数后,可用于按键、串口、传感器等多种数据存储场景。
结尾
本文详解了环形缓冲区的原理与实现,下一篇我们将把它应用到按键处理中 —— 在中断中写入按键数据,在主程序中读取处理,彻底解决按键丢失问题。
Hello_Embed 继续带你从理论到实战,掌握 STM32 中高效的数据处理技巧,敬请期待~
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)