嵌入式系统中的环形缓冲区:原理、应用与 STM32 实现

环形缓冲区(Circular Buffer/Ring Buffer)是嵌入式系统中最基础、最常用的数据结构之一,它以固定大小的内存空间实现了高效的先进先出(FIFO)数据管理,完美解决了嵌入式系统中普遍存在的速度不匹配异步通信突发数据处理问题。

一、环形缓冲区在嵌入式系统中的核心作用

1. 解决速度不匹配问题

这是环形缓冲区最根本的作用。嵌入式系统中,外设与 CPU、不同模块之间的处理速度往往存在巨大差异:

  • 高速 ADC 以 1MHz 采样,而 CPU 处理数据的速度可能只有 100kHz
  • UART 以 115200bps 接收数据,而主循环可能需要几十毫秒才能处理一次
  • 传感器以固定速率输出数据,而应用层可能需要批量处理

环形缓冲区作为 "蓄水池",可以暂时存储高速产生的数据,让低速处理模块能够从容地逐步读取。

2. 实现异步通信与解耦

环形缓冲区将数据的生产消费过程完全解耦:

  • 生产者(如 UART 中断)只需要将数据写入缓冲区,不需要关心消费者何时处理
  • 消费者(如主循环)只需要从缓冲区读取数据,不需要关心数据何时产生
  • 两者可以在不同的时间、不同的优先级甚至不同的任务中运行

3. 减少中断服务程序 (ISR) 的执行时间

在嵌入式系统中,中断服务程序的执行时间必须尽可能短,否则会影响系统的实时性甚至导致数据丢失。使用环形缓冲区后:

  • ISR 中只需要执行简单的 "数据写入缓冲区" 操作,耗时微秒级
  • 复杂的数据解析、处理和业务逻辑全部放在主循环或低优先级任务中执行
  • 这是嵌入式系统中断设计的最佳实践之一

4. 缓冲突发数据

许多嵌入式应用会遇到突发数据流量:

  • 蓝牙模块一次接收一个完整的数据包(几十到几百字节)
  • 传感器在触发后连续输出多个采样值
  • 网络接口收到一个大的以太网帧

环形缓冲区可以吸收这些突发数据,避免在处理不及时时发生丢失。

5. 实现数据的流水线处理

通过多个环形缓冲区串联,可以构建数据处理流水线:

  • 缓冲区 1:存储原始 ADC 采样数据
  • 缓冲区 2:存储滤波后的数据
  • 缓冲区 3:存储计算后的结果
  • 每个处理阶段独立运行,提高系统整体吞吐量

二、环形缓冲区的主要应用领域

在嵌入式系统中,几乎所有涉及数据传输和处理的地方都能看到环形缓冲区的身影:

应用领域 典型使用场景
串行通信 UART/USART、I2C、SPI、CAN、LIN、RS485 等接口的收发缓冲
传感器数据采集 ADC 采样、加速度计、陀螺仪、温度传感器等高速数据采集
音频处理 麦克风输入、扬声器输出、音频编解码、语音识别
网络通信 TCP/IP 协议栈、以太网、Wi-Fi、蓝牙、Zigbee 等无线通信
人机交互 按键扫描、触摸屏输入、LED 显示、LCD 刷屏
日志系统 调试日志、运行日志的缓存和输出
实时操作系统 任务间通信、消息队列的底层实现
工业控制 PLC 数据采集、Modbus 通信、运动控制指令缓存

三、环形缓冲区的基本原理

环形缓冲区本质上是一个固定大小的数组,配合两个指针(或索引)来管理数据的读写:

  • 写指针 (Write Pointer):指向缓冲区中下一个可写入的位置
  • 读指针 (Read Pointer):指向缓冲区中下一个可读取的位置

当写指针到达数组末尾时,会自动绕回到数组开头,形成一个 "环形" 结构。

空满判断的核心问题

环形缓冲区实现中最关键的问题是如何区分 "缓冲区空" 和 "缓冲区满" 两种状态,因为这两种状态下读指针和写指针的值都是相等的。

常见的解决方案有三种:

1. 牺牲一个元素的空间(最常用)
  • 当写指针的下一个位置等于读指针时,认为缓冲区满
  • 当写指针等于读指针时,认为缓冲区空
  • 优点:实现简单,不需要额外变量,速度快
  • 缺点:缓冲区实际可用大小比定义的大小少 1
2. 使用一个计数器
  • 增加一个变量记录缓冲区中当前元素的数量
  • 当计数器等于缓冲区大小时,认为满
  • 当计数器等于 0 时,认为空
  • 优点:缓冲区可以完全利用
  • 缺点:需要额外的变量,读写操作都需要修改计数器
