环形缓冲区:嵌入式开发的关键数据结构
摘要:本文全面讲解嵌入式系统中的环形缓冲区(Ring Buffer)技术。文章从基本概念、类型分析入手,详细介绍了简单型Ring Buffer的设计与实现,包括初始化、读写操作等核心代码。针对常见问题如线程安全、缓冲区溢出等给出了解决方案,并列举了UART通信、DMA传输等典型应用场景。此外,还探讨了优化技巧,如抽象层设计、动态缓冲区和间隙保留等高级应用。最后以Linux内核实现为例进行源码分析,
嵌入式基础技术博客
目录
嵌入式基础技术博客
为了让初学者更好地了解嵌入式系统中的基础数据结构和编程技术,本文将均等地对 “环形缓冲区 (Ring Buffer)” 进行全方位的讲解。我们将从以下几个综合角度来深入探索:
- 基本概念与原理
- 缓冲区类型分析
- 设计和实现
- 编程中的常见问题与解决
- 实际应用场景
- 优化技巧和抽象层设计
- 源码分析
- 结论与扩展思考
为了方便理解,本文将添加 流程结构图示意,并穿插 实际操作步骤,以流程化、模块化方式更直观地展现 Ring Buffer 的工作原理。
一、基本概念与原理
环形缓冲区,也称 Ring Buffer 或 Circular Buffer,是一种特殊的 FIFO (先入先出)数据结构,它将一块连续的内存省略地看作环状,形成一个循环。这种结构特别适合线程间和事件驱动下的数据纤结与通信。
基本组成元素:
- 缓冲区数组 buffer[n]
- 输入指针 write_index
- 输出指针 read_index
- 缓冲区大小 size
原理流程图:
[初始化] --> [判断是否满] --> [写入数据] --> [更新写指针] --> ...
↑ ↓
[判断是否空] <-- [读取数据] <-- [更新读指针] <---------
- 当 write_index 达到缓冲区尺寸时,会重置为 0,继续写入;
- 同理,read_index 也在读取后回绕;
- 通过读写指针差值可判断空满状态。
二、缓冲区类型分析
根据应用需求,环形缓冲区可分为不同类型:
1. 简单型 (单个生产者 / 单个消费者)
常用于二级事件系统,例如 UART 事件中断和主应用线程之间的通信。
示意流程:
[ISR] --> 写入缓冲区 --> 应用线程读取
2. 维护型 (多生产者 / 多消费者)
需要加入线程安全措施,例如使用 mutex/信号量等同步机制。
3. DMA Ring Buffer
在高效率数据传输场景,例如 camera 、 Ethernet DMA 、USB等场景,通常会配合 DMA操作。
三、设计和实现
以下以“简单型 Ring Buffer”为例实现:
#define BUFFER_SIZE 128
typedef struct {
uint8_t buffer[BUFFER_SIZE];
volatile size_t head;
volatile size_t tail;
} RingBuffer;
void rb_init(RingBuffer *rb) {
rb->head = 0;
rb->tail = 0;
}
bool rb_is_full(RingBuffer *rb) {
return ((rb->head + 1) % BUFFER_SIZE) == rb->tail;
}
bool rb_is_empty(RingBuffer *rb) {
return rb->head == rb->tail;
}
bool rb_push(RingBuffer *rb, uint8_t data) {
if (rb_is_full(rb)) return false;
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % BUFFER_SIZE;
return true;
}
bool rb_pop(RingBuffer *rb, uint8_t *data) {
if (rb_is_empty(rb)) return false;
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % BUFFER_SIZE;
return true;
}
写入数据流程:
[判断是否满] -> [写入 buffer[head]] -> [head++ (mod N)]
读取数据流程:
[判断是否空] -> [读取 buffer[tail]] -> [tail++ (mod N)]
四、编程中的常见问题与解决
- 超出/空问题:未良的满/空判断导致数据写入旧值或读取错误。
- 线程不安全:未加锁在多线程下导致数据出现转换错误或飞转。
- 功能扩展性不好:未展示并可自定义的 data type 结构,不适合应用抽象。
五、实际应用场景
- UART 通信
- 指令列表处理
- 事件队列
- 系统日志系统 buffer
- DMA 回调数据转储
场景应用流程示例(UART 接收):
[中断收到字符] -> [写入缓冲区] -> [主循环读取字符处理]
六、优化技巧和抽象层设计
1. 添加类型操作
typedef struct {
void *buffer;
size_t elem_size;
size_t length;
size_t head;
size_t tail;
} GenericRingBuffer;
2. 使用实现
- macro 定义 type
- memcpy 处理全类型数据
3. 加入动态缓冲区
- malloc 分配缓冲区
- 支持缓冲区大小动态变化
4. 间隙保留和解决 “满格 vs 空格” 冲突
- 保留一个元素空间,或加 “count” 表示缓冲区长度
七、源码分析
以 Linux kernel 中 tty 实现为例:
include/linux/circ_buf.h
中定义了CIRC_SPACE()
、CIRC_CNT()
kernel/printk/printk_ringbuffer.c
内核通常用模板方式创建基础 RingBuffer 模型,应用于不同方向。
简要源码流程:
[申请空间] -> [CIRC_SPACE 判断] -> [写入数据] -> [更新 head]
↓
[读取数据] -> [更新 tail]
八、结论与扩展思考
Ring Buffer 作为基础数据结构,在嵌入式开发中有着极其重要的场景使用价值。从最简单的 UART buffer 到复杂的 DMA-动态 buffer,都需要对它有稳固的理解。

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