1. 库总体功能与目的

RingBuffer 库是一个基于 FreeRTOS 的环形缓冲区实现,主要用于高效处理数据流的存储与读取。其核心目的是解决多线程环境下的数据缓冲问题,特别适用于 BMD101 等高频数据采集场景(每秒 512×8+12 字节数据流)。

环形缓冲区(Ring Buffer)通过循环利用固定大小的内存空间,实现了数据的高效读写操作,避免了线性缓冲区频繁移动数据的性能开销。该库具备以下特点:

  • 线程安全:通过 FreeRTOS 互斥锁实现多线程访问保护
  • 高效操作:使用 memcpy 实现批量数据复制,优化写入性能
  • 边界处理:自动处理跨缓冲区边界的数据读写
  • 内存优化:使用 SPIRAM 存储(适用于 ESP32 等平台)

2. 库接口函数功能

2.1 初始化函数

int RingBuf_Init(ringbuf_HandleTypeDef* rb);
  • 功能:初始化环形缓冲区及相关资源
  • 参数:rb – 缓冲区句柄指针
  • 返回值:0 表示成功,-1 表示失败
  • 说明:内部会初始化读写指针、创建互斥锁并分配缓冲区内存(大小为 RINGBUF_MAX_SIZE)

2.2 可用空间查询函数

size_t RingBuf_GetAvailable(ringbuf_HandleTypeDef* rb);
  • 功能:计算缓冲区当前可写入的字节数
  • 参数:rb – 缓冲区句柄指针
  • 返回值:可用空间大小(字节数)
  • 说明:为避免读写指针重叠导致的空满状态误判,会预留 1 字节空间

2.3 数据写入函数

size_t RingBuf_Write(ringbuf_HandleTypeDef* rb, const uint8_t* data, size_t len);
  • 功能:将数据写入环形缓冲区
  • 参数:
    • rb – 缓冲区句柄指针
    • data – 待写入数据的指针
    • len – 待写入数据的长度
  • 返回值:实际写入的字节数
  • 说明:
    • 支持跨缓冲区边界的数据写入
    • 会自动处理写入长度与可用空间的匹配(不会写入超过可用空间的数据)
    • 内部使用 memcpy 实现高效数据复制

2.4 单字节读取函数

int RingBuf_ReadByte(ringbuf_HandleTypeDef* rb, uint8_t* data);
  • 功能:从环形缓冲区读取一个字节
  • 参数:
    • rb – 缓冲区句柄指针
    • data – 存储读取数据的指针
  • 返回值:0 表示成功,-1 表示缓冲区为空
  • 说明:每次读取后会自动移动读指针,适用于字节流解析场景

2.5 缓冲区清空函数

void RingBuf_Clear(ringbuf_HandleTypeDef* rb);
  • 功能:清空环形缓冲区
  • 参数:rb – 缓冲区句柄指针
  • 说明:通过重置读写指针实现缓冲区清空,不会实际清除内存数据

3. 代码讲解

3.1 数据结构设计

typedef struct {
    uint8_t *buffer;         // 缓冲区存储区
    size_t write_idx;        // 写入指针
    size_t read_idx;         // 读取指针
    SemaphoreHandle_t mutex; // 互斥锁(保护多线程读写)
} ringbuf_HandleTypeDef;
  • buffer:指向实际存储数据的内存区域
  • write_idx:下一个待写入位置的索引
  • read_idx:下一个待读取位置的索引
  • mutex:FreeRTOS 互斥锁,确保多线程环境下的操作安全性

3.2 缓冲区大小定义

#define RINGBUF_MAX_SIZE 4096  // 适配BMD101高频数据流
  • 定义缓冲区的最大容量为 4096 字节,可根据实际需求调整

3.3 初始化实现细节

int RingBuf_Init(ringbuf_HandleTypeDef* rb) {
    if (rb == NULL) return -1;
    
    rb->write_idx = 0;
    rb->read_idx = 0;
    rb->mutex = xSemaphoreCreateMutex();  // 创建互斥锁
    if (rb->mutex == NULL) return -1;
    // 分配SPIRAM内存(适用于ESP32平台)
    rb->buffer = (uint8_t *)heap_caps_malloc(RINGBUF_MAX_SIZE, MALLOC_CAP_SPIRAM);
    
    memset(rb->buffer, 0, RINGBUF_MAX_SIZE);
    return 0;
}
  • 初始化时会重置读写指针、创建互斥锁并分配内存
  • 使用 heap_caps_malloc 分配 SPIRAM 内存,适合资源受限的嵌入式平台

3.4 可用空间计算逻辑

size_t RingBuf_GetAvailable(ringbuf_HandleTypeDef* rb) {
    if (rb == NULL) return 0;
    
    if (rb->write_idx >= rb->read_idx) {
        return RINGBUF_MAX_SIZE - (rb->write_idx - rb->read_idx) - 1;
    } else {
        return rb->read_idx - rb->write_idx - 1;
    }
}
  • 当写指针在 read 指针之后时:可用空间 = 缓冲区总大小 – 已用空间 – 1(预留)
  • 当写指针在 read 指针之前时:可用空间 = 读指针位置 – 写指针位置 – 1(预留)
  • 预留 1 字节是为了区分 “缓冲区满” 和 “缓冲区空” 两种状态

3.5 数据写入实现

size_t RingBuf_Write(ringbuf_HandleTypeDef* rb, const uint8_t* data, size_t len) {
    // 参数检查与互斥锁获取
    if (rb == NULL || data == NULL || len == 0) return 0;
    if (xSemaphoreTake(rb->mutex, portMAX_DELAY) != pdTRUE) return 0;
    
    // 计算实际可写入长度
    size_t available = RingBuf_GetAvailable(rb);
    size_t write_len = (len > available) ? available : len;
    if (write_len == 0) {
        xSemaphoreGive(rb->mutex);
        return 0;
    }
    
    // 处理跨边界写入
    size_t first_segment_len = RINGBUF_MAX_SIZE - rb->write_idx;
    if (first_segment_len >= write_len) {
        // 无需跨边界
        memcpy(rb->buffer + rb->write_idx, data, write_len);
        rb->write_idx += write_len;
    } else {
        // 跨边界写入:先写到缓冲区末尾,再写到开头
        memcpy(rb->buffer + rb->write_idx, data, first_segment_len);
        memcpy(rb->buffer, data + first_segment_len, write_len - first_segment_len);
        rb->write_idx = write_len - first_segment_len;
    }
    
    xSemaphoreGive(rb->mutex);
    return write_len;
}
  • 写入前会先获取互斥锁,确保线程安全
  • 自动调整实际写入长度,不会超过可用空间
  • 处理两种写入场景:无需跨边界和需要跨边界,保证数据连续性

3.6 线程安全机制

所有涉及读写指针修改的操作都通过互斥锁(mutex)进行保护:

  • 使用 xSemaphoreTake 获取锁
  • 使用 xSemaphoreGive 释放锁
  • 采用 portMAX_DELAY 等待时间,确保最终能获取到锁(避免永久阻塞)

这种机制保证了在多线程环境下,对缓冲区的读写操作不会相互干扰,避免了数据一致性问题。

Avatar photo

作者 skyate

发表回复