FIFOFirst In First Out是一种先进先出的数据缓存器,与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据, 其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
FIFO一般用于不同时钟域之间的数据传输,或不同宽度的数据接口相连。F根据FIFO读写时钟域是否相同,可以将FIFO分为同步FIFO和异步FIFO。
FIFO的常见参数:
- 宽度:即FIFO一次读写操作的数据位;
- 深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
- 满标志,空标志,读时钟,写时钟
- 读指针:总是指向下一个将要被读取的单元,复位时,指向第1个单元(编号为0)
- 写指针:总是指向当前要被写入的数据,复位时,指向第1个单元(编号为0)
空/满检测: 当读写指针相等时~
- FIFO 空:如果是复位或读操作引起(当读指针读出FIFO中最后一个字后,追赶上了写指针时)
- FIFO 满:如果是写操作引起
- 还可以用多加一位extra bit来记录折回,如果extra也一样代表空,不一样代表写满
下面具体介绍:
FIFO设计的关键:产生可靠的FIFO读写指针和生成FIFO“空”/“满”状态标志。
当读写指针相等时,表明FIFO为空,这种情况发生在复位操作时,或者当读指针读出FIFO中最后一个字后,追赶上了写指针时,如下图所示:

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

为了区分到底是满状态还是空状态,可以采用以下方法:
方法:在指针中添加一个额外的位(extra bit),当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1,其它位回零。对读指针也进行同样的操作。此时,对于深度为2n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。
- 如果两个指针的MSB不同,说明写指针比读指针多折回了一次;如r_addr=0000,而w_addr = 1000,为满。
- 如果两个指针的MSB相同,则说明两个指针折回的次数相等。其余位相等,说明FIFO为空;
写操作无条件清除空标志;读操作无条件清除满标志
在异步FIFO不同时间域,将指针同步到其它时钟域时,用两级FF传递格雷码地址防止亚稳态,gray码可以保证即使亚稳态数据不准,也能正确判断空满,需要一个always块专门进行bin2gray的转换。gray码每次只变一个寄存器一位,这样仅仅1bit可能产生亚稳态。采错最多错到上一位去,不会有影响
6个模块实现:顶层;双口RAM;2个跨域同步指针模块;空判断逻辑;满判断逻辑;

