欢迎光临
我们一直在努力

单片机开发进阶技巧:(四)一文讲清 环形缓冲区 ring_buffer,附完整代码

上一篇下一篇
生产者-消费者模型

目 录

  • 环形缓冲区
    • 1)背景
    • 2)基本结构
      • 2.1)传统的回绕下标指针
      • 2.2)单调递增的无符号计数器(★)
    • 3)图解(先看下面函数再看图解)
    • 4)相关函数(采用单调递增的 in/out )
      • 4.1)将环形缓冲区大小向上取整为最接近的

        2

        n

        2^n

        2n

        • 4.1.1)原始通用写法
        • 4.1.2)直观易懂写法
        • 4.1.3)限制缓冲区大小的写法
      • 4.2)初始化
      • 4.3)写入数据
      • 4.4)读取数据,但并不移除数据
      • 4.5)判断缓冲区是否为空
      • 4.6)读取数据,同时移除数据
      • 4.7)丢弃指定长度数据
      • 4.8)清空所有数据
      • 4.9)完全释放环形缓冲区
    • 5)完整工程级代码(采用单调递增的 in/out )
      • 5.1)ring_buffer.c
      • 5.2)ring_buffer.h
    • 6)调用示例(采用单调递增的 in/out )
      • 6.1)裸机
      • 6.2)FreeRTOS
    • 7)和 FreeRTOS 内核源码里队列的区别是什么

环形缓冲区

1)背景

在串口通信、网络接收、音频流等场景中,数据是源源不断到达的,而处理程序往往无法立即消费所有数据。如果用一个普通线性缓冲区,当生产者(中断服务程序)写入数据快于消费者(主循环)读取时,要么需要动态扩展内存,要么覆盖未读数据(像STM32的官方例程里默认都是这种方式)。这时就需要自己写一个环形缓冲区

⟶

\\longrightarrow

⟶

环形缓冲区是个数据结构,在裸机和RTOS中都可以用,用于实现 生产者-消费者模型 。

2)基本结构

物理内存上仍是线性数组,逻辑上首尾相连成环。主要有两种实现方式。

2.1)传统的回绕下标指针

类似队列

基本结构代码示例:

typedef struct {
volatile uint8_t buffer[RING_BUF_SIZE]; // 固定大小的数据数组
volatile uint32_t head; // 写指针(指向下一次写入的下标)
volatile uint32_t tail; // 读指针(指向下一次读取的下标)
} ring_buf_t;

数据结构说明:

  • 缓冲区数组:buffer[RING_BUF_SIZE] —— 编译期固定大小的线性数组,无需动态分配,所有数据直接存放在结构体内。
  • 写指针(head):
    • 由生产者(如中断服务程序)维护,指向下一个要写入的数组下标。
    • 每次写入后 head 向前移动(head+1),到达缓冲区末尾时回绕到 0。
  • 读指针(tail):
    • 由消费者(如任务)维护,指向下一个要读取的数组下标。
    • 每次读取后 tail 向前移动(tail+1),同样在末尾回绕。
  • 容量:缓冲区总长度由宏 RING_BUF_SIZE 决定,由于需要预留一个空位区分“空”和“满”,实际可存储数据量最多为 RING_BUF_SIZE – 1。

状态判断(基于下标回绕的实现):

  • 空:head == tail

    • 读写指针重合,表示缓冲区中没有尚未消费的数据

    • 如果为空则不可读

  • 满:(head + 1) % RING_BUF_SIZE == tail

    • 写指针的下一个位置将追上读指针,此时停止写入以避免“空”“满”混淆。需要牺牲一个字节的空间
    • 如果为满则不可写入

典型操作示例:

// 写入一个字节(假设已检查非满)
void ring_buf_put(ring_buf_t *rb, uint8_t data) {
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % RING_BUF_SIZE;
}

// 读取一个字节(假设已检查非空)
uint8_t ring_buf_get(ring_buf_t *rb) {
uint8_t data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % RING_BUF_SIZE;
return data;
}

但此法需要手动回绕,并且必须预留一个“空白”单元,否则当 head == tail 时无法区分是空还是满。

2.2)单调递增的无符号计数器(★)

使用单调递增的无符号计数器代替传统回绕的下标指针,利用无符号整数溢出的模特性(比如 uint32_t,

2

32

+

100

2^{32}+100

232+100 会自动变成

100

100

100,

100

−

200

=

100

(

+

2

32

)

−

200

100-200=100(+2^{32})-200

100−200=100(+232)−200),避免了每次操作后的回绕判断,并且可以完全利用缓冲区所有空间。

基本结构代码示例:

