news 2026/3/3 23:47:20

嵌入式开发居然有这神操作?环形缓冲区让数据读写快到飞起!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式开发居然有这神操作?环形缓冲区让数据读写快到飞起!

嵌入式开发居然有这神操作?环形缓冲区让数据读写快到飞起!

你是不是也遇到过这样的崩溃时刻:嵌入式开发里,串口数据刚收到就丢失、传感器数据缓存半天读不出来,或者普通数组当缓冲区时,数据搬来搬去搞得程序卡顿不已?明明是简单的数据存储需求,却硬生生变成了让人头秃的难题?

别急!今天要给大家安利一个嵌入式开发的“宝藏工具”——环形缓冲区(也叫Ring Buffer)。它就像一个会自动循环的“数据仓库”,不用手动搬运数据,读写速度快到离谱,堪称串口通信、传感器数据缓存、嵌入式系统数据处理的“救星”!看完这篇,你再也不用为数据缓存问题熬夜秃头啦~

一、环形缓冲区:嵌入式开发的“循环仓库”

先给大家通俗解释下:环形缓冲区就是一个固定大小的“先进先出(FIFO)”数据结构,你可以把它想象成一个圆形的传送带,数据就像传送带上的包裹,先放上去的先被取走。

它最神奇的地方在于“循环复用”——当数据写到缓冲区末尾时,不用把前面的数据往前挪,而是直接绕回开头继续写;读取数据也是一样,读到末尾就自动回到起点。这种设计直接省去了普通数组缓冲区的“数据搬移”麻烦,读写效率直接拉满!

和普通数组缓冲区比起来,环形缓冲区的优势简直碾压:

  • 无数据搬移开销:不用浪费CPU资源挪数据,省下来的性能能做更多正事;
  • 读写效率超高:不管缓冲区里有多少数据,读写操作都是O(1)时间复杂度(简单说就是“一秒完成”);
  • 内存利用率高:固定大小的内存能循环使用,不会造成内存浪费。

不管是单片机裸机开发、串口数据接收,还是传感器实时数据缓存,环形缓冲区都能轻松hold住,说是嵌入式开发的“必备神器”一点不为过~

二、核心实现原理:3个要素搞定“循环魔法”

很多人觉得环形缓冲区听起来高深,其实核心原理特别简单,就靠“2个指针+1个固定数组”,所有逻辑都围绕这三者展开,咱们一步步拆解开讲:

1. 基础组成:3个核心要素

  • 存储数组:这是数据的“物理仓库”,就是一块固定长度的连续内存,数据就实实在在存在这里;
  • 读指针(rd_idx):相当于仓库的“取货员”,永远指向下一个要读取的数据位置,取完货就自动往前走;
  • 写指针(wr_idx):相当于仓库的“送货员”,永远指向下一个要存放数据的位置,送完货也自动往前走。

这俩指针就像仓库里配合默契的搭档,一个只管存,一个只管取,分工明确还不打架~

2. 关键难题:怎么区分“空”和“满”?

环形缓冲区有个特别有意思的问题:当读指针和写指针指向同一个位置时,到底是缓冲区空了(所有数据都读完了),还是满了(所有位置都存满了)?这就像仓库里“送货员”和“取货员”站在同一个 spot,你没法直接判断是货送完了还是仓库满了。

业界最通用、最容易实现的解决方案来了——预留1个字节的存储空间不用!就像仓库特意留了一个“空位”,用这个小小的牺牲,换来了绝对无歧义的判断规则,性价比超高:

  • 缓冲区为空:读指针 == 写指针(rd_idx == wr_idx)→ 相当于“送货员”和“取货员”碰面了,仓库里没货了;
  • 缓冲区为满:(wr_idx + 1) % 缓冲区总大小 == rd_idx → 相当于“送货员”往前一步就追上“取货员”了,仓库里再也塞不下新货;
  • 有效存储容量:缓冲区总大小 - 1 → 比如总大小是8,实际能存7个数据,少一个位置换“不纠结”,太值了!

这里的“%”(取模运算)是实现“环形”的核心魔法!它能让指针走到数组末尾时,自动“瞬移”回起点。比如缓冲区大小是8,当写指针到7(最后一个位置)时,(7+1)%8=0,直接回到开头,完美实现循环~

3. 核心操作:读写数据原来这么简单