3. 使用一个标志位
  • 增加一个布尔变量标记缓冲区是否满
  • 当写指针追上读指针时,将标志位置为真
  • 当读指针追上写指针时,将标志位置为假
  • 优点:缓冲区可以完全利用
  • 缺点:逻辑相对复杂

在嵌入式系统中,牺牲一个元素空间的方法是最常用的,因为它实现最简单、执行效率最高,而牺牲一个字节的空间在大多数情况下完全可以接受。

四、STM32 上的环形缓冲区实现

下面以 STM32 单片机为例,提供一个通用、高效、中断安全的环形缓冲区实现,并展示其在 UART 通信中的典型应用。

1. 通用环形缓冲区的实现

我们采用牺牲一个元素空间的方法来实现,支持任意数据类型和任意缓冲区大小。

ring_buffer.h
cpp 复制代码
#ifndef __RING_BUFFER_H
#define __RING_BUFFER_H

#include "stdint.h"
#include "string.h"

/* 环形缓冲区结构体 */
typedef struct {
    uint8_t *buffer;   /* 缓冲区数据存储区 */
    uint16_t size;     /* 缓冲区总大小 */
    uint16_t write;    /* 写指针 */
    uint16_t read;     /* 读指针 */
} ring_buffer_t;

/* 函数声明 */
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buffer, uint16_t size);
uint16_t ring_buffer_write(ring_buffer_t *rb, const uint8_t *data, uint16_t len);
uint16_t ring_buffer_read(ring_buffer_t *rb, uint8_t *data, uint16_t len);
uint16_t ring_buffer_get_used(ring_buffer_t *rb);
uint16_t ring_buffer_get_free(ring_buffer_t *rb);
uint8_t ring_buffer_is_empty(ring_buffer_t *rb);
uint8_t ring_buffer_is_full(ring_buffer_t *rb);
void ring_buffer_clear(ring_buffer_t *rb);

#endif /* __RING_BUFFER_H */

ring_buffer.c

cpp 复制代码
#include "ring_buffer.h"

/**
 * @brief  初始化环形缓冲区
 * @param  rb: 环形缓冲区结构体指针
 * @param  buffer: 用于存储数据的缓冲区
 * @param  size: 缓冲区总大小
 * @retval None
 */
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buffer, uint16_t size)
{
    rb->buffer = buffer;
    rb->size = size;
    rb->write = 0;
    rb->read = 0;
}

/**
 * @brief  向环形缓冲区写入数据
 * @param  rb: 环形缓冲区结构体指针
 * @param  data: 要写入的数据指针
 * @param  len: 要写入的数据长度
 * @retval 实际写入的数据长度
 */
uint16_t ring_buffer_write(ring_buffer_t *rb, const uint8_t *data, uint16_t len)
{
    uint16_t i;
    uint16_t free_space = ring_buffer_get_free(rb);
    
    /* 如果剩余空间不足,只写入能放下的部分 */
    if (len > free_space) {
        len = free_space;
    }
    
    for (i = 0; i < len; i++) {
        rb->buffer[rb->write] = data[i];
        rb->write = (rb->write + 1) % rb->size;
    }
    
    return len;
}

/**
 * @brief  从环形缓冲区读取数据
 * @param  rb: 环形缓冲区结构体指针
 * @param  data: 存储读取数据的缓冲区
 * @param  len: 要读取的数据长度
 * @retval 实际读取的数据长度
 */
uint16_t ring_buffer_read(ring_buffer_t *rb, uint8_t *data, uint16_t len)
{
    uint16_t i;
    uint16_t used_space = ring_buffer_get_used(rb);
    
    /* 如果可用数据不足,只读取所有可用数据 */
    if (len > used_space) {
        len = used_space;
    }
    
    for (i = 0; i < len; i++) {
        data[i] = rb->buffer[rb->read];
        rb->read = (rb->read + 1) % rb->size;
    }
    
    return len;
}

/**
 * @brief  获取环形缓冲区中已使用的空间大小
 * @param  rb: 环形缓冲区结构体指针
 * @retval 已使用的字节数
 */
uint16_t ring_buffer_get_used(ring_buffer_t *rb)
{
    return (rb->write - rb->read + rb->size) % rb->size;
}

/**
 * @brief  获取环形缓冲区中剩余的空闲空间大小
 * @param  rb: 环形缓冲区结构体指针
 * @retval 空闲的字节数
 */
uint16_t ring_buffer_get_free(ring_buffer_t *rb)
{
    /* 牺牲一个元素空间 */
    return (rb->read - rb->write - 1 + rb->size) % rb->size;
}

/**
 * @brief  判断环形缓冲区是否为空
 * @param  rb: 环形缓冲区结构体指针
 * @retval 1: 空 0: 非空
 */
