同步FIFO详解
在学习科研的闲暇之余,打算记录一些FPGA基础知识,方便后续自己的查阅复习,如果能帮助到其他同学那就更好了。本文主要介绍同步FIFO的实现原理,内容如有不妥之处,欢迎评论区指正,共同学习进步。
1、FIFO概述
FIFO(First In First Out)是一种先进先出的数据存储、缓存器。我们知道一般的存储器是用外部的读写地址来进行读写,而FIFO这种存储器的结构并不需要外部的读写地址而是通过内部的读写指针自动的加一操作来控制读写,这也就决定了FIFO只能顺序的读写数据。FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。
FIFO 有两种类型:同步 FIFO 和异步 FIFO。同步 FIFO 使用同一个时钟信号来控制数据的读写,而异步 FIFO 有两个时钟信号,一个用于读操作,另一个用于写操作。
同步 FIFO 的作用一般是做一个数据缓冲,也就是一个 buffer。异步 FIFO 有两个较为重要的作用:一个是实现数据在不同时钟域进行传递,另一个作用就是实现不同数据宽度的数据接口。
2、同步FIFO的实现
FIFO 的设计原则是任何时候都不能向满FIFO中写入数据(写溢出),任何时候都不能从空FIFO中读取数据(读溢出)。FIFO 设计的核心是空满判断。FIFO 设置读,写地址指针,FIFO 初始化的时候 读指针和写指针都指向地址为 0 的位置,当往 FIFO 里面每写一个数据,写地址指针自动加 1 指向下一个要写入的地址。当从 FIFO 里面每读一个数据,读地址指针自动加 1 指向下一个要读出的地址,最后通过比较读地址指针和写地址指针的大小来确定空满状态。
写指针:总是指向下一个将要被写入的单元,复位时,指向第 1 个单元(编号为 0)。
读指针:总是指向当前要被读出的数据,复位时,指向第 1 个单元(编号为 0)。
产生可靠的 FIFO 读写指针和生成 FIFO “空”/“满”状态标志是 FIFO 设计的关键。
当读写指针相等时,表明 FIFO 为空,这种情况发生在复位操作时,或者当读指针读出 FIFO 中最后一个数据后,追赶上了写指针时,如下图所示[1]:

当读写指针再次相等时,表明 FIFO 为满,这种情况发生在写指针转了一圈,折回来又追上了读指针,如下图所示[1]:

参考刀哥的思路,我们将有两种思路进行同步fifo的设计。[2]
2.1、计数器法实现同步FIFO
构建一个计数器,该计数器(fifo_cnt)用于指示当前 FIFO 中数据的个数:
复位时,该计数器为 0,FIFO 中的数据个数为 0。
当读写使能信号均有效时,说明又读又写,计数器不变,FIFO 中的数据个数无变化。
当写使能有效且 full = 0,则 fifo_cnt + 1;表示写操作且 FIFO 未满时候,FIFO 中的数据增加了 1 。
当读使能有效且 empty = 0,则 fifo_cnt - 1;表示读操作且 FIFO 未空时候,FIFO 中的数据减少了 1 。
fifo_cnt = 0 的时候,表示 FIFO 空,需要设置 empty = 1;fifo_cnt = fifo的深度 的时候,表示 FIFO 现在已经满,需要设置 full = 1。
这种方法设计比较简单,但是需要的额外的计数器,就会产生额外的资源,而且当 FIFO 比较大时,会降低 FIFO 最终可以达到的速度。
//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
rd_addr <= 0;
else if (!empty && rd_en)begin
rd_addr <= rd_addr + 1'd1;
data_out <= fifo_buffer[rd_addr];
end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wr_addr <= 0;
else if (!full && wr_en)begin
wr_addr <= wr_addr + 1'd1;
fifo_buffer[wr_addr]<=data_in;
end
end
//更新计数器
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
fifo_cnt <= 0;
else begin
case({wr_en,rd_en})
2'b00:fifo_cnt <= fifo_cnt; //不读不写
2'b01: //仅仅读
if(fifo_cnt != 0)
fifo_cnt <= fifo_cnt - 1'b1;
2'b10: //仅仅写
if(fifo_cnt != DATA_DEPTH)
fifo_cnt <= fifo_cnt + 1'b1;
2'b11:fifo_cnt <= fifo_cnt; //读写同时
default:;
endcase
end
end
//依据计数器状态更新指示信号
assign full = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0; //满信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0; //空信号
仿真依次进行以下测试:基本单次读写测试、FIFO写满测试、FIFO读空测试、同时读写操作测试、边界条件测试

2.2、高位扩展法实现同步FIFO
高位扩展法是一种更加高效的FIFO实现方式。它通过在读写指针上增加一个额外的高位作为指示位,避免使用独立的计数器。其原理如下:
读写指针的宽度要比FIFO深度所需的地址位宽多1位
指针的低位用于寻址,高位用于判断FIFO的空满状态
当写指针与读指针相比,最高位不同而其他位相同时,说明写入数据已经超过一轮,此时FIFO已满。
当写指针与读指针的最高位和其他位都相同时,说明读指针已经追上写指针位置,此时FIFO为空。
高位扩展法要求FIFO深度为2的N次幂,以确保地址自然溢出和空满条件正确性。若深度非2的N次幂,需引入复杂的手动控制逻辑,失去高位扩展法的简洁性和高效性。
高位扩展法取消了计数器,在原来的基础上更改了读写指针的更新逻辑,通过比较读写指针的高位和低位来判断FIFO的空满状态。
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
//当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO被写满
assign full = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;
同样进行仿真测试:基本单次读写测试、FIFO写满测试、FIFO读空测试、同时读写操作测试、边界条件测试

3、代码下载
博客中涉及到的代码均在我的GitHub仓库中,欢迎大家下载学习。