搞懂了指针和判断规则,读写操作就像“按流程办事”,一步都不复杂:

  • 写数据:先看看缓冲区满没满(用上面的满判断规则),如果没满,就把数据放到写指针当前的位置,然后写指针往前挪一步(记得用取模运算实现循环);
  • 读数据:先看看缓冲区空没空(用上面的空判断规则),如果没空,就从读指针当前的位置取数据,然后读指针往前挪一步(同样用取模运算)。

整个过程没有多余的步骤,不用搬数据,不用等时间,效率直接拉满~

三、完整可运行代码:拿来就用,附带测试用例

说了这么多理论,不如直接上代码!下面是完整的环形缓冲区实现代码,支持字节型数据缓存,包含初始化、判空、判满、写数据、读数据、获取有效数据量全套核心接口,还附带测试用例,无任何依赖,直接编译就能运行~

#include<stdio.h>#include<stdint.h>#include<stdbool.h>// ===================== 配置区(可按需修改,像搭积木一样简单)=====================#defineBUF_SIZE8// 缓冲区总大小,实际可用 BUF_SIZE-1=7 个位置(预留1个防歧义)typedefuint8_tbuf_data_t;// 缓冲区存储的数据类型(支持uint8/uint16/int等,后续教你修改)// ===================== 环形缓冲区结构体定义(相当于给“仓库”画设计图)=====================typedefstruct{buf_data_tbuffer[BUF_SIZE];// 数据存储数组(仓库的“货架”)uint16_trd_idx;// 读指针(取货员的位置)uint16_twr_idx;// 写指针(送货员的位置)}RingBuffer_t;// ===================== 核心接口声明(告诉编译器有这些“工具函数”)=====================voidring_buf_init(RingBuffer_t*rb);// 初始化缓冲区(给仓库开业做准备)boolring_buf_is_empty(RingBuffer_t*rb);// 判断缓冲区是否为空(仓库没货了吗?)boolring_buf_is_full(RingBuffer_t*rb);// 判断缓冲区是否为满(仓库放不下了吗?)boolring_buf_write(RingBuffer_t*rb,buf_data_tdata);// 写入1个数据(送货员放包裹)boolring_buf_read(RingBuffer_t*rb,buf_data_t*data);// 读取1个数据(取货员拿包裹)uint16_tring_buf_get_len(RingBuffer_t*rb);// 获取有效数据长度(仓库里现在有多少货?)// ===================== 核心接口实现(工具函数的具体用法)=====================/** * @brief 初始化环形缓冲区 * 作用:给读指针和写指针“分配初始位置”,让仓库准备好接收数据 */voidring_buf_init(RingBuffer_t*rb){if(rb==NULL)return;// 防止传入空指针,避免程序崩溃(安全第一!)rb->rd_idx=0;// 读写指针都从0位置开始(送货员和取货员都站在起点)rb->wr_idx=0;}/** * @brief 判断缓冲区是否为空 * 返回值:true=空,false=非空 */boolring_buf_is_empty(RingBuffer_t*rb){if(rb==NULL)returntrue;// 指针为空就默认是空缓冲区return(rb->rd_idx==rb->wr_idx);// 读写指针碰面=没货了}/** * @brief 判断缓冲区是否为满 * 返回值:true=满,false=未满 */boolring_buf_is_full(RingBuffer_t*rb){if(rb==NULL)returntrue;// 指针为空就默认是满缓冲区(避免出错)// 核心判断:写指针下一个位置 == 读指针 → 仓库满了return((rb->wr_idx+1)%BUF_SIZE)==rb->rd_idx;}/** * @brief 写入单个数据到缓冲区 * @param data:要写入的数据(要送的包裹) * @return true=写入成功,false=缓冲区满写入失败 */boolring_buf_write(RingBuffer_t*rb,buf_data_tdata){// 先判断:指针为空或者仓库满了,就不写了if(rb==NULL||ring_buf_is_full(rb)){returnfalse;}rb->buffer[rb->wr_idx]=data;// 把数据放到写指针当前位置(放包裹)rb->wr_idx=(rb->wr_idx+1)%BUF_SIZE;// 写指针后移,循环处理(送货员往前走)returntrue;}/** * @brief 从缓冲区读取单个数据 * @param data:输出参数,存储读取到的数据(拿包裹的容器) * @return true=读取成功,false=缓冲区空读取失败 */boolring_buf_read(RingBuffer_t*rb,buf_data_t*data){// 先判断:指针为空、没给容器,或者仓库没货,就不读了if(rb==NULL||data==NULL||ring_buf_is_empty(rb)){returnfalse;}*data=rb->buffer[rb->rd_idx];// 从读指针位置取数据(拿包裹)rb->rd_idx=(rb->rd_idx+1)%BUF_SIZE;// 读指针后移,循环处理(取货员往前走)returntrue;}/** * @brief 获取缓冲区中有效数据的长度 * 返回值:有效数据个数(仓库里现有包裹数量) */uint16_tring_buf_get_len(RingBuffer_t*rb){if(rb==NULL)return0;// 指针为空就返回0个数据// 计算方式:(写指针 - 读指针 + 缓冲区大小) % 缓冲区大小,避免负数return(rb->wr_idx-rb->rd_idx+BUF_SIZE)%BUF_SIZE;}// ===================== 测试用例(验证功能好不好用,放心抄)=====================intmain(void){RingBuffer_t rb;// 定义一个环形缓冲区对象(新建一个仓库)buf_data_twrite_data=0,read_data;// 要写入的数据和读取到的数据uint8_ti;// 1. 初始化缓冲区(仓库开业)ring_buf_init(&rb);printf("=== 环形缓冲区测试(总大小%d,可用大小%d)===\n",BUF_SIZE,BUF_SIZE-1);// 2. 写入7个数据(仓库最大可用容量)printf("\n1. 写入7个数据:");for(i=0;i<7;i++){write_data++;// 数据从1开始递增if(ring_buf_write(&rb,write_data)){printf("%d ",write_data);// 打印成功写入的数据}}printf("\n当前有效数据量:%d\n",ring_buf_get_len(&rb));printf("缓冲区是否为满:%s\n",ring_buf_is_full(&rb)?"是":"否");// 3. 尝试写入第8个数据(应该失败,因为仓库满了)if(!ring_buf_write(&rb,++write_data)){printf("2. 写入第8个数据失败(缓冲区已满)\n");}// 4. 读取所有数据(把仓库里的货全拿出来)printf("3. 读取所有数据:");while(!ring_buf_is_empty(&rb)){if(ring_buf_read(&rb,&read_data)){printf("%d ",read_data);// 打印读取到的数据}}printf("\n当前有效数据量:%d\n",ring_buf_get_len(&rb));printf("缓冲区是否为空:%s\n",ring_buf_is_empty(&rb)?"是":"否");// 5. 空缓冲区读取(应该失败,因为没货了)if(!ring_buf_read(&rb,&read_data)){printf("4. 空缓冲区读取失败(符合预期)\n");}return0;}

