环形缓冲区(Ring Buffer)是一种高效的使用内存的方法,它将一段固定长度的内存看成一个环形结构,用于存储数据,能够避免使用动态申请内存导致的内存碎片问题,而且其能够更高效的使用内存。
在单片机中,由于内存有限,而且需要尽可能避免使用动态内存,所以环形缓冲区在单片机中应用非常广泛。
通常我们需要使用一个数组或者在程序开头申请一段不被释放的内存,将其作为缓冲区。然后使用两个指令分别指向读写位置。
使用读指针管理读位置,使用写指针管理写位置。当读指针追上写指针时,表示缓冲区为空,当写指针追上读指针时,表示缓冲区已满。
环形缓冲区的读写操作都是循环进行的,当读指针或写指针到达缓冲区的末尾时,会自动回到缓冲区的开头。
环形缓冲区的读写操作都是原子操作,即一次只能进行一个读写操作,避免了多线程或多任务同时读写缓冲区导致的数据混乱。
缓冲区满了以后可以选择覆盖写或者阻塞等待,需要注意的是,如果选择覆盖写,那么读索引也应该向前移动,此时最前面的数据就会丢失;如果选择阻塞等待的话,尽可能不要在中断中使用,否则中断嵌套会有无法预料的执行流程。
注:我们可以使用数组索引代替读写指针位置,毕竟对整数的加减还是比较容易理解的。下面我将读指针用读索引代替,写指针用写索引代替。
我们需要支持多个环形缓冲区,使用同一套代码逻辑,那么就不能将缓冲区数组的大小进行硬编码,而是需要在初始化环形缓冲区的时候通过参数传递进来,我们使用一个结构体来表示缓冲区数组。
我们的缓冲区不能只是支持字节数组,而是支持任意类型的数据,所以我们需要一个变量来保存缓冲区数组中每个元素的大小,这样我们就可以根据这个大小然后结合读写索引来获取和写入数据。
typedef struct{ void *ptr; // 缓冲区数组指针 uint32_t elem_num; // 缓冲区数组元素个数 uint32_t elem_size; // 缓冲区数组中每个元素的大小} Array;
我们通过一个环形缓冲区的结构体来管理缓冲区,读写索引,同时使用一个变量来记录当前缓冲区中有效的元素个数,以便判断环形缓冲区是否为空或者是否已满。
如果在RTOS中使用环形缓冲区,那么读写索引需要使用原子操作,防止多线程或多任务同时读写缓冲区导致的数据混乱。为保证可以实现原子操作,需要传入RTOS提供的原子操作方法(进入临界区、互斥量等)
typedef struct{ Array *buffer; // 缓冲区 uint32_t read_index; // 读索引 uint32_t write_index; // 写索引 uint32_t count; // 环形缓冲区中元素个数 void (*lock)(void); // 进入原子操作 void (*unlock)(void); // 退出原子操作} RingBuffer;
为了及时了解我们的函数执行结果,我们在函数执行结束后需要返回一个错误码用于判断执行情况。
typedef enum{ ARRAY_OK, // 成功 ARRAY_PARAMS_NULL, // 参数为空 ARRAY_INDEX_OUT_OF_RANGE, // 索引越界} ArrayError;typedef enum{ RB_OK, // 操作成功 RB_READ_NOT_ENOUGH, // 缓冲区中元素个数不足,这只是一个警告,程序可以进行进行 RB_PARAM_ERROR, // 参数错误 RB_FULL, // 缓冲区已满 RB_EMPTY, // 缓冲区为空} RingBufferStatus;
我们的缓冲区其实就是一个数组,但是我们这里为了支持存储不同类型的元素,使用了Array来对这个数组进行管理(类似C++中的Vector)。
在读写某个元素的时候,需要判断输入的索引是否在范围内。
// 初始化数组// pthis: 数组结构体指针// ptr: 数组指针// size: 数组元素个数// elem_size: 数组中每个元素的大小ArrayError array_init(Array *pthis, void *ptr, const uint32_t elem_num, const uint32_t elem_size){ if (pthis == NULL || ptr == NULL) { return ARRAY_PARAMS_NULL; } pthis->ptr = ptr; pthis->elem_num = elem_num; pthis->elem_size = elem_size; return ARRAY_OK;}// 获取数组元素个数// pthis: 数组结构体指针uint32_t array_get_elem_num(Array *pthis){ return pthis->elem_num;}// 获取数组中每个元素的大小// pthis: 数组结构体指针uint32_t array_get_elem_size(Array *pthis){ return pthis->elem_size;}// 向数组写入一个元素// pthis: 数组结构体指针// index: 要写入的元素索引// elem: 要写入的元素ArrayError array_write_elem(Array *pthis, const uint32_t index, const void *elem){ if (pthis == NULL || elem == NULL) { return ARRAY_PARAMS_NULL; } if (index >= pthis->elem_num) { return ARRAY_INDEX_OUT_OF_RANGE; } memcpy((char *)pthis->ptr + index * pthis->elem_size, elem, pthis->elem_size); return ARRAY_OK;}// 从数组读取一个元素// pthis: 数组结构体指针// index: 要读取的元素索引// elem: 读取的元素ArrayError array_read_elem(Array *pthis, const uint32_t index, void *elem){ if (pthis == NULL || elem == NULL) { return ARRAY_PARAMS_NULL; } if (index >= pthis->elem_num) { return ARRAY_INDEX_OUT_OF_RANGE; } memcpy(elem, (char *)pthis->ptr + index * pthis->elem_size, pthis->elem_size); return ARRAY_OK;}
为了方便判断环形缓冲区是否满或者空了,我使用一个变量来记录有效的元素数量,当有效元素数量为0时表示环形缓冲区空了,当有效元素数量为缓存数组的长度时表示环形缓冲区满了。
读写多个元素时,在内部都是一个一个进行的读写,只有在读写某个元素前才后判断环形缓冲区是否满或者空。那么读写多个元素的操作就不一定都能成功,在这里如果全部读写成功
或者只是因为环形缓冲区满、空导致失败,只需要返回成功读写的数据,如果是其他原因导致的读写失败,那么就需要根据RingBufferStatus的枚举类型返回相应的负数。
#define RB_LOCK() \ if (rb->lock) \ rb->lock()#define RB_UNLOCK() \ if (rb->unlock) \ rb->unlock()// 初始化环形缓冲区// @rb: 环形缓冲区// @array: 环形缓冲区使用的数组// @lock: 锁函数,进入原子操作时调用// @unlock: 解锁函数,退出原子操作时调用// 返回值: RB_OK, RB_PARAM_ERRORRingBufferStatus ring_buffer_init(RingBuffer *rb, Array *array, void (*lock)(void), void (*unlock)(void)){ if (rb == NULL || array == NULL) { return RB_PARAM_ERROR; } rb->buffer = array; rb->lock = lock; rb->unlock = unlock; rb->write_index = 0; rb->read_index = 0; rb->count = 0; return RB_OK;}// 向环形缓冲区写入一个元素// @rb: 环形缓冲区// @data: 要写入的数据// 返回值: RB_OK, RB_FULL, RB_PARAM_ERRORstatic RingBufferStatus ring_buffer_write_one(RingBuffer *rb, const void *data){ ArrayError err = ARRAY_OK; if (rb == NULL || data == NULL) { return RB_PARAM_ERROR; } if (rb->count == array_get_elem_num(rb->buffer)) { return RB_FULL; } err = array_write_elem(rb->buffer, rb->write_index, data); if (err != ARRAY_OK) { return RB_PARAM_ERROR; } rb->write_index = (rb->write_index + 1) % array_get_elem_num(rb->buffer); rb->count++; return RB_OK;}// 从环形缓冲区读取一个元素// @rb: 环形缓冲区// @data: 读取的数据// 返回值: RB_OK, RB_EMPTY, RB_PARAM_ERRORstatic RingBufferStatus ring_buffer_read_one(RingBuffer *rb, void *data){ ArrayError err = ARRAY_OK; if (rb == NULL || data == NULL) { return RB_PARAM_ERROR; } if (rb->count == 0) { return RB_EMPTY; } err = array_read_elem(rb->buffer, rb->read_index, data); if (err != ARRAY_OK) { return RB_PARAM_ERROR; } rb->read_index = (rb->read_index + 1) % array_get_elem_num(rb->buffer); rb->count--; return RB_OK;}// 向环形缓冲区写入数据// @rb: 环形缓冲区// @data: 要写入的数据// @elem_num: 要写入的元素个数// 返回值: 当至少能写入一个元素时,返回实际写入的元素个数;其他时候返回 -RB_PARAM_ERROR, -RB_FULLint32_t ring_buffer_write(RingBuffer *rb, const void *data, const uint32_t elem_num){ RingBufferStatus ret = RB_OK; int32_t write_num = 0; if (rb == NULL || data == NULL || elem_num == 0) { return -RB_PARAM_ERROR; } RB_LOCK(); for (int32_t i = 0; i < elem_num; i++) { ret = ring_buffer_write_one(rb, (char *)data + i * array_get_elem_size(rb->buffer)); write_num++; if (ret != RB_OK) { break; } } RB_UNLOCK(); if (ret == RB_OK || ret == RB_FULL) { return write_num; // 返回实际写入的元素个数 } else { return -ret; // 返回负数表示写入失败 }}// 从环形缓冲区读取数据// @rb: 环形缓冲区// @data: 读取的数据// @elem_num: 要读取的元素个数// 返回值: 当至少能读取一个元素时,返回实际读取的元素个数;其他时候返回 -RB_PARAM_ERROR, -RB_EMPTYint32_t ring_buffer_read(RingBuffer *rb, void *data, const uint32_t elem_num){ RingBufferStatus ret = RB_OK; int32_t read_num = 0; if (rb == NULL || data == NULL || elem_num == 0) { return -RB_PARAM_ERROR; } RB_LOCK(); for (int32_t i = 0; i < elem_num; i++) { ret = ring_buffer_read_one(rb, (char *)data + i * array_get_elem_size(rb->buffer)); read_num++; if (ret != RB_OK) { break; } } RB_UNLOCK(); if (ret == RB_OK || ret == RB_READ_NOT_ENOUGH) { return read_num; // 返回实际读取的元素个数 } else { return -ret; // 返回负数表示读取失败 }}
本章主要介绍了一种在单片机中常用的环形缓冲区,分析了设计思路和代码实现。
全部0条评论
快来发表一下你的评论吧 !