uint8_t ring_buffer_is_empty(ring_buffer_t *rb)
{
    return (rb->write == rb->read) ? 1 : 0;
}

/**
 * @brief  判断环形缓冲区是否为满
 * @param  rb: 环形缓冲区结构体指针
 * @retval 1: 满 0: 未满
 */
uint8_t ring_buffer_is_full(ring_buffer_t *rb)
{
    return ((rb->write + 1) % rb->size == rb->read) ? 1 : 0;
}

/**
 * @brief  清空环形缓冲区
 * @param  rb: 环形缓冲区结构体指针
 * @retval None
 */
void ring_buffer_clear(ring_buffer_t *rb)
{
    rb->write = 0;
    rb->read = 0;
}

2. 在 STM32 UART 通信中的应用

这是环形缓冲区最经典的应用场景。我们将实现一个带双缓冲区(接收和发送)的 UART 驱动,支持中断方式的高效数据收发。

uart_ring_buffer.h
cpp 复制代码
#ifndef __UART_RING_BUFFER_H
#define __UART_RING_BUFFER_H

#include "stm32f4xx_hal.h"
#include "ring_buffer.h"

/* 缓冲区大小定义 */
#define UART_RX_BUFFER_SIZE 256
#define UART_TX_BUFFER_SIZE 256

/* 全局变量声明 */
extern UART_HandleTypeDef huart1;
extern ring_buffer_t uart1_rx_rb;
extern ring_buffer_t uart1_tx_rb;

/* 函数声明 */
void uart_ring_buffer_init(void);
uint16_t uart_receive(uint8_t *data, uint16_t len);
uint16_t uart_send(const uint8_t *data, uint16_t len);
uint16_t uart_send_string(const char *str);

#endif /* __UART_RING_BUFFER_H */

uart_ring_buffer.c

cpp 复制代码
#include "uart_ring_buffer.h"

/* 缓冲区存储区 */
static uint8_t uart1_rx_buffer[UART_RX_BUFFER_SIZE];
static uint8_t uart1_tx_buffer[UART_TX_BUFFER_SIZE];

/* 环形缓冲区结构体 */
ring_buffer_t uart1_rx_rb;
ring_buffer_t uart1_tx_rb;

/**
 * @brief  初始化UART环形缓冲区
 * @param  None
 * @retval None
 */
void uart_ring_buffer_init(void)
{
    /* 初始化环形缓冲区 */
    ring_buffer_init(&uart1_rx_rb, uart1_rx_buffer, UART_RX_BUFFER_SIZE);
    ring_buffer_init(&uart1_tx_rb, uart1_tx_buffer, UART_TX_BUFFER_SIZE);
    
    /* 使能UART接收中断 */
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&huart1.Instance->DR, 1);
}

/**
 * @brief  UART接收中断回调函数
 * @param  huart: UART句柄
 * @retval None
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    uint8_t data;
    
    if (huart->Instance == USART1) {
        /* 读取接收到的数据 */
        data = (uint8_t)huart->Instance->DR;
        
        /* 写入接收环形缓冲区 */
        ring_buffer_write(&uart1_rx_rb, &data, 1);
        
        /* 重新使能接收中断 */
        HAL_UART_Receive_IT(huart, (uint8_t *)&huart->Instance->DR, 1);
    }
}

/**
 * @brief  UART发送中断回调函数
 * @param  huart: UART句柄
 * @retval None
 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    uint8_t data;
    
    if (huart->Instance == USART1) {
        /* 从发送环形缓冲区读取一个字节 */
        if (ring_buffer_read(&uart1_tx_rb, &data, 1) == 1) {
            /* 发送数据 */
            HAL_UART_Transmit_IT(huart, &data, 1);
        }
    }
}

/**
 * @brief  从UART接收缓冲区读取数据
 * @param  data: 存储读取数据的缓冲区
 * @param  len: 要读取的数据长度
 * @retval 实际读取的数据长度
 */
uint16_t uart_receive(uint8_t *data, uint16_t len)
{
    return ring_buffer_read(&uart1_rx_rb, data, len);
}

/**
 * @brief  通过UART发送数据(非阻塞)
 * @param  data: 要发送的数据指针
 * @param  len: 要发送的数据长度
 * @retval 实际写入发送缓冲区的数据长度
 */
uint16_t uart_send(const uint8_t *data, uint16_t len)
{
    uint16_t written;
    uint8_t first_byte;
    
    /* 写入发送环形缓冲区 */
    written = ring_buffer_write(&uart1_tx_rb, data, len);
    
    /* 如果发送器空闲,启动第一次发送 */
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) && written > 0) {
        ring_buffer_read(&uart1_tx_rb, &first_byte, 1);
        HAL_UART_Transmit_IT(&huart1, &first_byte, 1);
    }
    
    return written;
}