- 根据异步FIFO的设计架构,归纳以下设计步骤:
写时钟域:
(1)根据写使能wr_en和写满标志位wr_full产生二进制写指针
(2)根据二进制写指针产生双端口RAM的写地址
(3)由二进制写指针转换成格雷码写指针
(4)对格雷码读指针在写时钟域中进行两级同步得同步后格雷码读指针,通过比较格雷码来判断是否为满
(5)同步后格雷码读指针转化成同步后二进制读指针
(6)步骤(3)与步骤(4)比较得写满标志位wr_full
(7)步骤(1)与步骤(5)相减得指示写FIFO的数据量
读时钟域:
(8)根据读使能rd_en和读空标志位rd_empty产生二进制读指针
(9)根据二进制读指针产生双端口RAM的读地址
(10)由二进制读指针转换成格雷码读指针
(11)对格雷码写指针在读时钟域中进行两级同步得同步后格雷码写指针,通过比较格雷码来判断是否为空
(12)同步后格雷码写指针转化成同步后二进制写指针
(13)步骤(10)与步骤(11)比较得读空标志位rd_empty
(14)步骤(8)与步骤(12)相减得指示读FIFO的数据量
关键代码:
reg[DATA_SIZE-1:0] mem_name[RAM_Depth-1:0]; //RAM深度=2^ADDR_WIDTH
assign rdata = Mem[raddr];
assign rempty = (rgraynext == rq2_wptr); //指针是否相等
assign wfull = (wgraynext == {~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
下面来看一个例子:
DualRAM模块
module DualRAM
#(
parameter DATA_SIZE = 8, // 数据位宽
parameter ADDR_SIZE = 4 // 地址位宽
)
(
input wclken,wclk,
input [ADDR_SIZE-1:0] raddr, //RAM read address
input [ADDR_SIZE-1:0] waddr, //RAM write address
input [DATA_SIZE-1:0] wdata, //data input
output reg [DATA_SIZE-1:0] rdata //data output
);
localparam RAM_DEPTH = 1 << ADDR_SIZE; //RAM深度 = 2^ADDR_WIDTH,所以为1左移ADDR_SIZE 位
reg [DATA_SIZE-1:0] Mem[RAM_DEPTH-1:0];
always@(posedge wclk)
begin
if(wclken)
begin Mem[waddr] <= wdata; end
rdata <= Mem[raddr];
end
endmodule
同步模块1,将读指针通过两级触发器同步到写时域
module sync_r2w
#(parameter ADDRSIZE = 4)
(
output reg [ADDRSIZE:0] wq2_rptr,
input [ADDRSIZE:0] rptr,//输入的读地址为格雷码
input wclk, wrst_n
);
reg [ADDRSIZE:0] wq1_rptr;
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
{wq2_rptr,wq1_rptr} <= 0;
else
{wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
endmodule
同步模块2,将写指针通过两级触发器同步到读时域
module sync_w2r
#(parameter ADDRSIZE = 4)
(
output reg [ADDRSIZE:0] rq2_wptr,
input [ADDRSIZE:0] wptr,//输入的写地址为格雷码
input rclk, rrst_n
); reg [ADDRSIZE:0] rq1_wptr;
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)
{rq2_wptr,rq1_wptr} <= 0;
else
{rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
endmodule
在读时钟域的空判断逻辑
module rptr_empty
#(parameter ADDRSIZE = 4)
(
output reg rempty,
output [ADDRSIZE-1:0] raddr,
output reg [ADDRSIZE :0] rptr,
input [ADDRSIZE :0] rq2_wptr,//输入的写地址为格雷码
input rinc, rclk, rrst_n);
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext, rbinnext;
wire rempty_val;
//-------------------
// GRAYSTYLE2 pointer: gray码读地址指针
//-------------------
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)
begin
rbin <= 0;
rptr <= 0;
end
else
begin
rbin <= rbinnext ;
rptr <= rgraynext;//下一个读地址的格雷码
end
// gray码计数逻辑
assign rbinnext = !rempty ? (rbin + rinc) : rbin;//若不为空,则读地址加1作为下一个读地址
assign rgraynext = (rbinnext>>1) ^ rbinnext; //二进制到gray码的转换,将右移一位的结果与原来的数异或
assign raddr = rbin[ADDRSIZE-1:0];//下一个读地址作为输出
//---------------------------------------------------------------
// FIFO empty when the next rptr == synchronized wptr or on reset
//---------------------------------------------------------------
/*
* 读指针是一个n位的gray码计数器,比FIFO寻址所需的位宽大一位
* 当读指针和同步过来的写指针完全相等时(包括MSB),说明二者折回次数一致,FIFO为空
*
*/
assign rempty_val = (rgraynext == rq2_wptr);//比较读写指针的格雷码
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)
rempty <= 1'b1;//若复位,则空信号置1
else
rempty <= rempty_val;
endmodule
在写时钟域的满判断逻辑
module wptr_full
#(
parameter ADDRSIZE = 4
)
(
output reg wfull,
output [ADDRSIZE-1:0] waddr,
output reg [ADDRSIZE :0] wptr,
input [ADDRSIZE :0] wq2_rptr,
input winc, wclk, wrst_n);
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext, wbinnext;
wire wfull_val;
// GRAYSTYLE2 pointer
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
begin
wbin <= 0;
wptr <= 0;
end
else
begin
wbin <= wbinnext;
wptr <= wgraynext;
end
//gray 码计数逻辑
assign wbinnext = !wfull ? wbin + winc : wbin;
assign wgraynext = (wbinnext>>1) ^ wbinnext;
assign waddr = wbin[ADDRSIZE-1:0];
/*由于满标志在写时钟域产生,因此比较安全的做法是将读指针同步到写时钟域*/
/**/
//------------------------------------------------------------------
// Simplified version of the three necessary full-tests:
// assign wfull_val=((wgnext[ADDRSIZE] !=wq2_rptr[ADDRSIZE] ) &&
// (wgnext[ADDRSIZE-1] !=wq2_rptr[ADDRSIZE-1]) &&
// (wgnext[ADDRSIZE-2:0]==wq2_rptr[ADDRSIZE-2:0]));
//------------------------------------------------------------------
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],
wq2_rptr[ADDRSIZE-2:0]});
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
wfull <= 1'b0;//复位状态,满置0
else
wfull <= wfull_val;
endmodule
顶层模块
module AsyncFIFO
#(parameter ASIZE = 4, //地址位宽
parameter DSIZE = 8) //数据位宽
(
input [DSIZE-1:0] wdata,
input winc, wclk, wrst_n, //写请求信号,写时钟,写复位
input rinc, rclk, rrst_n, //读请求信号,读时钟,读复位
output [DSIZE-1:0] rdata,
output wfull,
output rempty
);
wire [ASIZE-1:0] waddr, raddr;
wire [ASIZE:0] wptr, rptr, wq2_rptr, rq2_wptr; /************************************************************
* In order to perform FIFO full and FIFO empty tests using
* this FIFO style, the read and write pointers must be
* passed to the opposite clock domain for pointer comparison
*************************************************************/
/*在检测“满”或“空”状态之前,需要将指针同步到其它时钟域时,使用格雷码,可以降低同步过程中亚稳态出现的概率*/
sync_r2w I1_sync_r2w(
.wq2_rptr(wq2_rptr),
.rptr(rptr),
.wclk(wclk),
.wrst_n(wrst_n));
sync_w2r I2_sync_w2r (
.rq2_wptr(rq2_wptr),
.wptr(wptr),
.rclk(rclk),
.rrst_n(rrst_n));
/*
* DualRAM
*/
DualRAM #(DSIZE, ASIZE) I3_DualRAM(
.rdata(rdata),
.wdata(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(winc),
.wclk(wclk));
/*
* 空、满比较逻辑
*/
rptr_empty #(ASIZE) I4_rptr_empty(
.rempty(rempty),
.raddr(raddr),
.rptr(rptr),
.rq2_wptr(rq2_wptr),
.rinc(rinc),
.rclk(rclk),
.rrst_n(rrst_n));
wptr_full #(ASIZE) I5_wptr_full(
.wfull(wfull),
.waddr(waddr),
.wptr(wptr),
.wq2_rptr(wq2_rptr),
.winc(winc),
.wclk(wclk),
.wrst_n(wrst_n));
endmodule
为安全起见推荐使用IP核定制FIFO,本文的目的只是作为思路参考!!
使用单端口SRAM制作双端口FIFO
为保证FIFO能同时读写,中间的SRAM必须为双端口SRAM。而双端口SRAM的面积比单端口读写的SRAM面积大得多。如果能改用单端口SRAM,则可有效减少FIFO的面积/功耗消耗。
如果想使用单端口SRAM,为保证读写同时进行,需要使用双倍数据位宽的SRAM。例如,一个1024*8bit的SRAM,要改为512*16bit大小。在同时连续读写时,需要两倍的存储带宽才能保证写入和读出不停顿。而写入/读出的数据也变成了两倍位宽。这会产生一些问题,比如:读写的数据是奇数个、写入1个数据之后立即读出时,尚未凑齐足够的数据写入SRAM,但多出的这个数据又要被读出。所以,在SRAM的前后,需要添加一些辅助的寄存器。FIFO的结构改为下图所示的样子:

