
一、普通线性缓冲区的痛点我们先看一个简单的线性缓冲区示例uint8_t buffer[8] {0}; // 长度为8的数组当依次写入1、2、3…8时数组会被填满。如果继续写入9、10等新数据数组没有多余空间就会发生溢出新数据无处存放直接导致丢失。核心问题总结空间单向使用数组空间是一次性的填满后无法循环利用。易丢失数据持续写入时超出数组长度的数据会被丢弃。稳定性差在串口、网络等持续收发数据的场景中溢出会直接导致通信失败。环形缓冲区工作原理示意图为了更直观地理解环形缓冲区的工作机制下面用 Mermaid 流程图展示数据写入、指针移动和循环覆盖的过程flowchart TD subgraph A [初始化状态] direction LR A1[数组元素: 空] -- A2[写指针: 0] -- A3[读指针: 0] end subgraph B [写入数据 1-8] direction LR B1[数据写入: 1-8] --gt; B2[写指针移动: 0→8] --gt; B3[读指针保持: 0] B4[数组状态: 满] end subgraph C [写入数据 9] direction LR C1[数据写入: 9] --gt; C2[写指针循环: 8→0] --gt; C3[覆盖旧数据: 位置0] --gt; C4[读指针移动: 0→1] end subgraph D [写入数据 10] direction LR D1[数据写入: 10] --gt; D2[写指针移动: 0→1] --gt; D3[覆盖旧数据: 位置1] --gt; D4[读指针移动: 1→2] end subgraph E [读取数据] direction LR E1[数据读取: 从读指针位置] --gt; E2[读指针移动: 循环递增] --gt; E3[释放空间: 可重新写入] end A --gt; B --gt; C --gt; D --gt; E style A fill:#e1f5fe style B fill:#f3e5f5 style C fill:#e8f5e8 style D fill:#fff3e0 style E fill:#fce4ec/code/pre 流程图说明 初始化状态数组为空写指针和读指针都指向起始位置索引0。 写入数据1-8数据依次写入数组写指针从0移动到8数组末尾读指针保持为0此时缓冲区满。 写入数据9当缓冲区满时新数据9需要写入。写指针循环回到数组开头索引0覆盖该位置的旧数据1同时读指针向前移动一位从0到1释放已读空间。 写入数据10写指针继续向后移动从0到1覆盖位置1的旧数据2读指针也相应移动从1到2。 读取数据从读指针位置读取数据读指针循环递增释放的空间可被后续写入操作重新利用。 通过这种循环覆盖机制环形缓冲区实现了空间的重复利用避免了线性缓冲区的溢出问题同时通过读写指针分离保证了数据的有序存取。 二、环形缓冲区的核心原理 环形缓冲区的本质是通过代码逻辑将线性数组虚拟成一个首尾相连的圆环底层依然是固定长度的数组不会额外占用内存。 工作逻辑 虚拟环形结构将数组的首尾通过指针逻辑连接形成循环存储的效果。 循环利用空间当数组写满后新数据会从数组开头重新写入覆盖旧数据。 读写分离通过写指针和读指针分别记录写入和读取的位置实现数据的有序存取。 示例演示 以长度为8的数组为例 写入1~8数组被填满写指针移动到数组末尾。 写入9写指针回到数组开头覆盖第一个位置存入9。 写入10写指针继续向后移动存入第二个位置。 以此类推数据会在数组中循环存储不会因为写满而溢出。 三、C语言完整实现 下面是一个可直接使用的环形缓冲区实现支持数据的写入、读取和清空操作。 1. 结构体定义 #include stdint.h #include stdbool.h #include string.h // 环形缓冲区结构体 typedef struct { uint8_t *buffer; // 数据缓冲区 uint16_t size; // 缓冲区总大小 uint16_t write; // 写指针 uint16_t read; // 读指针 } RingBuffer; 2. 初始化函数 /** * brief 初始化环形缓冲区 * param rb 环形缓冲区指针 * param buffer 数据缓冲区指针 * param size 缓冲区大小 */ void ring_buffer_init(RingBuffer *rb, uint8_t *buffer, uint16_t size) { rb-buffer buffer; rb-size size; rb-write 0; rb-read 0; } 3. 写入数据 /** * brief 向环形缓冲区写入数据 * param rb 环形缓冲区指针 * param data 要写入的数据 * return true写入成功false缓冲区已满 */ bool ring_buffer_write(RingBuffer *rb, uint8_t data) { // 计算下一个写指针位置 uint16_t next_write (rb-write 1) % rb-size; // 缓冲区已满写指针追上读指针 if (next_write rb-read) { return false; } // 写入数据并更新写指针 rb-buffer[rb-write] data; rb-write next_write; return true; } 4. 读取数据 /** * brief 从环形缓冲区读取数据 * param rb 环形缓冲区指针 * param data 读取到的数据指针 * return true读取成功false缓冲区为空 */ bool ring_buffer_read(RingBuffer *rb, uint8_t *data) { // 缓冲区为空 if (rb-read rb-write) { return false; } // 读取数据并更新读指针 *data rb-buffer[rb-read]; rb-read (rb-read 1) % rb-size; return true; } 5. 清空缓冲区 /** * brief 清空环形缓冲区 * param rb 环形缓冲区指针 */ void ring_buffer_clear(RingBuffer *rb) { rb-write 0; rb-read 0; memset(rb-buffer, 0, rb-size); } 四、使用示例 #include stdio.h int main() { // 定义缓冲区 uint8_t buffer[8]; RingBuffer rb; // 初始化环形缓冲区 ring_buffer_init(rb, buffer, 8); // 写入数据 for (uint8_t i 1; i lt; 10; i) { if (ring_buffer_write(rb, i)) { printf(写入数据%d\n, i); } else { printf(缓冲区已满无法写入%d\n, i); } } // 读取数据 uint8_t data; while (ring_buffer_read(rb, data)) { printf(读取数据%d\n, data); } return 0; } 运行结果 写入数据1 写入数据2 写入数据3 写入数据4 写入数据5 写入数据6 写入数据7 写入数据8 缓冲区已满无法写入9 缓冲区已满无法写入10 读取数据1 读取数据2 读取数据3 读取数据4 读取数据5 读取数据6 读取数据7 读取数据8 五、进阶优化 1. 支持批量读写 可以扩展函数支持一次写入或读取多个字节 bool ring_buffer_write_batch(RingBuffer *rb, uint8_t *data, uint16_t len) { for (uint16_t i 0; i len; i) { if (!ring_buffer_write(rb, data[i])) { return false; } } return true; } bool ring_buffer_read_batch(RingBuffer *rb, uint8_t *data, uint16_t len) { for (uint16_t i 0; i len; i) { if (!ring_buffer_read(rb, data[i])) { return false; } } return true; } 2. 支持覆盖旧数据 如果需要在缓冲区满时覆盖旧数据可以修改写入函数 bool ring_buffer_write_override(RingBuffer *rb, uint8_t data) { uint16_t next_write (rb-write 1) % rb-size; // 缓冲区满时移动读指针 if (next_write rb-read) { rb-read (rb-read 1) % rb-size; } rb-buffer[rb-write] data; rb-write next_write; return true; } 六、总结 环形缓冲区是嵌入式开发中非常实用的数据结构它通过虚拟环形的方式解决了线性缓冲区溢出丢包的问题同时保持了低内存占用的优势。在串口通信、网络数据处理、传感器数据采集等场景中环形缓冲区都是必备的工具。 如果你觉得这篇文章对你有帮助欢迎点赞、收藏、关注后续会带来更多嵌入式开发的实用技巧 七、实战应用场景 环形缓冲区在嵌入式系统中应用广泛以下是三个典型场景的详细说明 1. 串口通信 在串口通信中数据以字节流形式异步到达环形缓冲区作为接收缓存可以有效应对数据突发和接收速度不匹配的问题。典型流程串口接收中断服务程序ISR将接收到的字节写入环形缓冲区主循环则从缓冲区读取并处理数据。注意事项缓冲区大小需根据波特率和数据处理速度合理设置避免因处理不及时导致缓冲区满而丢失数据同时要考虑中断与主循环的共享资源访问通常需要在写入和读取时临时关闭中断或使用原子操作。 2. 网络数据包接收 在嵌入式网络协议栈如 TCP/IP中环形缓冲区常用于存储接收到的以太网帧或 IP 数据包。网卡驱动将接收到的数据包存入缓冲区协议栈上层按序取出并解析。注意事项由于网络数据包长度可变通常需要结合长度字段或定界符来标识包边界缓冲区容量应能容纳多个最大传输单元MTU防止高流量下丢包在多任务系统中可能需要使用信号量或互斥锁保护缓冲区。 3. 传感器数据采集 传感器如加速度计、陀螺仪通常以固定频率输出数据环形缓冲区作为数据暂存区实现采集与处理的解耦。采集线程或定时器中断将传感器读数写入缓冲区数据处理线程如滤波、融合算法从缓冲区读取历史数据进行批量计算。注意事项采样频率与处理速度需匹配避免缓冲区溢出对于多轴传感器可将各通道数据打包为结构体再存入缓冲区保证数据同步在实时性要求高的场景可结合 DMA 进一步降低 CPU 负载。