/**
 * @brief  通过UART发送字符串(非阻塞)
 * @param  str: 要发送的字符串
 * @retval 实际写入发送缓冲区的数据长度
 */
uint16_t uart_send_string(const char *str)
{
    return uart_send((const uint8_t *)str, strlen(str));
}

3. 主函数中的使用示例

cpp 复制代码
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "uart_ring_buffer.h"

int main(void)
{
    uint8_t rx_data[128];
    uint16_t rx_len;
    
    /* HAL库初始化 */
    HAL_Init();
    
    /* 系统时钟配置 */
    SystemClock_Config();
    
    /* 外设初始化 */
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    
    /* 初始化UART环形缓冲区 */
    uart_ring_buffer_init();
    
    /* 发送欢迎信息 */
    uart_send_string("UART Ring Buffer Demo\r\n");
    uart_send_string("Please enter some characters...\r\n");
    
    while (1) {
        /* 从接收缓冲区读取数据 */
        rx_len = uart_receive(rx_data, sizeof(rx_data));
        
        if (rx_len > 0) {
            /* 回显接收到的数据 */
            uart_send(rx_data, rx_len);
            
            /* 处理接收到的数据 */
            // process_data(rx_data, rx_len);
        }
        
        /* 其他任务 */
        HAL_Delay(10);
    }
}

五、关键注意事项和优化技巧

1. 中断安全问题

在上面的实现中,我们没有使用任何互斥机制,但它在中断环境下是安全的,原因是:

  • 写操作只会修改写指针,读操作只会修改读指针
  • 指针的修改是原子操作(在 32 位单片机上,16 位整数的赋值是单周期指令)
  • 生产者和消费者分别在不同的上下文(中断和主循环)中运行

重要提示:如果有多个生产者或多个消费者同时访问同一个环形缓冲区,就必须添加互斥机制(如关中断、使用信号量等)。

2. 溢出处理策略

当缓冲区满时,上面的实现会丢弃新的数据。在实际应用中,你可以根据需求选择不同的溢出处理策略:

  • 丢弃新数据:适用于实时性要求高的场景,如传感器数据采集
  • 覆盖旧数据:适用于日志系统等场景,总是保留最新的数据
  • 阻塞等待:适用于多任务系统,生产者等待缓冲区有空闲空间

3. 性能优化

  • 使用指针操作代替数组索引:可以稍微提高访问速度
  • 批量读写:尽量一次读写多个字节,减少函数调用开销
  • 使用 DMA 配合环形缓冲区:对于高速数据传输,可以使用 DMA 将数据直接写入环形缓冲区,完全不需要 CPU 干预

4. FreeRTOS 环境下的使用

在使用 FreeRTOS 等实时操作系统时,你有两种选择:

  1. 使用上面的通用环形缓冲区实现,配合 FreeRTOS 的信号量实现阻塞读写
  2. 使用 FreeRTOS 提供的xQueueCreate()等队列函数

自己实现的环形缓冲区通常比 FreeRTOS 队列更轻量、更高效,特别是对于字节流数据。

六、总结

环形缓冲区是嵌入式系统中不可或缺的基础组件,它以简单的实现解决了复杂的异步通信和速度匹配问题。在 STM32 单片机上,环形缓冲区广泛应用于 UART、SPI、I2C、CAN 等各种通信接口,以及传感器数据采集、音频处理等领域。

本文提供的实现是一个通用、高效、中断安全的版本,可以直接应用于大多数 STM32 项目。在实际使用中,你可以根据具体需求调整缓冲区大小、溢出处理策略和性能优化方法。

相关推荐
Full Stack Developme10 小时前
事件驱动与状态机比较
网络
星夜夏空9910 小时前
STM32单片机学习(16) —— 中断相关概念
stm32·单片机·学习
余生皆假期-10 小时前
配置 CodeX 环境的 Simlink AI 工具链
笔记·单片机·嵌入式硬件·算法
嵌入式-老费10 小时前
esp开发与应用(1602液晶显示屏)
单片机·嵌入式硬件
嵌入式小站10 小时前
STM32 临界区是什么:为什么有时候要用 __disable_irq() 保护变量
chrome·stm32·嵌入式硬件
leo_jk10 小时前
STM32单片机 空闲中断
stm32·单片机·嵌入式硬件
砍材农夫10 小时前
物联网 基于netty控制报文结构(报文分类)
网络·物联网·struts
weyyhdke10 小时前
2026电源与MCU控制设计实战:用Gemini3.5镜像站免费优化开关电源环路与电机FOC算法硬核教程
单片机·嵌入式硬件·算法
Irissgwe10 小时前
三、Socket 编程 TCP
linux·网络·tcp·socket编程