| 生产者-消费者模型 |
目 录
- 环形缓冲区
-
- 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)完全释放环形缓冲区
- 4.1)将环形缓冲区大小向上取整为最接近的
- 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
核心目的就是用位运算代替除法/取模,从而大幅提高速度。特别是在嵌入式场景(比如串口中断里频繁写入数据),每微秒都很宝贵。
具体来说,环形缓冲区有两个最频繁的操作:
当 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 的二进制特点是:bitn
n
n 为1
1
1,其余均为0
0
0(例如2
3
2^3
23→ bit3
3
3 =1 → 1000); -
2
n
−
1
2^n-1
2n−1 的二进制特点是:bit0
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 ,则其 bitn
n
n 为1
1
1,其余均为0
0
0;2
n
−
1
2^n-1
2n−1 的 bit0
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 和 out 是单调递增的无符号整数(uint32_t),代表写入和读出的累计计数。则已用字节数 = in – out,即使 in 或 out 溢出回绕,差值仍然准确。
-
计算写入起始物理位置
-
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 队列就是正确工具。