核心的SRAM被包装成两倍位宽的单端口读写FIFO。为优化时序,此FIFO被设为读优先。读写部分另有两倍位宽的写Reg和单倍位宽的读Reg。每次写入时,先写入写Reg的低位。第二次写入数据时,如果此时单端口FIFO不在读数据,则将输入数据与写reg存储的低位数据一起写进单端口FIFO中。如果此时在读数据,则输入数据寄存在写FIFO的高位,等下一周期写入。
读出数据时,如果单端口FIFO不空,则读出两个数据,高位数据暂存在读reg中,低位数据输出。下一次读取时,优先读取读reg中的内容。如果单端口FIFO已空,但写reg中有一个数据,则从写reg中直接读出,并设置"已提前读出"的标志位。
第二次写入时,如果看到"已提前读出"的标志位置1,则新数据直接送给读Reg寄存,并且不再写入单端FIFO中。这种情况下,下一个被读取的数据一定是当前正在写入的数据,并且这是个存在高位的数据。那就没有必要再进行单端口FIFO的读写,可以直接将此数据bypass至读取reg。
对于Full/Empty标志位,如果单端读写FIFO已满,则输出Full。虽然理论上此时写Reg中还可继续存储一个数据,但为简化操作,就不占用这个位置了。Empty则要复杂一些。如果单端FIFO已空,则需要看此时的读写Reg是否为空。如果读写Reg中还有数据,先读取读Reg中的。再读取写Reg中的。如果写Reg为空或者已读,此时整个FIFO才是真正的Empty状态。
总之,通过增加读写reg以及几个标志位,就可以使用单端口SRAM完成双端口FIFO的操作。这可以大幅缩小FIFO占用的面积。但同时需要注意,标志位的操作逻辑很容易出错,必须经过大量验证,确保功能正确,才能正式使用。
1878

被折叠的 条评论
为什么被折叠?



