环形缓冲区(循环队列)模式

环形缓冲区在单片机中的应用

一、环形缓冲区(循环队列)模式详细描述

环形缓冲区(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(); // 读取缓冲区数据并解析指令

       // 其他业务逻辑...

   }

}

代码说明

  1. 临界区保护:中断和主循环共享缓冲区,需通过__disable_irq()/__enable_irq()关闭 / 开启全局中断,避免读写指针同时修改导致数据错乱;

  2. 计数器优化:用len替代 “预留空闲位” 的方式判断满 / 空,更直观,适合新手理解;

  3. 硬件适配:串口初始化(USART_Config)、中断函数名需根据实际单片机(如 STM32、51、STM8)调整,核心逻辑通用;

  4. 解耦设计:中断仅负责 “写数据”,主循环负责 “读数据 + 处理”,避免中断阻塞导致数据丢失。

四、扩展说明

  • 若需处理多字节数据包(如带帧头 / 帧尾的指令),可在主循环中累计读取字节,直到匹配帧头 / 帧尾后再解析;

  • 对于多生产者 / 多消费者场景(如多个中断写、多个任务读),需增加互斥锁(嵌入式中常用信号量、关中断);

  • 缓冲区大小需根据实际需求调整:过小易溢出,过大浪费内存(如串口指令一般 64~256 字节足够)。

posted @ 2025-12-10 12:32  Afangdong  阅读(587)  评论(0)    收藏  举报