四、编译运行结果:一目了然,功能拉满

写完代码怎么验证好不好用?用GCC编译就行,编译命令超简单:

gcc ring_buf.c -o ring_buf && ./ring_buf

运行后会输出这样的结果,和预期完全一致,说明功能没问题:

=== 环形缓冲区测试(总大小8,可用大小7)=== 1. 写入7个数据:1 2 3 4 5 6 7 当前有效数据量:7 缓冲区是否为满:是 2. 写入第8个数据失败(缓冲区已满) 3. 读取所有数据:1 2 3 4 5 6 7 当前有效数据量:0 缓冲区是否为空:是 4. 空缓冲区读取失败(符合预期)

从输出能看到:7个数据顺利写入,第8个因为缓冲区满失败;读取时能完整读出所有数据,空缓冲区读取也会失败,完全符合我们的设计逻辑~

五、接口使用说明:4步上手,小白也会用

很多小伙伴拿到代码会犯愁:“这么多函数,我该怎么用啊?” 其实超简单,跟着这4步走,分分钟搞定:

基础使用步骤(4步走)

  1. 定义缓冲区对象:就像新建一个“仓库”,一行代码搞定:RingBuffer_t rb;
  2. 初始化缓冲区:给“仓库”做好开业准备:ring_buf_init(&rb);
  3. 写数据:往“仓库”里放数据,比如放一个0x12:ring_buf_write(&rb, 0x12);
  4. 读数据:从“仓库”里拿数据,把拿到的数据存到data里:buf_data_t data; ring_buf_read(&rb, &data);

常用接口速查表(收藏起来,随用随查)

函数名功能返回值 / 说明
ring_buf_init初始化缓冲区无,调用就行
ring_buf_is_empty判断缓冲区是否为空true=空,false=非空
ring_buf_is_full判断缓冲区是否为满true=满,false=未满
ring_buf_write写入单个数据true=成功,false=失败(缓冲区满)
ring_buf_read读取单个数据true=成功,false=失败(缓冲区空)
ring_buf_get_len获取有效数据量无符号整数(比如返回7,就是有7个有效数据)

