环形缓冲区(循环队列)模式
环形缓冲区在单片机中的应用
一、环形缓冲区(循环队列)模式详细描述
环形缓冲区(Ring Buffer/Circular Queue)是一种逻辑首尾相连的线性数据结构,核心是复用连续的内存空间,通过两个指针(读指针 / 写指针)管理数据的写入和读取,本质是用 “循环取模” 的方式让固定大小的数组具备 “环形” 特性,解决传统线性缓冲区频繁扩容 / 释放导致的内存碎片、效率低下问题。
1. 核心结构与原理
| 核心元素 | 作用 |
|---|---|
| 缓冲区数组(buf) | 物理载体,固定大小,存储实际数据 |
| 写指针(tail) | 指向下一个可写入的空闲位置,写入后指针循环后移 |
| 读指针(head) | 指向当前可读取的有效数据位置,读取后指针循环后移 |
| 状态判断 | - 空:head == tail- 满:(tail + 1) % size == head(预留 1 个空闲位,避免空 / 满判断冲突);或用计数器(len)记录有效数据数,满时 len == size |
2. 核心操作(伪代码)
// 写入数据(非满时)
int ring_buf_write(RingBuf *buf, uint8_t data) {
if ((buf->tail + 1) % buf->size == buf->head) { // 缓冲区满
return -1;
}
buf->buf[buf->tail] = data;
buf->tail = (buf->tail + 1) % buf->size; // 循环后移
return 0;
}
// 读取数据(非空时)
int ring_buf_read(RingBuf *buf, uint8_t *data) {
if (buf->head == buf->tail) { // 缓冲区空
return -1;
}
*data = buf->buf[buf->head];
buf->head = (buf->head + 1) % buf->size; // 循环后移
return 0;
}
3. 核心优势
-
无内存碎片:固定大小的连续内存,复用而非频繁分配 / 释放,适配单片机等内存受限场景;
-
高效读写:读写操作均为 O (1) 时间复杂度,无需移动数据(传统线性缓冲区读取后需整体前移);
-
异步解耦:支持 “生产者 - 消费者” 模型(如中断写、主循环读),避免中断中处理耗时逻辑导致数据丢失;
-
实现简单:仅需基础数组和指针操作,无复杂依赖。
4. 局限性
-
容量固定:初始化时需确定大小,无法动态扩容;
-
满 / 空判断需额外处理:需避免 “假满” 或 “假空”(如预留空闲位或增加计数器)。
二、典型使用场景
环形缓冲区是嵌入式系统中最常用的数据流处理模式,典型场景包括:
| 场景 | 应用说明 |
|---|---|
| 单片机串口(UART)收发 | 中断接收串口数据(生产者)写入缓冲区,主循环(消费者)读取处理,避免中断中处理耗时逻辑导致数据丢失; |
| 传感器高频数据缓存 | ADC 采集、温湿度传感器等高频数据写入缓冲区,后台定时读取分析,平衡采集 / 处理速率; |
| DMA 数据缓冲 | DMA 传输(如 SPI/I2C/ADC DMA)的数据写入缓冲区,CPU 分批读取处理,提升传输效率; |
| 音频 / 视频数据流处理 | 嵌入式音视频设备中,缓冲实时码流,解决播放 / 采集速率不匹配问题; |
| 嵌入式消息队列 | 任务间通信的消息缓存,生产者任务写消息,消费者任务读消息,解耦任务依赖; |
三、单片机 C 语言实现:串口 UART 中断接收 + 主循环处理
场景背景
单片机串口接收上位机下发的指令时,数据以字节流形式异步到达(中断触发),若在中断中直接处理指令(如解析、执行),可能因处理耗时导致后续字节丢失。因此用环形缓冲区作为 “中转”:
-
生产者:串口接收中断,将字节写入缓冲区;
-
消费者:主循环,从缓冲区读取字节并解析指令。
代码实现(通用 51/STM32 风格,简化外设初始化)
#include <stdint.h>
#include <string.h>
// 1. 定义环形缓冲区结构体(带计数器,简化满/空判断)
#define RING_BUF_SIZE 64 // 缓冲区大小,根据需求调整
typedef struct {
uint8_t buf[RING_BUF_SIZE]; // 数据缓冲区
uint16_t head; // 读指针
uint16_t tail; // 写指针
uint16_t len; // 有效数据长度
} RingBuf;
// 2. 全局缓冲区(串口接收专用)
static RingBuf uart_ring_buf = {0};
// 3. 缓冲区初始化函数
void ring_buf_init(RingBuf *buf) {
memset(buf->buf, 0, sizeof(buf->buf));
buf->head = 0;
buf->tail = 0;
buf->len = 0;
}
// 4. 写入单个字节(临界区保护,避免中断/主循环冲突)
int uart_ring_buf_write(uint8_t data) {
// 关中断保护临界区(单片机中需适配具体中断关闭指令,如__disable_irq())
__disable_irq();
if (uart_ring_buf.len >= RING_BUF_SIZE) { // 缓冲区满
__enable_irq();
return -1; // 写入失败
}
uart_ring_buf.buf[uart_ring_buf.tail] = data;
uart_ring_buf.tail = (uart_ring_buf.tail + 1) % RING_BUF_SIZE;
uart_ring_buf.len++;
__enable_irq(); // 开中断
return 0; // 写入成功
}
// 5. 读取单个字节
int uart_ring_buf_read(uint8_t *data) {
__disable_irq();
if (uart_ring_buf.len == 0) { // 缓冲区空
__enable_irq();
return -1; // 读取失败
}
*data = uart_ring_buf.buf[uart_ring_buf.head];
uart_ring_buf.head = (uart_ring_buf.head + 1) % RING_BUF_SIZE;
uart_ring_buf.len--;
__enable_irq();
return 0; // 读取成功
}
// 6. 串口接收中断服务函数(生产者)
// 注:不同单片机中断函数名不同,如STM32为USART1_IRQHandler,51为UART_RX_INT
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { // 接收中断标志置位
uint8_t recv_data = USART_ReceiveData(USART1); // 读取接收寄存器
uart_ring_buf_write(recv_data); // 写入环形缓冲区
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志
}
}
// 7. 指令解析函数(消费者,主循环调用)
void uart_cmd_parse(void) {
uint8_t recv_byte;
while (uart_ring_buf_read(&recv_byte) == 0) { // 循环读取所有有效数据
// 示例:解析简单指令(如接收到0x01执行LED_ON,0x02执行LED_OFF)
switch (recv_byte) {
case 0x01:
// LED_ON(); // 实际项目中替换为LED点亮逻辑
break;
case 0x02:
// LED_OFF(); // 实际项目中替换为LED熄灭逻辑
break;
default:
// 无效指令处理
break;
}
}
}
// 8. 主函数(初始化+循环处理)
int main(void) {
// 初始化:串口(波特率9600等)、环形缓冲区、中断
ring_buf_init(&uart_ring_buf);
USART_Config(); // 串口初始化(需适配硬件,如GPIO、波特率、中断使能)
NVIC_Config(); // 中断优先级配置,使能串口接收中断
while (1) { // 主循环
uart_cmd_parse(); // 读取缓冲区数据并解析指令
// 其他业务逻辑...
}
}
代码说明
-
临界区保护:中断和主循环共享缓冲区,需通过
__disable_irq()/__enable_irq()关闭 / 开启全局中断,避免读写指针同时修改导致数据错乱; -
计数器优化:用
len替代 “预留空闲位” 的方式判断满 / 空,更直观,适合新手理解; -
硬件适配:串口初始化(
USART_Config)、中断函数名需根据实际单片机(如 STM32、51、STM8)调整,核心逻辑通用; -
解耦设计:中断仅负责 “写数据”,主循环负责 “读数据 + 处理”,避免中断阻塞导致数据丢失。
四、扩展说明
-
若需处理多字节数据包(如带帧头 / 帧尾的指令),可在主循环中累计读取字节,直到匹配帧头 / 帧尾后再解析;
-
对于多生产者 / 多消费者场景(如多个中断写、多个任务读),需增加互斥锁(嵌入式中常用信号量、关中断);
-
缓冲区大小需根据实际需求调整:过小易溢出,过大浪费内存(如串口指令一般 64~256 字节足够)。

浙公网安备 33010602011771号