循环缓冲区FIFO在嵌入式开发中非常常见,比如串口的收发驱动,协议包的接收等都会用到,这一篇我们就来实现一个自己简单的循环FIFO的”轮子”。
定义FIFO数据结构
/**
* \struct fifo_st
* FIFO缓冲区结构.
*/
typedef struct
{
uint32_t in; /**< 写入索引 */
uint32_t out; /**< 读出索引 */
uint32_t len; /**< 有效数据长度 */
uint32_t buffer_len; /**< 有效长度 */
uint8_t* buffer; /**< 缓存,用户分配 */
} fifo_st;
写入和读出接口
/**
* \fn fifo_in
* 往fifo里写数据
* \param[in] dev \ref fifo_st
* \param[in] buffer 待写入的数据
* \param[in] len 待写入的长度
* \retval 返回实际写入的数据量
*/
uint32_t fifo_in(fifo_st* dev, uint8_t* buffer, uint32_t len);
/**
* \fn fifo_out
* 从fifo读出数据
* \param[in] dev \ref fifo_st
* \param[in] buffer 存读出的数据
* \param[in] len 需要读出的数据长度
* \retval 返回实际读出的数据量
*/
uint32_t fifo_out(fifo_st* dev, uint8_t* buffer, uint32_t len);
实现思路详见注释。
写入
/**
* in为写入索引 0~(buffer_len-1)。
* out为读出索引 0~(buffer_len-1)。
* in == out时可能是满,也可能是空,可以通过len有效数据长度来确认。
* 写数据in增加,直到追赶到out则满。
* 读数据则out增加,直到追赶到in则空。
* in大于out时则[out,in)区间是有效数据。
* in小于out时则[out,buffer_len)和[0,in)区间是有效数据。
***********************************************************
* 0 buffer_len-1 buffer_len
* (1)开始 in和out都是0
* | |
* in(0)
* out(0)
* len = 0
* (2)写入n字节数据 in变为n和out还是0 对应in大于out的情况
* | |
* out(0)————————————>in(n) |
* len = n
* (3)读出m字节数据(m
* | |
* out(m)————>in(n)
* len = n-m
* (4)继续写入数据,绕回到开头,对应in小于out的情况
* | |
* out(m)————————————————————————————————>
* ——>in(k)
* len = k + buffer_len-m
*/
uint32_t fifo_in(fifo_st* dev, uint8_t* buffer, uint32_t len)
{
uint32_t space = 0; /* 用于记录空闲空间大小 */
/* 参数检查 */
if((dev == 0) || (buffer == 0) || (len == 0))
{
return 0;
}
if(dev->buffer == 0)
{
return 0;
}
/* 限制len的最大长度为buffer大小 */
if(len > dev->buffer_len)
{
len = dev->buffer_len;
}
/* 计算空闲空间大小
* 正常dev->len不应该大于dev->buffer_len
*/
if(dev->buffer_len >= dev->len)
{
space = dev->buffer_len - dev->len;
}
else
{
/* 这里不应该出现, 出现则是异常 */
dev->len = 0;
space = dev->buffer_len;
}
/* 计算待写入大小, 如果len大于剩余空间则只写入剩余空间大小 */
len = (len >= space) ? space : len;
if(len == 0)
{
return 0; /* 这里有可能无剩余空间,直接返回 */
}
/* 计算len的长度是否需要有绕回,需要分次写入 */
space = dev->buffer_len - dev->in; /* 当前写入位置in到缓存末尾剩余可写入空间 */
if(space >= len)
{
/* 当前写入位置in到缓存末尾足够一次写入 */
memcpy(dev->buffer+dev->in,buffer,len);
}
else
{
/* 当前写入位置in到缓存末尾不够,还需要绕回到前面写 */
memcpy(dev->buffer+dev->in,buffer,space); /* 先写入tail部分 */
memcpy(dev->buffer,buffer+space,len-space); /* 再写入绕回头部分 */
}
/* 更新写入索引和有效数据长度 */
dev->in += len;
if(dev->in >= dev->buffer_len)
{
dev->in -= dev->buffer_len; /* 判断加减法 替代 dev->in %= dev->buffer->len */
}
dev->len += len; /* dev->len最大dev->buffer->len,无需%= dev->buffer->len */
return len;
}
读出
uint32_t fifo_out(fifo_st* dev, uint8_t* buffer, uint32_t len)
{
uint32_t space = 0;
/* 参数检查 */
if((dev == 0) || (buffer == 0) || (len == 0))
{
return 0;
}
if(dev->buffer == 0)
{
return 0;
}
/* 判断是否有数据 */
if(dev->len == 0)
{
return 0;
}
/* 可读出数据量取需要的和有的之间的小值 */
len = (dev->len) > len ? len : dev->len;
/* 计算len的长度是否需要有绕回,需要分次读出 */
space = dev->buffer_len - dev->out; /* 当前读出位置out到缓存末尾剩余可读出空间 */
if(space >= len)
{
/* 当前读出位置out到缓存末尾足够一次读出 */
memcpy(buffer,dev->buffer+dev->out,len);
}
else
{
/* 当前读出位置out到缓存末尾不够,还需要绕回到前面读 */
memcpy(buffer,dev->buffer+dev->out,space); /* 先读出tail部分 */
memcpy(buffer+space,dev->buffer,len-space); /* 再读出绕回头部分 */
}
/* 更新读出索引和有效数据长度 */
dev->out += len;
if(dev->out >= dev->buffer_len)
{
dev->out -= dev->buffer_len; /* 判断加减法 替代 dev->out %= dev->buffer->len */
}
dev->len -= len; /* 这里dev->len 不可能小于len,不会溢出 */
return len;
}
fifo_test.c
不断往fifo写入随机个数数据,然后随机个数读出,最后看是否写入读出数据个数一样。
代码如下:
uint8_t fifo_buffer[1024];
fifo_st fifo_dev=
{
.in=0,
.out=0,
.len=0,
.buffer_len=sizeof(buffer);
.buffer=fifo_buffer
};
int getrand(int min,int max)
{
return rand()%(max-min) + min;
}
int main(void)
{
uint8_t buffer[1024];
uint32_t size;
uint32_t len;
uint32_t inlen=0;
uint32_t outlen=0;
for(int i=0; i<1000; i++)
{
do
{
size = getrand(1,sizeof(fifo_buffer));
len = fifo_in(&fifo_dev, buffer, size);
printf("fifo in size %d len %d \r\n", size, len);
} while(len > 0);
do
{
size = getrand(1,sizeof(fifo_buffer));
len = fifo_out(&fifo_dev, buffer, size);
printf("fifo out size %d len %d \r\n", size, len);
} while(len > 0);
}
printf("fifo in inlen %d outlen %d \r\n", inlen, outlen);
return 0;
}
编译gcc fifo.c fifo_test.c -o fifo.c
运行./fifo.exe
可以看到写入多次总共1024字节,读出多次总共1024字节,最后总的写入读出数一致。
注意以上仅仅实现FIFO本身,实际应用在多线程,或者前后台(中断和主循环)中访问FIFO,需要做临界段保护。可以根据具体环境封装一层即可。