嵌入式基础技术博客

嵌入式基础技术博客

为了让初学者更好地了解嵌入式系统中的基础数据结构和编程技术,本文将均等地对 “环形缓冲区 (Ring Buffer)” 进行全方位的讲解。我们将从以下几个综合角度来深入探索:

  1. 基本概念与原理
  2. 缓冲区类型分析
  3. 设计和实现
  4. 编程中的常见问题与解决
  5. 实际应用场景
  6. 优化技巧和抽象层设计
  7. 源码分析
  8. 结论与扩展思考

为了方便理解,本文将添加 流程结构图示意,并穿插 实际操作步骤,以流程化、模块化方式更直观地展现 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 结构,不适合应用抽象。

五、实际应用场景

  1. UART 通信
  2. 指令列表处理
  3. 事件队列
  4. 系统日志系统 buffer
  5. 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,都需要对它有稳固的理解。

Logo

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

更多推荐