一、核心原理回顾
环形缓冲区的本质是一个首尾相连的数组,通过两个指针维护数据状态:
- 写指针(wr_ptr):指向下一个可写入的位置
- 读指针(rd_ptr):指向下一个可读取的位置
- 空判断:
wr_ptr == rd_ptr表示缓冲区无数据 - 满判断:采用“牺牲一格”策略,当
(wr_ptr + 1) % 缓冲区大小 == rd_ptr时判定为满,避免空满状态冲突
二、完整代码实现
1. 头文件定义(ring_buffer.h)
#ifndef __RING_BUFFER_H #define __RING_BUFFER_H #include <stdint.h> #include <stdbool.h> #include <string.h> // 缓冲区大小可根据实际需求调整,建议为2的幂次优化模运算 #define RING_BUF_SIZE 64 // 环形缓冲区结构体定义 typedef struct { uint8_t buffer[RING_BUF_SIZE]; // 数据存储数组 uint16_t wr_ptr; // 写指针 uint16_t rd_ptr; // 读指针 } ring_buf_t; // 函数声明 void ring_buf_init(ring_buf_t *rb); bool ring_buf_is_empty(ring_buf_t *rb); bool ring_buf_is_full(ring_buf_t *rb); uint16_t ring_buf_write(ring_buf_t *rb, const uint8_t *data, uint16_t len); uint16_t ring_buf_read(ring_buf_t *rb, uint8_t *data, uint16_t len); uint16_t ring_buf_used_size(ring_buf_t *rb); // 新增:获取已用空间 uint16_t ring_buf_free_size(ring_buf_t *rb); // 新增:获取剩余空间 #endif2. 功能实现(ring_buffer.c)
#include "ring_buffer.h" /** @brief 初始化环形缓冲区 @param rb 缓冲区结构体指针 */ void ring_buf_init(ring_buf_t *rb) { if (rb == NULL) return; // 初始化指针为0 rb->wr_ptr = 0; rb->rd_ptr = 0; // 清空缓冲区数据 memset(rb->buffer, 0, sizeof(rb->buffer)); } /** @brief 判断缓冲区是否为空 @param rb 缓冲区结构体指针 @return true: 空 false: 非空 */ bool ring_buf_is_empty(ring_buf_t *rb) { return (rb->wr_ptr == rb->rd_ptr); } /** @brief 判断缓冲区是否为满 @param rb 缓冲区结构体指针 @return true: 满 false: 未满 */ bool ring_buf_is_full(ring_buf_t *rb) { return ((rb->wr_ptr + 1) % RING_BUF_SIZE == rb->rd_ptr); } /** @brief 获取缓冲区已用空间大小 @param rb 缓冲区结构体指针 @return 已使用的字节数 */ uint16_t ring_buf_used_size(ring_buf_t *rb) { if (rb == NULL) return 0; return (rb->wr_ptr - rb->rd_ptr + RING_BUF_SIZE) % RING_BUF_SIZE; } /** @brief 获取缓冲区剩余空间大小 @param rb 缓冲区结构体指针 @return 剩余可用字节数 */ uint16_t ring_buf_free_size(ring_buf_t *rb) { if (rb == NULL) return 0; return (RING_BUF_SIZE - 1 - ring_buf_used_size(rb)) % RING_BUF_SIZE; } /** @brief 向缓冲区写入数据 @param rb 缓冲区结构体指针 @param data 待写入数据指针 @param len 待写入数据长度 @return 实际写入的字节数 */ uint16_t ring_buf_write(ring_buf_t *rb, const uint8_t *data, uint16_t len) { if (rb == NULL || data == NULL || len == 0) return 0; uint16_t write_cnt = 0; for (uint16_t i = 0; i < len; i++) { if (ring_buf_is_full(rb)) break; // 写入数据并移动指针 rb->buffer[rb->wr_ptr] = data[i]; rb->wr_ptr = (rb->wr_ptr + 1) % RING_BUF_SIZE; write_cnt++; } return write_cnt; } /** @brief 从缓冲区读取数据 @param rb 缓冲区结构体指针 @param data 存储读取数据的指针 @param len 期望读取的长度 @return 实际读取的字节数 */ uint16_t ring_buf_read(ring_buf_t *rb, uint8_t *data, uint16_t len) { if (rb == NULL || data == NULL || len == 0) return 0; uint16_t read_cnt = 0; for (uint16_t i = 0; i < len; i++) { if (ring_buf_is_empty(rb)) break; // 读取数据并移动指针 data[i] = rb->buffer[rb->rd_ptr]; rb->rd_ptr = (rb->rd_ptr + 1) % RING_BUF_SIZE; read_cnt++; } return read_cnt; }3. 使用示例(main.c)
#include <stdio.h> #include "ring_buffer.h" int main() { // 定义并初始化缓冲区 ring_buf_t uart_rx_buf; ring_buf_init(&uart_rx_buf); // 测试数据 uint8_t test_data[] = {0x01, 0x02, 0x03, 0x04, 0x05}; uint8_t read_buf[10] = {0}; // 写入数据 uint16_t write_len = ring_buf_write(&uart_rx_buf, test_data, 5); printf("写入 %d 字节数据\n", write_len); // 读取数据 uint16_t read_len = ring_buf_read(&uart_rx_buf, read_buf, 5); printf("读取 %d 字节数据: ", read_len); for (uint16_t i = 0; i < read_len; i++) { printf("0x%02X ", read_buf[i]); } printf("\n"); // 测试空满状态 printf("缓冲区是否为空: %s\n", ring_buf_is_empty(&uart_rx_buf) ? "是" : "否"); printf("缓冲区是否为满: %s\n", ring_buf_is_full(&uart_rx_buf) ? "是" : "否"); return 0; }三、关键设计说明
- 结构体封装:将缓冲区数组和指针封装在同一个结构体中,便于管理多个独立的缓冲区(如串口接收、发送各一个)
- 安全检查:所有函数都加入了空指针判断,避免嵌入式系统中常见的崩溃问题
- 原子操作:在中断中使用时,建议在读写操作前关闭全局中断,防止指针竞争
- 性能优化:缓冲区大小设为2的幂次时,模运算可优化为位与操作(
(wr_ptr + 1) & (RING_BUF_SIZE - 1)),提升运行效率
四、常见问题与扩展
- 如何获取缓冲区已用空间和剩余空间?使用新增的实用函数:
// 获取已用空间 uint16_t used = ring_buf_used_size(&uart_rx_buf); printf("已用空间: %d 字节\n", used); // 获取剩余空间 uint16_t free = ring_buf_free_size(&uart_rx_buf); printf("剩余空间: %d 字节\n", free); // 验证:已用 + 剩余 = 总容量-1(牺牲一格策略) printf("验证: %d + %d = %d (总容量 %d - 1)\n", used, free, used + free, RING_BUF_SIZE); - 支持多字节数据?只需修改
buffer类型为uint16_t或自定义结构体,指针逻辑保持不变 - 内存不足怎么办?可动态分配缓冲区内存,将结构体中的
buffer改为指针类型,初始化时传入外部数组
这套实现方案已在多个嵌入式项目中验证,可直接用于串口、CAN、SPI等外设的数据收发场景。