/* 环形缓冲区控制块 */
typedef struct _ring_buffer_struct
{
uint8_t *buffer; // the buffer holding the data,指向物理缓冲区的指针
uint32_t size; // the size(power of 2) of the allocated buffer
volatile uint32_t in; // data is added at offset(in % size)
volatile uint32_t out; // data is extracted from offset(out % size)
}RING_BUFFER_T;

数据结构说明:

  • 缓冲区数组:uint8_t *buffer —— 指向动态分配的内存块,大小为 size 字节,实际存储数据
  • 写计数器(in):
    • 由生产者(如中断服务程序)递增,每次写入 n 字节后 in += n
    • (in)in_pos 所代表的位置是下一次要写入的起始位置
    • 物理写入位置为 in & (size – 1)(等价于 in % size,但用位运算加速)
  • 读计数器(out):
    • 由消费者(如任务)递增,每次读取/丢弃 n 字节后 out += n
    • (out)out_pos 所代表的位置是下一次要读取的起始位置
    • 物理读取位置为 out & (size – 1)。
  • 容量(size):必须是 2 的幂,用于实现高效的取模运算。

状态判断(基于计数器差值的实现):

  • 已用字节数 = in – out == 0
  • 剩余空闲字节数 = size – (in – out) == 0
  • 空:in == out
  • 满:in – out == size

注意:

  • 该实现不需要预留空位,缓冲区可以 100% 填满。状态判断完全由无符号减法完成,in 和 out 永远不会回绕为 0,而是自然溢出。因此 “满” 的判断条件就是已用空间等于 size,写入前会检查空闲空间,不会覆盖未读数据。
  • 采用此方案的话,不论 in / out 是什么类型,值是多少,不论 size 等于多少,都可以用 in & (size – 1) 和 out & (size – 1) 把 in / out 映射到缓冲区中的实际数组下标。

3)图解(先看下面函数再看图解)

在这里插入图片描述

4)相关函数(采用单调递增的 in/out )

4.1)将环形缓冲区大小向上取整为最接近的

2

n

2^n

2n

核心目的就是用位运算代替除法/取模,从而大幅提高速度。特别是在嵌入式场景(比如串口中断里频繁写入数据),每微秒都很宝贵。

