一、前言
嵌入式开发中我们要时刻保持代码的高效与整洁。在嵌入式的开发中缓冲器是非常常用的,比如串口的数据,MCU处理数据的时候,只能先处理先来的,那么处理完后呢,就会把数据释放掉,再处理下一个。已经处理的数据的内存就会被浪费掉。因为后来的数据只能往后排队,如果要将剩余的数据都往前移动一次,那么效率就会低下了,肯定不现实,所以,环形队列就出现了。
环形缓冲区通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区。通过移动读指针和写指针就可以实现缓冲区的数据读取和写入。在通常情况下,环形缓冲区的读用户仅仅会影响读指针,而写用户仅仅会影响写指针。如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确性。如果有多个读写用户访问环形缓冲区,那么必须添加互斥保护机制来确保多个用户互斥访问环形缓冲区。
二、代码
经典的环形缓冲器源代码可以参考linux内核中的kernel/kfifo.c,巧夺天工的kfifo:Linux Kernel中的无锁环形缓冲,下面代码是移植的nordic ble SDK中的,相对比较简单,使用方便。
#include "cola_fifo.h"
#include "utils.h"
static __inline uint32_t fifo_length(cola_fifo_t * p_fifo)
{
uint32_t tmp = p_fifo->read_pos;
return p_fifo->write_pos - tmp;
}
#define FIFO_LENGTH() fifo_length(p_fifo) /**< Macro for calculating the FIFO length. */
/**@brief Put one byte to the FIFO. */
static __inline void fifo_put(cola_fifo_t * p_fifo, uint8_t byte)
{
p_fifo->p_buf[p_fifo->write_pos & p_fifo->buf_size_mask] = byte;
p_fifo->write_pos++;
}
/**@brief Look at one byte in the FIFO. */
static __inline void fifo_peek(cola_fifo_t * p_fifo, uint16_t index, uint8_t * p_byte)
{
*p_byte = p_fifo->p_buf[(p_fifo->read_pos + index) & p_fifo->buf_size_mask];
}
/**@brief Get one byte from the FIFO. */
static __inline void fifo_get(cola_fifo_t * p_fifo, uint8_t * p_byte)
{
fifo_peek(p_fifo, 0, p_byte);
p_fifo->read_pos++;
}
uint32_t cola_fifo_init(cola_fifo_t * p_fifo, uint8_t * p_buf, uint16_t buf_size)
{
// Check buffer for null pointer.
if (p_buf == NULL)
{
return 0;
}
// Check that the buffer size is a power of two.
if (!IS_POWER_OF_TWO(buf_size))
{
return 0;
}
p_fifo->p_buf = p_buf;
p_fifo->buf_size_mask = buf_size - 1;
p_fifo->read_pos = 0;
p_fifo->write_pos = 0;
return 1;
}
uint32_t cola_fifo_put(cola_fifo_t * p_fifo, uint8_t byte)
{
if (FIFO_LENGTH() <= p_fifo->buf_size_mask)
{
fifo_put(p_fifo, byte);
return 1;
}
return 0;
}
uint32_t cola_fifo_get(cola_fifo_t * p_fifo, uint8_t * p_byte)
{
if (FIFO_LENGTH() != 0)
{
fifo_get(p_fifo, p_byte);
return 1;
}
return 0;
}
uint32_t cola_fifo_peek(cola_fifo_t * p_fifo, uint16_t index, uint8_t * p_byte)
{
if (FIFO_LENGTH() > index)
{
fifo_peek(p_fifo, index, p_byte);
return 1;
}
return 0;
}
uint32_t cola_fifo_flush(cola_fifo_t * p_fifo)
{
p_fifo->read_pos = p_fifo->write_pos;
return 1;
}
uint32_t cola_fifo_read(cola_fifo_t * p_fifo, uint8_t * p_byte_array, uint32_t p_size)
{
//VERIFY_PARAM_NOT_NULL(p_fifo);
//VERIFY_PARAM_NOT_NULL(p_size);
if(p_fifo == NULL)
{
return 0;
}
if(p_size == 0)
{
return 0;
}
const uint32_t byte_count = fifo_length(p_fifo);
const uint32_t requested_len = (p_size);
uint32_t index = 0;
uint32_t read_size = MIN(requested_len, byte_count);
(p_size) = byte_count;
// Check if the FIFO is empty.
if (byte_count == 0)
{
return 0;
}
// Check if application has requested only the size.
if (p_byte_array == NULL)
{
return 0;
}
// Fetch bytes from the FIFO.
while (index < read_size)
{
fifo_get(p_fifo, &p_byte_array[index++]);
}
(p_size) = read_size;
return p_size;
}
uint32_t cola_fifo_write(cola_fifo_t * p_fifo, uint8_t const * p_byte_array, uint32_t p_size)
{
//VERIFY_PARAM_NOT_NULL(p_fifo);
//VERIFY_PARAM_NOT_NULL(p_size);
if(p_fifo == NULL)
{
return 0;
}
if(p_size == 0)
{
return 0;
}
const uint32_t available_count = p_fifo->buf_size_mask - fifo_length(p_fifo) + 1;
const uint32_t requested_len = (p_size);
uint32_t index = 0;
uint32_t write_size = MIN(requested_len, available_count);
(p_size) = available_count;
// Check if the FIFO is FULL.
if (available_count == 0)
{
return 0;
}
// Check if application has requested only the size.
if (p_byte_array == NULL)
{
return 0;
}
//Fetch bytes from the FIFO.
while (index < write_size)
{
fifo_put(p_fifo, p_byte_array[index++]);
}
(p_size) = write_size;
return p_size;
}
三、引申
环形串口缓冲器的缺点是同一块buffer中只能有一个缓冲器工作,如果当前申请的缓冲区没有在使用那么也会占用内存,因此在这将缓冲器的设计思想做一下引申,一个可以同时多个缓冲器共用同一个buffer的例子。
实现原理:
1.申请一块固定长度的数组用于缓冲区。如:uint8_t buffer[2048],申请2K的总缓冲区
2.将总的缓冲区分成2的整数幂字节大小的块。如每块128字节,可分成16块。
3.使用位图方式管理每块,uint32_t bitmap,32位整形最大可管理32块内存块。
4.0-127字节标号块0,128-255字节标号块2.......,以此类推标号。
5.每次可申请一块的大小,每次申请空闲且标号最小的块,这样可保证每次都是用低位空间。
6.每块最高位字节记录下一块标号,这样可将多个块连城一个大块
7.实现多通道,每个通道包括,最大字节长度,当前字节说,当前写入的块号,当前写入处于某个块的位置,当前读出块号,当前读出块位置。
缺点:多个缓冲器公用同一个buffer,在读写数据时需要加锁。