就像查字典一样,需要什么功能就调用对应的函数,完全不用记复杂逻辑~

六、进阶扩展:按需修改,适配各种场景

基础版代码已经能满足大部分需求,但如果你的场景比较特殊,比如要存整数、要更大的缓冲区,或者想批量读写数据,不用重写代码,简单修改就行,超灵活!

1. 修改存储数据类型(想存啥就存啥)

默认代码里存的是uint8_t(字节型数据),但实际开发中可能需要存整数、浮点型,甚至自定义的结构体。没关系,只需要修改1行代码!

// 原配置(字节型,默认)typedefuint8_tbuf_data_t;// 想存整型?改这里typedefintbuf_data_t;// 想存浮点型?改这里typedeffloatbuf_data_t;// 想存自定义结构体?比如存ID和数值,改这里typedefstruct{intid;floatval;}buf_data_t;

修改后,整个缓冲区的读写都会自动适配新的数据类型,不用改其他代码,是不是超方便?

2. 修改缓冲区大小(想要多大就多大)

如果觉得默认的8个总大小不够用,想搞个更大的“仓库”,只需要修改宏定义BUF_SIZE就行,支持任意正整数(建议设为2的幂,比如64、128,取模运算效率更高):

#defineBUF_SIZE64// 总大小64,可用63个位置(预留1个防歧义)

改完重新编译,缓冲区就变成你想要的大小了,完美适配不同的存储需求~

3. 批量读写扩展(一次搞定多个数据)

有时候需要一次性写很多数据,或者一次性读很多数据,一个个调用ring_buf_writering_buf_read太麻烦?可以新增批量接口!

比如新增批量写入接口,一次写入n个数据,写满就停止,还会返回实际写入的个数:

// 批量写入n个数据uint16_tring_buf_write_batch(RingBuffer_t*rb,buf_data_t*data,uint16_tn){if(rb==NULL||data==NULL||n==0)return0;// 参数无效就返回0uint16_twrite_cnt=0;// 记录实际写入的个数for(uint16_ti=0;i<n;i++){if(ring_buf_write(rb,data[i]))write_cnt++;// 写入成功就计数elsebreak;// 缓冲区满了就停止写入}returnwrite_cnt;// 返回实际写入个数,方便判断是否写全}

批量读取接口可以照着这个逻辑写,原理一样,一次性搞定多个数据,效率更高~

七、线程安全说明:这些坑千万别踩!

重要提醒!重要提醒!重要提醒!(说三遍)

咱们这个基础版环形缓冲区,在单线程环境下用着完全没问题,但如果涉及多线程或者中断,可不能直接用,不然可能会出现数据错乱、程序崩溃的情况!

先搞懂:哪些场景是安全的?

  • 单线程使用:比如裸机单片机的主循环里,或者中断服务函数内部,只有一个“写者”和一个“读者”,还在同一个线程里 → 完全安全,放心用;
  • 多线程/中断+主循环:比如线程1写数据、线程2读数据,或者中断里写数据、主循环里读数据 → 不安全!必须加保护,不然指针操作会冲突。

线程安全改造方案(2种实用方法)

方案1:裸机/嵌入式系统(中断+主循环)→ 关闭/开启中断

如果是单片机裸机开发,经常会遇到“中断写、主循环读”的场景,这时候可以通过关闭全局中断来避免冲突,修改写数据接口就行:

// 写数据(中断中写,主循环读)boolring_buf_write_isr(RingBuffer_t*rb,buf_data_tdata){__disable_irq();// 关闭全局中断(不同MCU接口可能不同,比如关总中断)bool ret=ring_buf_write(rb,data);// 执行写入操作__enable_irq();// 开启全局中断(写完必须开,不然其他中断用不了)returnret;}

原理很简单:关闭中断后,主循环里的读操作会暂停,等写入完成再开启中断,避免读写指针“打架”。

方案2:操作系统(Linux/RTOS)→ 加互斥锁/信号量

如果是在Linux或者RTOS(比如FreeRTOS、UCOS)环境下,多线程读写的话,就需要用互斥锁来保护,比如Linux下的pthread_mutex:

