前言

大家好,这里是 Hello_Embed。在处理按键输入时,我们先后经历了 “轮询法” 和 “中断法”:轮询法在执行耗时操作(如 LCD 显示)时会丢失按键;中断法虽能及时响应,但全局变量存储按键值时,新值可能覆盖旧值,仍会导致丢失。本篇解决这一问题,我们需要一种专门专门在 “写入快、读取慢” 场景下可靠存储数据的结构 ——环形缓冲区(Ring Buffer)。本篇将详解环形缓冲区的原理与实现,为下一篇 “按键按键数据防丢失” 实战打基础。

一、为什么需要环形缓冲区?

先看两个典型问题:

  1. 轮询法的缺陷:当 CPU 执行耗时操作(如 LCD 刷新)时,若此时按下按键,轮询及时读取,导致按键丢失;
  2. 中断法的隐患:用全局变量存储按键值时,若短时间内多次按键,新值会覆盖旧值(如第一次处理完第一次按键,第二次按键被触发)。
    核心需求:需要一块 “临时存储空间”,让中断程序快速写入按键数据,主程序再 “慢悠悠” 地读取处理,且数据不会因新写入而丢失 —— 环形缓冲区正是为此设计的。
二、环形缓冲区的基本概念

环形缓冲区本质是一块固定大小的一维数组,通过 “读写指针” 的循环移动模拟 “环形” 结构,适合 “一边写入、一边读取” 的场景(如中断写、主程序读)。

1. 结构示意图

请添加图片描述

  • 数组:实际存储数据的空间(如buf[8],容量为 8);
  • 读指针(r):标记下一次读取数据的位置;
  • 写指针(w):标记下一次写入数据的位置;
  • “环形” 特性:当指针到达数组末尾时,自动绕回开头(如w=8时重置为0)。
2. 核心逻辑
  • 空缓冲区r == w(读写指针位置相同,无数据);
  • 满缓冲区:下一次写指针位置next_w == r(避免与 “空” 状态冲突,预留一个空位);
  • 写入数据:若未满,将数据存入buf[w]w移至下一位(绕回);
  • 读取数据:若未空,从buf[r]读取数据,r移至下一位(绕回)。
三、环形缓冲区的实现(库函数封装)

为提高复用性,我们将环形缓冲区封装为独立的库函数(circle_buffer.ccircle_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(预留一个空位,避免与 “空” 状态混淆)。
  • 指针绕回:当rw等于len(数组长度)时,重置为0,模拟环形结构。
四、库函数的使用准备
  1. 文件组织
    在工程目录新建circle_buffer文件夹,放入circle_buffer.ccircle_buffer.h
  2. Keil 配置
    新建 “CircleBuffer” 组,添加circle_buffer.c
    在工程选项中,将circle_buffer文件夹路径添加至 “Include Paths”(确保头文件可被引用)。
总结

环形缓冲区通过固定大小的数组和循环指针,高效解决了 “一边写入、一边读取” 场景下的数据丢失问题。其核心优势在于:

  • 无数据覆盖:写入时若未满,新数据不会覆盖旧数据;
  • 高效循环:指针绕回操作无需移动数组元素,时间复杂度为 O (1);
  • 易于复用:封装为库函数后,可用于按键、串口、传感器等多种数据存储场景。
结尾

本文详解了环形缓冲区的原理与实现,下一篇我们将把它应用到按键处理中 —— 在中断中写入按键数据,在主程序中读取处理,彻底解决按键丢失问题。
Hello_Embed 继续带你从理论到实战,掌握 STM32 中高效的数据处理技巧,敬请期待~请添加图片描述

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