1、环形缓冲区的概念
环形缓冲区和缓冲区一样,都是通过向内存申请一段空间,用来存放数据。
环形缓冲区(Ring Buffer)是一种特殊类型的缓冲区,数据在缓冲区的末尾会绕回到开头,形成一个环形结构。它通常包含一个缓冲区数组和两个指针(head
和tail
),分别指向读取和写入的位置。
2、环形缓冲区和缓冲区的区别
2.1 普通缓冲区
一段连续的线性内存空间,读写操作从头到尾单向进行。
频繁申请/释放导致内存利用率下降
2.2 环形缓冲区
物理上连续,逻辑上首尾相连的循环内存空间。
2.3 两者的优势比对
2.3.1 内存效率
指标 | 普通缓冲区 | 环形缓冲区 |
---|---|---|
内存利用率 | 低(需预留安全区或频繁搬移数据) | 高(循环覆盖,无冗余搬移) |
碎片风险 | 高(动态分配易碎片化) | 无(固定预分配,无动态管理) |
2.3.2 时间效率
指标 | 普通缓冲区 | 环形缓冲区 |
---|---|---|
写入/读取 | O(n)(可能需数据搬移) | O(1)(仅指针移动) |
并发性能 | 低(需加锁保护整体缓冲区) | 高(无锁设计潜力,读写指针分离) |
2.3.2 实时性与确定性
指标 | 普通缓冲区 | 环形缓冲区 |
---|---|---|
实时响应 | 不确定(受数据搬移影响) | 确定(恒定时间操作) |
中断安全性 | 低(长时临界区) | 高(原子操作指针,短时临界区) |
2.4 应用场景
2.4.1 普通缓冲区应用场景
- 小数据块的非频繁操作
- 静态数据存储
2.4.2 环形缓冲区应用场景
场景 | 优势体现 |
---|---|
UART/USART通信 | DMA+环形缓冲区实现零拷贝传输,避免中断频繁触发(如115200bps高速传输)。 |
ADC/DAC数据流处理 | 连续采样数据缓存,确保实时性(如音频信号处理)。 |
RTOS任务间通信 | 无锁队列传递消息,提升多任务效率(如FreeRTOS的xQueue 内部实现)。 |
网络数据包缓冲 | 高效管理突发流量,防止丢包(如LWIP协议栈的pbuf 链式缓冲区)。 |
3、STM32中环形缓冲区实现
1、环形缓冲区要知道前一时刻的数据包存放在环形缓冲区的帧头和帧尾指针
2、知道环形缓冲区的剩余空间大小
3、知道环形缓冲区的大小
4、知道环形缓冲区的首地址
写入数据时帧尾动,读数据完修改帧头。
3.1 创建环形缓冲区
typedef struct {
uint32_t rbCapacity; //空间大小
char *rbHead; //头
char *rbTail; //尾
char *rbBuff; //数组的首地址
}rb_t;extern rb_t pRb;
//创建或者说初始化环形缓冲区
void rbCreate(rb_t* rb,void *Buff,uint32_t BuffLen)
{
if(NULL == rb)
{
printf("ERROR: input rb is NULL\n");
return;
}
rb->rbCapacity = BuffLen;
rb->rbBuff = Buff;
rb->rbHead = rb->rbBuff; //头指向数组首地址
rb->rbTail = rb->rbBuff; //尾指向数组首地址
}
创建一个环形缓冲区,知道环形缓冲区的起始地址;空间容量;帧头和帧尾指向起始地址;
3.2 删除一个环形缓冲区
void rbDelete(rb_t* rb)//删除一个环形缓冲区
{
if(NULL == rb)
{
printf("ERROR: input rb is NULL\n");
return;
}rb->rbBuff = NULL; //地址赋值为空
rb->rbHead = NULL; //头地址为空
rb->rbTail = NULL; //尾地址尾空
rb->rbCapacity = 0; //长度为空
}
3.3 获取链表长度
int32_t rbCapacity(rb_t *rb)//获取链表的长度
{
if(NULL == rb)
{
printf("ERROR: input rb is NULL\n");
return -1;
}return rb->rbCapacity;
}
获取环形缓冲区的容量
3.4 查看当前可读数据量
int32_t rbCanRead(rb_t *rb)//返回能读的空间 当前可读取的数据量 写入了多少数据到环形缓冲区
{
if(NULL == rb)
{
printf("ERROR: input rb is NULL\n");
return -1;
}if (rb->rbHead == rb->rbTail)//头与尾相遇
{
return 0;
}if (rb->rbHead < rb->rbTail)//尾大于头
{
return rb->rbTail - rb->rbHead;
}return rbCapacity(rb) - (rb->rbHead - rb->rbTail);//头大于尾
}