具体来说,环形缓冲区有两个最频繁的操作:

  • 把逻辑读写位置映射到实际数组下标(pos = index % size)
  • 计算当前已用空间、剩余空间等(used = in – out; free = size – used)
  • 当 size 是 2 的幂时,这些操作有极大的优化空间。一般数组下标需要 index % size,而CPU 的 除法/取模指令非常慢(可能需要几十个时钟周期)。如果 size = 2^n,那么 index % size 等价于 index & (size – 1),这是单条按位与指令,通常 1 个时钟周期就能完成。还有方便回绕、计算剩余空间等等方面。

    4.1.1)原始通用写法

    (移位操作,执行时间短且可预测;但需要理解)

    static uint32_t roundup_pow2(uint32_t v)
    {
    v—; // 1. 减 1
    v |= v >> 1; // 2. 右移1位后按位或
    v |= v >> 2;// 3. 右移2位后按位或
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    // 如果输入和输出的类型不是uint32_t,则需要调整最后或到哪个数字
    return v + 1; // n. 加 1 返回
    }

    原理:

    • 2

      n

      2^n

      2n 的二进制特点是:bit

      n

      n

      n 为

      1

      1

      1,其余均为

      0

      0

      0(例如

      2

      3

      2^3

      23→ bit

      3

      3

      3 =1 → 1000);

    • 2

      n

      −

      1

      2^n-1

      2n−1 的二进制特点是:bit

      0

      0

      0 ~ bit

      (

      n

      −

      1

      )

      (n-1)

      (n−1) 全为

      1

      1

      1,其余均为

      0

      0

      0。

    • 那么只要先将输入值

      −

      1

      -1

      −1 防止原本就是

      2

      n

      2^n

      2n 而误将其变成

      2

      n

      +

      1

      2^{n+1}

      2n+1,再通过一系列移位和按位或(|),把最高位

      1

      1

      1 后面的所有低位都变成

      1

      1

      1 ;最后再加

      1

      1

      1,使得原最高位的下一位变成

      1

      1

      1,而后面全为

      0

      0

      0,从而得到一个

      2

      2

      2 的幂。

    4.1.2)直观易懂写法

    (易懂;但执行时间较长,要循环多次)

    static uint32_t roundup_pow2(uint32_t v)
    {
    // 若v本身就是2的幂,则直接返回
    if ( (v!=0) && (0==(v&(v–1))) )
    return v;

    uint32_t r = 1;
    while (r < v)
    r <<= 1;
    return r;
    }

    原理:

    • 首先判断输入值本身是不是

      2

      n

      2^n

      2n:

      • 若是

        2

        n

        2^n

        2n ,则其 bit

        n

        n

        n 为

        1

        1

        1,其余均为

        0

        0

        0;

        2

        n

        −

        1

        2^n-1

        2n−1 的 bit

        0

        0

        0 ~ bit

        (

        n

        −

        1

        )

        (n-1)

        (n−1) 全为

        1

        1

        1,其余均为

        0

        0

        0;两者相

        &

        \\&

        & 之后为

        0

        0

        0;

      • 若不是

        2

        n

        2^n

        2n ,则相

        &

        \\&

        & 之后不为

        0

        0

        0。

    • 然后从

      1

      1

      1 开始左移并比较大小,直到找到一个大于等于输入值的

      2

      n

      2^n

      2n 。

    4.1.3)限制缓冲区大小的写法

    /**
    * @brief将输入值向上取整为2的幂,提高运算速度
    * @paramn – 用户期望的缓冲区字节数
    * @return返回2的幂
    */

    static uint32_t _align_power_of_2(uint32_t n)
    {
    const uint32_t MAX_SIZE = 0x1000;// 限制最大尺寸为2^12=4*4096字节

    if (n == 0) return 0; // 或 return 1
    if (n > MAX_SIZE) return MAX_SIZE;

    /* 经典向上取整 */
    uint32_t r = 1;
    while (r < n)
    r <<= 1;
    return r;
    }

    4.2)初始化

    代码:

    /**
    * @brief初始化
    * @paramsize – 用户期望的缓冲区字节数
    * @return返回初始化后的环形缓冲区控制块
    */

    volatile RING_BUFFER_T * ring_buffer_init(uint32_t size)
    {
    /* 为控制块分配内存 */
    RING_BUFFER_T *ring_buffer = NULL;
    ring_buffer = (RING_BUFFER_T *)malloc(sizeof(RING_BUFFER_T));
    if (!ring_buffer)
    {
    return NULL;
    }
    /* 为真正缓冲区分配内存 */
    size = _align_power_of_2(size);
    ring_buffer->buffer = (uint8_t *)malloc(size);
    if (!ring_buffer->buffer)
    {
    free(ring_buffer);
    return NULL;
    }
    /* 控制块成员初始化 */
    ring_buffer->size = size;
    ring_buffer->in = 0;
    ring_buffer->out = 0;

    return ring_buffer;
    }

    4.3)写入数据

    代码:

    /**
    * @brief向环形缓冲区写入数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    buffer – 指向要写入的数据源数组的指针
    len – 请求写入的字节数
    * @return实际写入的字节数
    */

    uint32_t ring_buffer_add(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len)
    {
    uint32_t free_size = 0; /*free buffer size*/
    uint32_t used_size = 0; /*used buffer size*/
    uint32_t len_to_end = 0; /*length to buffer end*/
    uint32_t in_pos = 0;

    if (!ring_buffer || !buffer || len == 0)
    {
    return 0;
    }

    /* 计算当前空闲空间 */
    used_size = ring_buffer->in – ring_buffer->out;
    free_size = ring_buffer->size – used_size;
    len = len < free_size ? len : free_size;// 防止写入超出空闲空间, 若超出则舍弃超出的部分

    /* 计算写入起始物理位置 */
    in_pos = ring_buffer->in & (ring_buffer->size – 1);

    /* 计算是否需要回绕写入 */
    len_to_end = ring_buffer->size – in_pos;
    len_to_end = len_to_end < len ? len_to_end : len;

    /* 从 in_pos 开始拷贝第一段数据 */
    memcpy(ring_buffer->buffer + in_pos, buffer, len_to_end);
    /* 如果还有剩余数据,从缓冲区头部(地址 0)继续拷贝 */
    memcpy(ring_buffer->buffer, buffer + len_to_end, len – len_to_end);
    ring_buffer->in += len;

    return len;
    }

    主要步骤解析:

    • 计算当前空闲空间:

      • 由于这里的 in 和 out 是单调递增的无符号整数(uint32_t),代表写入和读出的累计计数。则已用字节数 = in – out,即使 in 或 out 溢出回绕,差值仍然准确。
        • C 标准规定无符号整数的减法永远产生非负结果,即使因为溢出使得 in<out,in-out 的值仍为正且准确,以 uint8_t 为例,10-250=10(+256)-250=16 ;
      • 剩余空闲字节数 = 缓冲区总大小 size – 已用字节数,若超出则舍弃超出的部分。
    • 计算写入起始物理位置

      • in 自己并不是下标,而是已写入总字节数,,当出现溢出回绕时,要想转换成数组下标,需要用:

        in & (size – 1)

        因为 size 被对齐为 2 的幂,所以 size – 1 是一个低位全 1 的掩码。in & (size – 1) 会自动保留 in 的低 10 位,高位的进位全部丢弃,其结果等价于 in % size,但位运算要快得多。(比如 size = 1024 = 0x400,size – 1 = 0x3FF,in = 1100 = 0x44C,则对应数组下标为 in & (size – 1) = 0x4C = 76)

    4.4)读取数据,但并不移除数据

    代码:

    /**
    * @brief从环形缓冲区中读取数据,但并不移除数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    buffer – 指向存放读出数据的目标数组的指针
    len – 请求读取的字节数
    * @return实际读取的字节数
    */

    uint32_t ring_buffer_get(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len)
    {
    uint32_t used_size = 0; /*used buffer size*/
    uint32_t len_to_end = 0; /*length to buffer end*/
    uint32_t out_pos = 0;

    if (!ring_buffer || !buffer || len == 0)
    {
    return 0;
    }

    /* 计算已用的数据量,限制读取长度 */
    used_size = ring_buffer->in – ring_buffer->out;
    len = len < used_size ? len : used_size;

    /* 计算读取起始物理位置 */
    out_pos = ring_buffer->out & (ring_buffer->size – 1);

    /* 计算是否需要回绕读取 */
    len_to_end = ring_buffer->size – out_pos;
    len_to_end = len_to_end < len ? len_to_end : len;

    /*first get the data starting from ring_buffer->out to buffer end*/
    memcpy(buffer, ring_buffer->buffer + out_pos, len_to_end);
    /*then get the rest(if any) at the beginning of the buffer*/
    memcpy(buffer + len_to_end, ring_buffer->buffer, len – len_to_end);
    return len;
    }

    主要逻辑和写入时类似。

    4.5)判断缓冲区是否为空

    代码:

    /**
    * @brief判断环形缓冲区是否为空
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    * @return-1:缓冲区失败,0:不为空,1:为空
    */

    int32_t ring_buffer_is_empty(volatile RING_BUFFER_T *ring_buffer)
    {
    if (!ring_buffer)
    return –1; // 错误
    return (ring_buffer->in == ring_buffer->out) ? 1 : 0; // 1 表示为空
    }

    4.6)读取数据,同时移除数据

    代码:

    /**
    * @brief从环形缓冲区中读取数据,同时移除数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    buffer – 指向存放读出数据的目标数组的指针
    len – 请求读取的字节数
    * @return实际读取的字节数
    * @note和ring_buffer_get()的区别只在于返回值
    */

    uint32_t ring_buffer_get_with_del(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len)
    {
    uint32_t used_size = 0; /*used buffer size*/
    uint32_t len_to_end = 0; /*length to buffer end*/
    uint32_t out_pos = 0;

    if (!ring_buffer || !buffer || len == 0)
    {
    return 0;
    }

    used_size = ring_buffer->in – ring_buffer->out;
    len = len < used_size ? len : used_size;

    out_pos = ring_buffer->out & (ring_buffer->size – 1);
    len_to_end = ring_buffer->size – out_pos;
    len_to_end = len_to_end < len ? len_to_end : len;

    /*first get the data starting from ring_buffer->out to buffer end*/
    memcpy(buffer, ring_buffer->buffer + out_pos, len_to_end);
    /*then get the rest(if any) at the beginning of the buffer*/
    memcpy(buffer + len_to_end, ring_buffer->buffer, len – len_to_end);

    /* 将out右移len,等效于删除数据 */
    ring_buffer->out += len;

    return len;
    }

    解析:

    • 最后一步的作用是:并不真的删除或置零物理内存上的数据,只是将 out 右移 len ,下次读取就从 out+len 位置读,原来的数据会被后续写入覆盖掉。

    4.7)丢弃指定长度数据

    代码:

    /**
    * @brief从环形缓冲区中丢弃数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    len – 请求丢弃的字节数
    * @returnlen – 实际丢弃的字节数
    * @note从out位置开始丢弃
    */

    uint32_t ring_buffer_delete(volatile RING_BUFFER_T *ring_buffer, uint32_t len)
    {
    uint32_t used_size = 0; /*used buffer size*/

    if (!ring_buffer)
    {
    return 0;
    }

    used_size = ring_buffer->in – ring_buffer->out;
    len = len < used_size ? len : used_size;// 限制丢弃长度不会超过已用长度
    ring_buffer->out += len;// 将out右移len,等效于删除数据

    return len;
    }

    4.8)清空所有数据

    代码:

    /**
    * @brief清空环形缓冲区中的所有数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    * @return-1:缓冲区无效,0:清空成功
    * @note并不清空真实数据,只将in和out置0,等效于清空
    */

    int32_t ring_buffer_clean(volatile RING_BUFFER_T *ring_buffer)
    {
    if (!ring_buffer)
    {
    return –1;
    }

    ring_buffer->in = 0;
    ring_buffer->out = 0;

    return 0;
    }

    4.9)完全释放环形缓冲区

    代码:

    int32_t ring_buffer_release(volatile RING_BUFFER_T **pp_ring_buffer)
    {
    if (!pp_ring_buffer || !*pp_ring_buffer)
    return –1;
    RING_BUFFER_T *rb = (RING_BUFFER_T *)*pp_ring_buffer; // 去掉 volatile
    if (rb->buffer) {
    free(rb->buffer);
    rb->buffer = NULL;
    }
    free(rb);
    *pp_ring_buffer = NULL; // 修改调用者的指针
    return 0;
    }

    必须用二级指针才能真正的将外部指针置NULL。

    5)完整工程级代码(采用单调递增的 in/out )

    在调用相关函数的时候,要使用互斥锁之类的机制,保证线程安全。

    5.1)ring_buffer.c

    #include "ring_buffer.h"
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>

    /**
    * @brief将输入值向上取整为2的幂,提高运算速度
    * @paramn – 用户期望的缓冲区字节数
    * @return返回2的幂
    */

    static uint32_t _align_power_of_2(uint32_t n)
    {
    const uint32_t MAX_SIZE = 0x1000;// 限制最大尺寸为2^12=4*4096字节

    if (n == 0) return 0; // 或 return 1
    if (n > MAX_SIZE) return MAX_SIZE;

    /* 经典向上取整 */
    uint32_t r = 1;
    while (r < n)
    r <<= 1;
    return r;
    }

    /**
    * @brief初始化
    * @paramsize – 用户期望的缓冲区字节数
    * @return返回初始化后的环形缓冲区控制块
    */

    volatile RING_BUFFER_T * ring_buffer_init(uint32_t size)
    {
    /* 为控制块分配内存 */
    RING_BUFFER_T *ring_buffer = NULL;
    ring_buffer = (RING_BUFFER_T *)malloc(sizeof(RING_BUFFER_T));
    if (!ring_buffer)
    {
    return NULL;
    }
    /* 为真正缓冲区分配内存 */
    size = _align_power_of_2(size);
    ring_buffer->buffer = (uint8_t *)malloc(size);
    if (!ring_buffer->buffer)
    {
    free(ring_buffer);
    return NULL;
    }
    /* 控制块成员初始化 */
    ring_buffer->size = size;
    ring_buffer->in = 0;
    ring_buffer->out = 0;

    return ring_buffer;
    }

    /**
    * @brief向环形缓冲区写入数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    buffer – 指向要写入的数据源数组的指针
    len – 请求写入的字节数
    * @return实际写入的字节数
    */

    uint32_t ring_buffer_add(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len)
    {
    uint32_t free_size = 0; /*free buffer size*/
    uint32_t used_size = 0; /*used buffer size*/
    uint32_t len_to_end = 0; /*length to buffer end*/
    uint32_t in_pos = 0;

    if (!ring_buffer || !buffer || len == 0)
    {
    return 0;
    }

    /* 计算当前空闲空间 */
    used_size = ring_buffer->in – ring_buffer->out;
    free_size = ring_buffer->size – used_size;
    len = len < free_size ? len : free_size;// 防止写入超出空闲空间, 若超出则舍弃超出的部分

    /* 计算写入起始物理位置 */
    in_pos = ring_buffer->in & (ring_buffer->size – 1);

    /* 计算是否需要回绕写入 */
    len_to_end = ring_buffer->size – in_pos;
    len_to_end = len_to_end < len ? len_to_end : len;

    /* 从 in_pos 开始拷贝第一段数据 */
    memcpy(ring_buffer->buffer + in_pos, buffer, len_to_end);
    /* 如果还有剩余数据,从缓冲区头部(地址 0)继续拷贝 */
    memcpy(ring_buffer->buffer, buffer + len_to_end, len – len_to_end);
    ring_buffer->in += len;

    return len;
    }

    /**
    * @brief从环形缓冲区中读取数据,但并不移除数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    buffer – 指向存放读出数据的目标数组的指针
    len – 请求读取的字节数
    * @returnlen – 实际读取的字节数
    */

    uint32_t ring_buffer_get(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len)
    {
    uint32_t used_size = 0; /*used buffer size*/
    uint32_t len_to_end = 0; /*length to buffer end*/
    uint32_t out_pos = 0;

    if (!ring_buffer || !buffer || len == 0)
    {
    return 0;
    }

    /* 计算已用的数据量,限制读取长度 */
    used_size = ring_buffer->in – ring_buffer->out;
    len = len < used_size ? len : used_size;

    /* 计算读取起始物理位置 */
    out_pos = ring_buffer->out & (ring_buffer->size – 1);

    /* 计算是否需要回绕读取 */
    len_to_end = ring_buffer->size – out_pos;
    len_to_end = len_to_end < len ? len_to_end : len;

    /*first get the data starting from ring_buffer->out to buffer end*/
    memcpy(buffer, ring_buffer->buffer + out_pos, len_to_end);
    /*then get the rest(if any) at the beginning of the buffer*/
    memcpy(buffer + len_to_end, ring_buffer->buffer, len – len_to_end);
    return len;
    }

    /**
    * @brief判断环形缓冲区是否为空
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    * @return-1:缓冲区无效,0:不为空,1:为空
    */

    int32_t ring_buffer_is_empty(volatile RING_BUFFER_T *ring_buffer)
    {
    if (!ring_buffer)
    return –1; // 错误
    return (ring_buffer->in == ring_buffer->out) ? 1 : 0; // 1 表示为空
    }

    /**
    * @brief从环形缓冲区中读取数据,同时移除数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    buffer – 指向存放读出数据的目标数组的指针
    len – 请求读取的字节数
    * @returnlen – 实际读取的字节数
    * @note和ring_buffer_get()的区别只在于返回值
    */

    uint32_t ring_buffer_get_with_del(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len)
    {
    uint32_t used_size = 0; /*used buffer size*/
    uint32_t len_to_end = 0; /*length to buffer end*/
    uint32_t out_pos = 0;

    if (!ring_buffer || !buffer || len == 0)
    {
    return 0;
    }

    used_size = ring_buffer->in – ring_buffer->out;
    len = len < used_size ? len : used_size;

    out_pos = ring_buffer->out & (ring_buffer->size – 1);
    len_to_end = ring_buffer->size – out_pos;
    len_to_end = len_to_end < len ? len_to_end : len;

    /*first get the data starting from ring_buffer->out to buffer end*/
    memcpy(buffer, ring_buffer->buffer + out_pos, len_to_end);
    /*then get the rest(if any) at the beginning of the buffer*/
    memcpy(buffer + len_to_end, ring_buffer->buffer, len – len_to_end);

    /* 将out右移len,等效于删除数据 */
    ring_buffer->out += len;

    return len;
    }

    /**
    * @brief从环形缓冲区中丢弃数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    len – 请求丢弃的字节数
    * @returnlen – 实际丢弃的字节数
    * @note从out位置开始丢弃
    */

    uint32_t ring_buffer_delete(volatile RING_BUFFER_T *ring_buffer, uint32_t len)
    {
    uint32_t used_size = 0; /*used buffer size*/

    if (!ring_buffer)
    {
    return 0;
    }

    used_size = ring_buffer->in – ring_buffer->out;
    len = len < used_size ? len : used_size;// 限制丢弃长度不会超过已用长度
    ring_buffer->out += len;

    return len;
    }

    /**
    * @brief清空环形缓冲区中的所有数据
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    * @return-1:缓冲区无效,0:清空成功
    * @note并不清空真实数据,只将in和out置0,等效于清空
    */

    int32_t ring_buffer_clean(volatile RING_BUFFER_T *ring_buffer)
    {
    if (!ring_buffer)
    {
    return –1;
    }

    ring_buffer->in = 0;
    ring_buffer->out = 0;

    return 0;
    }

    /**
    * @brief完全释放环形缓冲区
    * @parampp_ring_buffer – 指向环形缓冲区控制块的指针的指针(二级指针)
    * @return-1:缓冲区无效,0:清空成功
    * @note真实的将缓冲区的内存释放掉,并且将原指针置NULL
    */

    int32_t ring_buffer_release(volatile RING_BUFFER_T **pp_ring_buffer)
    {
    if (!pp_ring_buffer || !*pp_ring_buffer)
    return –1;
    RING_BUFFER_T *rb = (RING_BUFFER_T *)*pp_ring_buffer; // 去掉 volatile
    if (rb->buffer) {
    free(rb->buffer);
    rb->buffer = NULL;
    }
    free(rb);
    *pp_ring_buffer = NULL; // 修改调用者的指针
    return 0;
    }

    /**
    * @brief获取已用空间,只读函数,避免直接访问in和out
    * @paramring_buffer – 指向环形缓冲区控制块的指针
    * @return已用空间
    */

    uint32_t ring_buffer_used(volatile RING_BUFFER_T *ring_buffer)
    {
    if (!ring_buffer)
    return 0; // 或通过额外参数返回错误
    return ring_buffer->in – ring_buffer->out;
    }

    5.2)ring_buffer.h

    #ifndef _RING_BUFFER_H_
    #define _RING_BUFFER_H_

    #ifndef __IO
    #define __IO volatile
    #endif

    #include <stdint.h>

    /* 环形缓冲区控制块 */
    typedef struct _ring_buffer_struct
    {
    uint8_t *buffer; // the buffer holding the data,指向真正缓冲区的指针
    uint32_t size; // the size(power of 2) of the allocated buffer
    __IO uint32_t in; // data is added at offset(in % size)
    __IO uint32_t out; // data is extracted from offset(out % size)
    }RING_BUFFER_T;

    volatile RING_BUFFER_T * ring_buffer_init(uint32_t size);
    uint32_t ring_buffer_add(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len);
    uint32_t ring_buffer_get(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len);
    uint32_t ring_buffer_delete(volatile RING_BUFFER_T *ring_buffer, uint32_t len);
    int32_t ring_buffer_release(volatile RING_BUFFER_T **pp_ring_buffer);
    int32_t ring_buffer_clean(volatile RING_BUFFER_T *ring_buffer);
    int32_t ring_buffer_is_empty(volatile RING_BUFFER_T *ring_buffer);
    uint32_t ring_buffer_get_with_del(volatile RING_BUFFER_T *ring_buffer, uint8_t *buffer, uint32_t len);
    uint32_t ring_buffer_used(volatile RING_BUFFER_T *ring_buffer);

    #endif

    6)调用示例(采用单调递增的 in/out )

    整个环形缓冲区的操作都是位运算 + memcpy,适合在中断里快速执行,不会长时间关中断。

    6.1)裸机

    main.c(调用示例)

    #include "ring_buffer.h"
    #include <stdio.h>
    #include <string.h>

    // 模拟串口接收中断:向环形缓冲区填入数据
    void simulate_uart_rx_isr(volatile RING_BUFFER_T *rb, const char *data) {
    ring_buffer_add(rb, (uint8_t *)data, strlen(data));
    }

    int main(void) {
    // 1. 初始化一个256字节的环形缓冲区(内部会对齐到256)
    volatile RING_BUFFER_T *rb = ring_buffer_init(256);
    if (!rb) {
    printf("缓冲区初始化失败\\n");
    return –1;
    }

    // 2. 模拟中断写入一些数据
    simulate_uart_rx_isr(rb, "Hello");
    simulate_uart_rx_isr(rb, " RingBuffer!");

    // 3. 读取并消费缓冲区中的所有数据
    uint8_t buf[64];
    uint32_t total = 0;
    while (!ring_buffer_is_empty(rb)) { // 返回1表示空,0表示非空
    uint32_t n = ring_buffer_get_with_del(rb, buf, sizeof(buf) – 1);
    if (n > 0) {
    buf[n] = '\\0';
    printf("读到: %s\\n", buf);
    total += n;
    }
    }
    printf("总共读取 %u 字节\\n", total);

    // 4. 也可单独查看已用字节数
    printf("当前缓冲区已用: %u 字节\\n", ring_buffer_used(rb));

    // 5. 释放并安全置空外部指针
    ring_buffer_release((volatile RING_BUFFER_T **)&rb);
    // 此时 rb 已被置为 NULL,可安全检查
    if (rb == NULL) {
    printf("缓冲区已释放,指针已置空\\n");
    }

    return 0;
    }

    6.2)FreeRTOS

    下面是一个基于 FreeRTOS 的简短示例。它包含两个任务:

    • 生产者任务(模拟串口接收中断,持续向环形缓冲区写入数据)
    • 消费者任务(显示任务,不断从环形缓冲区读出数据并打印)

    main.c(调用示例)

    #include "FreeRTOS.h"
    #include "task.h"
    #include "ring_buffer.h"
    #include <stdio.h>
    #include <string.h>

    /* 全局环形缓冲区,供两个任务共享 */
    static volatile RING_BUFFER_T *g_uart_rb = NULL;
    // …… 其余配置代码

    /*———————————————————–*/

    /*
    * 模拟串口接收中断写入任务
    * 在实际项目中,ring_buffer_add() 通常在真正的 UART 中断服务程序里调用,
    * 这里为了演示用任务模拟周期性写入。
    */

    void vUartRxTask(void *pvParameters)
    {
    const char *pacing_msg = "Hello FreeRTOS! ";
    const TickType_t xDelay = pdMS_TO_TICKS(500);

    for (;;) {
    /* 模拟每次收到一帧数据,写入环形缓冲区 */
    ring_buffer_add(g_uart_rb,
    (uint8_t *)pacing_msg,
    strlen(pacing_msg));

    /* 实际中断不会延时,这里仅模拟接收间隔 */
    vTaskDelay(xDelay);
    }
    }

    /*———————————————————–*/

    /*
    * 显示任务:读取并打印缓冲区中的数据。
    * 采用轮询方式,当缓冲区非空时就取出显示,否则短暂休眠让出 CPU。
    */

    void vDisplayTask(void *pvParameters)
    {
    uint8_t buf[128];

    for (;;) {
    /* ring_buffer_is_empty 返回 0 表示非空,1 表示空 */
    if (!ring_buffer_is_empty(g_uart_rb)) {
    uint32_t n = ring_buffer_get_with_del(g_uart_rb, buf, sizeof(buf) – 1);
    if (n > 0) {
    buf[n] = '\\0'; /* 作为字符串结尾 */
    printf("%s", (char *)buf); /* 实际需重定向 printf */
    }
    } else {
    /* 缓冲区空,稍作休眠,避免频繁空转 */
    vTaskDelay(pdMS_TO_TICKS(10));
    }
    }
    }

    /*———————————————————–*/

    int main(void)
    {
    /* 初始化环形缓冲区,申请 256 字节(内部会向上取 2 的幂) */
    g_uart_rb = ring_buffer_init(256);
    if (g_uart_rb == NULL) {
    /* 初始化失败处理(例如亮灯或死循环) */
    for (;;);
    }

    /* 创建生产者任务(模拟串口接收) */
    xTaskCreate(vUartRxTask, "UartRx", 128, NULL, 1, NULL);
    /* 创建消费者任务(显示) */
    xTaskCreate(vDisplayTask, "Display", 256, NULL, 2, NULL);

    /* 启动调度器,之后由 FreeRTOS 管理 */
    vTaskStartScheduler();

    /* 正常情况下不会执行到这里 */
    for (;;);
    return 0;
    }

    在真实中断中使用环形缓冲区的方法:上面只是演示,若要在真实的 UART 接收中断中写入,只需在中断服务程序里直接调用:

    void UART_RX_IRQHandler(void) {
    uint8_t ch = UART_ReceiveData(); // 读取一个字节
    ring_buffer_add(g_uart_rb, &ch, 1);
    }

    并且如果消费者任务希望立即被唤醒而不是轮询,可以配合 FreeRTOS 的 任务通知等方法。

    7)和 FreeRTOS 内核源码里队列的区别是什么

    在 ISR 里直接用 xQueueSendFromISR() 也可行,但和自写的环形缓冲区相比,有几个关键区别:

    • 阻塞与唤醒机制

      • FreeRTOS 队列:当任务从队列读取但无数据时,可以挂起进入阻塞态,数据到达再被唤醒。适合“任务等待数据”的场景。
      • 自定义环形缓冲区:没有阻塞能力,任务必须轮询或由其他机制唤醒(如任务通知、信号量)。但它完全在用户空间,不经过内核调度。
    • 性能与开销

      • FreeRTOS 队列在 ISR 中写入需要进入临界区(关中断或挂起调度器),还可能触发任务调度,开销比简单的 memcpy + 指针更新大得多。
      • 自定义环形缓冲区在 ISR 中只做几次运算和内存拷贝,没有锁、没有内核参与,适合高频、低延迟的中断(如串口每次接收一个字节)。
    • 数据粒度和灵活性

      • FreeRTOS 队列每次投递的是固定大小的消息(如 1 字节、1 个结构体),适合传递完整事件或小消息。
      • 自定义环形缓冲区可以一次写入任意长度字节流,非常适合流式数据(如串口、音频、网络数据)。
    • 内存与复杂度的权衡

      • FreeRTOS 队列由内核管理,自带安全特性(阻塞、优先级继承等),开发快。
      • 自定义环形缓冲区需要自己保证单生产者/单消费者模型下的并发安全,依赖 volatile + 内存屏障(或原子操作),但换来的是完全可控的极简实现。

    如果需求是从串口收字节并快速送到任务处理,不在乎阻塞,且想降低开销和耦合,自定义环形缓冲区是更轻量、更高效的选择;

    如果需要任务阻塞等待数据、或者传递多条不同消息队列,那 FreeRTOS 队列就是正确工具。


    赞(0)
    未经允许不得转载:171主机测评 » 单片机开发进阶技巧:(四)一文讲清 环形缓冲区 ring_buffer,附完整代码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址