pthread_mutex_trb_mutex;// 定义一个互斥锁(相当于“仓库大门钥匙”)// 线程安全的写数据接口boolring_buf_write_safe(RingBuffer_t*rb,buf_data_tdata){pthread_mutex_lock(&rb_mutex);// 加锁(拿钥匙开门,其他人不能进)bool ret=ring_buf_write(rb,data);// 执行写入操作pthread_mutex_unlock(&rb_mutex);// 解锁(还钥匙,其他人可以用了)returnret;}

互斥锁的作用就是“同一时间只有一个线程能操作缓冲区”,完美解决多线程冲突问题。

八、总结:环形缓冲区,嵌入式开发的“效率神器”

看到这里,相信你已经彻底搞懂环形缓冲区了!总结一下重点,方便大家记忆:

  1. 核心优势:固定大小FIFO,无数据搬移,读写效率O(1),嵌入式场景必备;
  2. 实现关键:靠“读写指针+取模运算”实现循环,预留1字节解决“空满判断歧义”;
  3. 适用场景:单线程直接用,多线程/中断场景加互斥保护就行;
  4. 灵活适配:修改BUF_SIZE改大小,修改buf_data_t改数据类型,批量读写可扩展;
  5. 接口齐全:初始化、判空、判满、读写、获取长度,全套接口直接复用,不用重复造轮子。

其实环形缓冲区一点都不复杂,核心逻辑就那几点,掌握后能解决嵌入式开发中很多数据缓存的难题。赶紧把代码抄过去试试,体验一下“数据读写快到飞起”的快乐吧!如果有其他扩展需求,也可以根据上面的进阶方法自己修改,超级灵活~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/3 11:04:12

量子计算机实现无条件指数级优势突破

量子计算机刚刚击败了经典计算机——指数级且无条件地 量子计算机有潜力加速计算、帮助设计新药物、破译密码以及发现奇异的材料&#xff0c;但这只有在它们真正能运行时才成立。 其中一个关键阻碍是&#xff1a;噪声&#xff0c;或者说在量子机器上计算过程中产生的错误——…

作者头像 李华
网站建设 2026/3/2 18:41:32

MinerU适合中小企业吗?低成本文档自动化落地案例

MinerU适合中小企业吗&#xff1f;低成本文档自动化落地案例 1. 中小企业文档处理的现实困境 你有没有遇到过这样的情况&#xff1a;公司每天要处理几十份产品说明书、合同、技术白皮书&#xff0c;全是PDF格式。人工一页页复制粘贴不仅费时&#xff0c;还容易出错——表格错…

作者头像 李华
网站建设 2026/3/2 12:48:08

【Matlab】MATLAB数值转逻辑:从批量转换到条件筛选,简化数据处理逻辑

精通MATLAB数值转逻辑:从批量转换到条件筛选,简化数据处理逻辑 在MATLAB数据处理中,数值转逻辑是连接数值计算与条件判断的重要转换,其核心是将数值按“0/非0”规则映射为逻辑类型(logical)的false或true。这种转换能大幅简化条件筛选、状态判断的代码逻辑,避免复杂的关…

作者头像 李华
网站建设 2026/3/3 10:08:56

象过河多端通用进销存:PC + 手机 + PAD 同步 随时随地管好企业业务

在中小企业的日常经营中&#xff0c;进销存管理是核心环节&#xff0c;可不少商家都遇到过这样的难题&#xff1a;坐在电脑前才能录单查库存&#xff0c;外出谈业务时没法实时掌握数据&#xff1b;销售人员现场接单&#xff0c;回头再录入系统容易出错&#xff1b;多部门数据不…

作者头像 李华
网站建设 2026/3/3 23:42:47

充电桩漏电流检测的重要性

充电桩作为电动汽车的核心配套设施&#xff0c;其安全性直接关系到用户生命财产和电网稳定。漏电流检测是充电桩安全保护的关键环节&#xff0c;能够有效预防因绝缘故障、设备老化或环境潮湿导致的漏电事故。国际标准如IEC 61851、GB/T 18487.1均明确要求充电桩必须配备漏电流保…

作者头像 李华
网站建设 2026/3/3 20:26:47

Glyph视觉推理实战:把技术文档变成可读图像

Glyph视觉推理实战&#xff1a;把技术文档变成可读图像 你有没有遇到过这样的情况&#xff1a;手头有一份几十页的技术文档&#xff0c;密密麻麻全是文字&#xff0c;想快速抓住重点却无从下手&#xff1f;或者需要向团队讲解某个复杂系统设计&#xff0c;但光靠PPT和口头描述…

作者头像 李华