3.5 查看当前可写入空间
int32_t rbCanWrite(rb_t *rb)//返回能写入的空间、剩余可操作空间
{
if(NULL == rb)
{
printf("ERROR: input rb is NULL\n");
return -1;
}return rbCapacity(rb) - rbCanRead(rb);//总的减去已经写入的空间
}
可写入的空间就是总空间-已经被占用空间(写入数据,还未被读出)
3.6 从环形缓冲区读数据
int32_t rbRead(rb_t *rb, void *data, uint32_t count)
{
int copySz = 0;if(NULL == rb) // 检测环形缓冲区
{
printf("ERROR: input rb is NULL\n");
return -1;
}if(NULL == data) // 检测传入数据
{
printf("ERROR: input data is NULL\n");
return -1;
}if (rb->rbHead < rb->rbTail) //尾大于头 数据从环形缓冲区拿出以后,帧头的位子就要移动到数据最后一个的地址
{
copySz = min(count, rbCanRead(rb)); //查看能读的个数
memcpy(data, rb->rbHead, copySz); //读出数据到data
rb->rbHead += copySz; //头指针加上读取的个数
return copySz; //返回读取的个数
}
else //头大于等于了尾
{
if (count < rbCapacity(rb)-(rb->rbHead - rb->rbBuff))//读的个数小于头上面的数据量
{
copySz = count;//读出的个数
memcpy(data, rb->rbHead, copySz);//
rb->rbHead += copySz;
return copySz;
}
else//读的个数大于头上面的数据量
{
copySz = rbCapacity(rb) - (rb->rbHead - rb->rbBuff);//先读出来头上面的数据
memcpy(data, rb->rbHead, copySz);
rb->rbHead = rb->rbBuff;//头指针指向数组的首地址
//还要读的个数
copySz += rbRead(rb, (char*)data+copySz, count-copySz);//接着读剩余要读的个数
return copySz;
}
}
}

左边图:
数据存放在绿色区域,首先确定要获取的数据个数和这个绿色区域的容量谁大,取最小(最大获取绿色那么多数据,能实现获取一部分数据,无法实现获取更多数据)
将绿色部分的数据拷贝到一个buf中,返回取出数据的个数值。数据的起始地址为帧头,拷贝完成后帧头就指向数据被读出的尾部。
右边图:
判断要读取的数据个数在哪个阶段
如果要读取的数据个数是红色部分,直接这个数据个数的,存入到buf中,并记录获取了多少数据,帧头指针指向到红色部分。
如果要读取的数据个数是黑色部分,首先将帧头到基地址这段空间(顺时针)的数据获取出来,帧头指向基地址,然后就是左图的操作步骤,
3.7 向环形缓冲区写数据
int32_t rbWrite(rb_t *rb, const void *data, uint32_t count)
{
int tailAvailSz = 0;if(NULL == rb)
{
printf("ERROR: rb is empty \n");
return -1;
}if(NULL == data)
{
printf("ERROR: data is empty \n");
return -1;
}if (count >= rbCanWrite(rb))//如果剩余的空间不够
{
printf("ERROR: no memory \n");
return -1;
}if (rb->rbHead <= rb->rbTail)//头小于等于尾
{
tailAvailSz = rbCapacity(rb) - (rb->rbTail - rb->rbBuff);//查看尾上面剩余的空间
if (count <= tailAvailSz)//个数小于等于尾上面剩余的空间
{
memcpy(rb->rbTail, data, count);//拷贝数据到环形数组
rb->rbTail += count;//尾指针加上数据个数
if (rb->rbTail == rb->rbBuff+rbCapacity(rb))//正好写到最后
{
rb->rbTail = rb->rbBuff;//尾指向数组的首地址
}
return count;//返回写入的数据个数
}
else
{
memcpy(rb->rbTail, data, tailAvailSz);//填入尾上面剩余的空间
rb->rbTail = rb->rbBuff;//尾指针指向数组首地址
//剩余空间 剩余数据的首地址 剩余数据的个数
return tailAvailSz + rbWrite(rb, (char*)data+tailAvailSz, count-tailAvailSz);//接着写剩余的数据
}
}
else //头大于尾
{
memcpy(rb->rbTail, data, count);
rb->rbTail += count;
return count;
}
}
向环形缓冲区写数据,首先要确定内存空间支持这个数据的写入(内存空间比带写入的数据大)。
左边图:
非绿色区域为剩余空间,判断待写入的数据大小在基地址前能否存储,就直接存入;不可以就先写入到基地址之前,然后再将后面的数据补全存入。
右边图:
直接将数据写入到以帧尾为起始地址的地址空间中。