深入了解FIFO的工作原理及设计方法

1.简介

        FIFO( First Input First Output)简单说就是指先进先出。FIFO存储器是一个先入先出的双口缓冲器,即第一个进入其内的数据第一个被移出,其中一个口是存储器的输入口,另一个口是存储器的输出口。

        对于单片FIFO来说,主要有两种结构:触发导向结构和零导向传输结构。触发导向传输结构的FIFO是由寄存器阵列构成的,零导向传输结构的FIFO是由具有读和写地址指针的双口RAM构成。

        FIFO与普通RAM存储器的区别是没有外部读写地址线(指针),使用方便,但缺点是只能顺序写入数据和读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。

1.1.功能

        FIFO存储器是系统的缓冲环节,主要有几方面的功能:

                1)对连续的数据流进行缓存,防止在进机和存储操作时丢失数据;

                2)数据集中起来进行进栈和存储,可避免频繁的总线操作,减轻CPU的负担;

                3)允许系统进行DMA操作,提高数据的传输速度。这是至关重要的一点,如果不采用DMA操作,数据传输将达不到传输要求,而且大大增加CPU的负担,无法同时完成数据的存储工作。

1.2.用途

1.2.1.跨时钟域多bit数据传输

        解决一个系统多个时钟所带来的问题:异步时钟之间的接口电路。异步FIFO是解决这个问题的一种便捷简单的方案,使用异步FIFO可以在两个不同时钟系统之间快速方便地传输实时数据。

1.2.2.达到数据匹配问题(读写位宽不一致)

        对于不同宽度的数据接口也可以使用FIFO,例如单片机的8位输出而DSP可能是16位输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。

1.3.主要参数

  • 宽度(WIDTH):FIFO每个地址的数据位宽(W);
  • 深度(DEEPTH):FIFO可以存储多少个W位的数据;
  • 满(full)标志:FIFO已满或将满时,会输出一个对写操作的反压信号,以阻止被继续写入数据而溢出;
  • 空(empty)标志:FIFO已空或将空时,会输出一个对读操作的反压信号,以避免被继续读出无效数据;
  • 读/写时钟:读/写操作所遵循的时钟,每个时钟沿触发。
  •         根据FIFO工作的时钟域分为同步/异步FIFO。同步FIFO是指读时钟和写时钟为同一个时钟在时钟沿来临时同时发生读写。异步FIFO读写时钟不一致,读写相互独立。

            读写指针即读写地址,当前读/写操作完成后,指针自动加一指向下一个地址(连续递增)。

  • 写指针:总是指向下一个将要被写入的地址,复位时指向编号0的地址;
  • 读指针:总是指向下一个将要被读出的数据地址,复位时也指向编号0的地址且此时数据无效;
  • 2.工作原理

    2.1.空满标志

    2.1.1.读空信号(rd_empty)

            一般情况下当读写指针相等时,表明FIFO已空,这种情况发生在复位操作时或当读指针读出FIFO中最后一个有效数据时(即读指针追赶上写指针),此时读空信号有效,如下左图:

    2.1.2.写满信号(wr_full)

            当读写指针再次相等时,即写指针转了一圈又折回来(wrapped around)从起始低位追上了读指针(写比读快),此时表明FIFO已满,如上右图:

    2.2.空满判断机制

    2.2.1.同步fifo空满判断

  • 方案一:extra bit
  •                 深度为N=2^{n}的FIFO其地址位宽为n,若数据位宽为W则该FIFO的容量为N*W bits。

                    现在在指针添加1个extra bit即地址的MSB,使其变为n+1 bits,该extra bit用来指示读/写指针是否连续递增并越过了FIFO的最后一个地址,若越过则该MSB加1,其他位清零。例如深度为8的fifo,需要采用1+3bits的地址位宽,MSB作为指针折回标志,低3bits作为地址。

                    那么判断机制读指针读出FIFO最后一个有效数据后即会停止递增)为:

                    ①如果两个指针的MSB不同,就说明写指针比读指针多折回一次,此时若除开MSB以外的地址位相等,则表示FIFO已满;

                    ②如果两个指针的MSB相同,就说明读写指针的折回次数相同,若其他地址位相等,则表示读写指针完全相等,FIFO已空。

  • 方案二:设置数据计数器
  •                 设置一个data_counter,当写使能有效时数据计数器加1,每读出一个数据时该计数器又减1。如此,当data_counter=0时FIFO为空,data_counter=FIFO深度时表明已满。

                    缺点:计数器会占用额外资源,当FIFO较大时,可能会降低FIFO的读写速度。

    2.2.2.异步fifo空满判断

  • 判断步骤如下:
  •                 ①地址指针采用二进制(binary)+extra bit

                    ②二进制指针转gray码后跨时钟域同步做比较

            当读写指针采用二进制表示且读写操作属于异步时钟时,读写指针做比较前需要先将其中一个指针同步到另一个指针的时钟域后再操作,直接同步这样容易产生亚稳态问题。

            可以使用一个二进制转gray码的转换电路,将地址转换为对应的gray码后再同步到另一个时钟域,进行对比产生空满指示,如左下图:

                    例如1+3bits的二进制地址完全转换为gray后如右上图所示,此时空满标志不能按照原来二进制的方法来判断,gary码指针的空满判断标准如下:

                    空标志:gray码地址完全相等(包括MSB)。

                    满标志:高两位(MSB+次高位)不同,其余各位相同。

                    PS:二进制与格雷码互相转换 。

  • 补充:
  •                 ①同步方向产生保守的空满机制:

                            读指针同步到写时钟域:经过一定的同步时间后,此时同步后(写时钟域的)读指针小于或等于真实的(读时钟域)的读指针,而写指针是即时且真实的,空满判断机制可产生保守的“假写满”(正确且安全设计)和错误的“读空”。

                            反之同理,总结:写时钟域产生正确的“假写满”,读时钟域产生正确的“假读空”

                    ②由于读写异步快时钟域同步慢时钟域指针可能会漏采,不会影响空满判断逻辑:

                            举例读慢写快:写指针同步到读时钟域发生漏采,即读时钟域采样到的写指针小于真实的(写时钟域的)写指针,此时不会导致“读空判断逻辑”错误,也是保守且正确的。反之亦然。

    3.FIFO代码设计示例

    3.1.同步FIFO代码

            同步FIFO由于没有跨时钟的操作,所以只需要使用二进制即可,不用格雷码操作。根据上面的分析,有两种方法进行表示full/empty状态,代码如下:

    //1、generate full/empty signal by addr
    assign full = (waddr_ptr == {~raddr_ptr[ADDR_WIDTH-1],
                                  radde_ptr[ADDR_WIDTH-2:0]});
    assign empty = (waddr_ptr == raddr_ptr);
    //2、generate full/empty signal by conuter
    always @(posedge clk or negedge rst_n)begin
    	if(!rst_n)begin
    		data_cnt <= {ADDR_WIDTH{1'b0}};
    	end
    	else if(wen && ren && !full && !empty)begin
    		data_cnt <= data_cnt;
    	end
    	else if(wen && !full)begin
    		data_cnt <= data_cnt + 1'b1;
    	end
    	else if(ren && !empty)begin
    		data_cnt <= data_cnt - 1'b1;
    	end
    end
    
    assign full = (data_cnt == FIFO_DEPTH);
    assign empty = (data_cnt == 0);

    3.2.异步FIFO代码

            由于存在读写时钟不同步的问题,采用的解决方法是:加两级寄存器同步 + 格雷码(目的都是消除亚稳态),代码示例如下: 

    `timescale 1ns/1ps
    module async_fifo #(
    	parameter	DATA_WIDTH	= 32,
    	parameter	DATA_DEPTH	=  8,
    	parameter	PTR_WIDTH	= $clog2(DATA_DEPTH)
    )(
    	//write interface	
    	input  wire					 clk_wr_i,
    	input  wire					 rst_n_wr_i,	
    	input  wire					 wr_en_i,
    	input  wire [DATA_WIDTH-1:0] wr_data_i ,
    	output wire					 wr_full_o,
    	
    	//read interface
    	input  wire					 clk_rd_i,
    	input  wire					 rst_n_rd_i,
    	input  wire					 rd_en_i,
    	output reg  [DATA_WIDTH-1:0] rd_data_o,
    	output wire					 rd_empty_o
    );
    	reg  [DATA_WIDTH-1:0]	fifo[DATA_DEPTH-1:0];
    
        reg                     wr_ptr_ext;
    	reg	 [ PTR_WIDTH-1:0]	wr_ptr;
    	wire [ PTR_WIDTH  :0]	wr_ptr_gray;
    	reg	 [ PTR_WIDTH  :0]	wr_ptr_gray_d1;
    	reg	 [ PTR_WIDTH  :0]	wr_ptr_gray_d2;
    
    	reg                     rd_ptr_ext;
    	reg  [ PTR_WIDTH-1:0]	rd_ptr;
    	wire [ PTR_WIDTH  :0]	rd_ptr_gray;
    	reg	 [ PTR_WIDTH  :0]	rd_ptr_gray_d1;
    	reg  [ PTR_WIDTH  :0]	rd_ptr_gray_d2;
    	
    //------------- ptr++ and data inout--------------
    always @(posedge clk_wr_i or negedge rst_n_wr_i) begin
    	if(!rst_n_wr_i)begin
    		{wr_ptr_ext, wr_ptr} <= {(PTR_WIDTH+1){1'b0}};
            fifo[wr_ptr]         <= { (DATA_WIDTH){1'b0}};
        end
    	else if(wr_en_i && !wr_full_o)begin
    		{wr_ptr_ext, wr_ptr} <= {wr_ptr_ext, wr_ptr} + 1'b1;
            fifo[wr_ptr]         <= wr_data_i;
        end
    end
    
    always @(posedge clk_rd_i or negedge rst_n_rd_i) begin
    	if(!rst_n_rd_i)begin
    		{rd_ptr_ext, rd_ptr} <= {(PTR_WIDTH+1){1'b0}};
    		rd_data_o            <= { (DATA_WIDTH){1'b0}};
        end
    	else if(rd_en_i && !rd_empty_o)begin
    		{rd_ptr_ext, rd_ptr} <= {rd_ptr_ext, rd_ptr} + 1'b1;
    		rd_data_o            <= fifo[rd_ptr];
        end
    end
    	
    //--------- binary to gray ---------
    assign rd_ptr_gray = {rd_ptr_ext, rd_ptr} ^ ({rd_ptr_ext, rd_ptr}>>1);
    assign wr_ptr_gray = {wr_ptr_ext, wr_ptr} ^ ({wr_ptr_ext, wr_ptr}>>1);
    
    //--------- pointer sync -----------
    always @(posedge clk_wr_i or negedge rst_n_wr_i) begin
    	if(!rst_n_rd_i) begin
    		rd_ptr_gray_d1 <= {(PTR_WIDTH+1){1'b0}};
    		rd_ptr_gray_d2 <= {(PTR_WIDTH+1){1'b0}};
    	end
    	else begin
    		rd_ptr_gray_d1 <= rd_ptr_gray;
    		rd_ptr_gray_d2 <= rd_ptr_gray_d1;
    	end
    end
    
    always @(posedge clk_rd_i or negedge rst_n_rd_i) begin
    	if(!rst_n_rd_i) begin
    		wr_ptr_gray_d1 <= {(PTR_WIDTH+1){1'b0}};
    		wr_ptr_gray_d2 <= {(PTR_WIDTH+1){1'b0}};
    	end
    	else begin
    		wr_ptr_gray_d1 <= wr_ptr_gray;
    		wr_ptr_gray_d2 <= wr_ptr_gray_d1;
    	end
    end
    
    //------------ full_o and empty_o ------------------
    assign wr_full_o  = (wr_ptr_gray=={~rd_ptr_gray_d2[PTR_WIDTH:PTR_WIDTH-1],
                         rd_ptr_gray_d2[PTR_WIDTH-2:0]}) ? 1'b1 : 1'b0;
    assign rd_empty_o = (rd_ptr_gray==wr_ptr_gray_d2) ? 1'b1 : 1'b0;
    
    endmodule
    
    

    4.FIFO的深度计算

    4.1.概念

            突发(burst)传输:In telecommunication, a burst transmission or data burst is the broadcast of a relatively high-bandwidth transmission over a short period。某个短时间内相对高带宽的数据传输。

            假如模块A不间断地往FIFO中写数据,模块B同样不间断地从FIFO中读数据,不同的是模块A写数据的时钟频率要大于模块B读数据的时钟频率,那么在一段时间内总是有一些数据没来得及被读走,如果系统一直在工作,那么那些没有被读走的数据会越累积越多,那么FIFO的深度需要是无穷大的,因此只有在突发数据传输过程中讨论FIFO深度才是有意义的。一次传递一包数据完成后再去传递下一包数据,一段时间内传递的数据个数称为burst length

            FIFO的最小深度与burst rate, burst size, read and write frequency等因素有关。要确定FIFO的深度,关键在于计算出在突发读写这段时间内有多少个数据没有被读走,即FIFO的最小深度就等于没有被读走的数据个数。

    4.2.深度计算示例

            假定模块A向FIFO写数据的时钟频率为fa,模块B从FIFO读数据的时钟频率为fb。

    场景1:idle cycles in both write and(or) read

    假设:

  • 写数据时钟频率fa=80MHz,读数据时钟频率fb=50MHz;
  • 突发长度= number of data to be transferred = 120;
  • 每隔1个cycle写一次,每隔3个cycle读一次。
  • 那么:

  • 每隔1个cycle写一次,意味着2个cycle才写一个数据;每隔3个cycle读一次,意味着4个cycle才读一个数据。
  • 写一个数据所需要的时间 = 2*1/80MHz = 25ns。突发传输中,写完所有数据所需要的时间 = 120*25ns = 3000ns。
  • 读一个数据所需要的时间 = 4*1/50MHz = 80ns。在3000ns内能够读走的数据个数 = 3000ns/80ns = 37.5。
  • 所以在3000ns内还没有被读走的数据个数 = 120-37.5 = 82.5,因此FIFO的最小深度为83。
  • 场景2fa ≤ fb with no idle cycles in both write and read

    假设:

  • 写数据时钟频率fa=40MHz,读数据时钟频率fb≥40MHz
  • 突发长度= number of data to be transferred = 120
  • 在突发传输过程中,数据都是连续读写的
  • 由于读数据比写数据要快,因此FIFO只起到跨时钟域的作用,FIFO的最小深度为1即可。

    场景3:Data rates are given,read and write random

            在工程设计中还存在一种情形,只给出数据在一段时间内的读写速率,怎么读写完全随机,这种情况需要考虑最坏的一种情况避免数据丢失。在最坏的情形中,读写的速率应该相差最大,也就是说需要找出最大的写速率和最小的读速率。

    假设:

  • 写数据时钟频率fa=80MHz,读数据时钟频率fb=50MHz
  • 在写时钟周期内,每100个周期就有40个数据写入FIFO
  • 在读时钟周期内,每10个周期可以有8个数据读出FIFO
  • 那么:

  • 首先没有给出数据的突发长度,从假设中可以得出每100个周期就有40个数据写入FIFO,因为数据是随机写入FIFO的,需要考虑做坏的情形,即写速率最大的情形,只有如下图背靠背的情形才是写速率最高的情形,burst length为80。
  • 注意:这里需要验证一下是否有解即写入burst数据时间必须大于等于读出burst数据时间,不然数据就会越累积越多,使得FIFO的深度必须为无穷大。首先写入80个数据需要的时间 = 1/80MHz*(80*100/40)=2500ns,读出80个数据需要的时间 = 1/50MHz*(80*10/8)=2000ns,由于写入burst数据时间大于对出burst数据时间,因此有解。
  • 下面来计算FIFO最小深度,连续写入80个数据最快所需要时间 = 1/80MHz * 80 = 1000ns。
  • 从FIFO中读出一个数据至少所需时间 = (1/50MHz) * (10/8) = 25ns。那么在1000ns内能够读出的数据 = 1000ns/25ns = 40。
  • 在1000ns内没有读出的数据 = 80 – 40 = 40,因此FIFO的最小深度为40。
  •          参考FIFO深度计算。

    4.3.异步FIFO的深度不为2的正整数次幂

    4.3.1.FIFO的深度为1

            方案一:将深度加1变为2,这样地址指针用1bit表示即可,且不用添加extra bit。

             方案二:采用脉冲同步读写信号,参考深度为1的异步FIFO设计。

            方案三:采用握手机制去跨时钟,不属于FIFO类型。

    4.3.2.FIFO的深度为其他任意数

            无论FIFO的深度为奇数或偶数,都需要对地址指针扩展1bit来作为标志位,这样产生的地址指针循环一定为偶数,再利用格雷码的环回对称性,采用“掐头去尾+地址偏移”的方法。

            地址同步后不能采用原来2的整数次幂的gray判断空满机制来做判断,此时可以将格雷码再转回二进制后再做空满判断。

            参考任意深度异步FIFO设计。

    5.读写位宽不一致问题

            对于异步fifo,由于地址不能跳变,fifo的位宽可以选择输入输出位宽的最小公倍数,会有一定的保守性。

            参考FPGA之FIFO详解,读写位宽不同。

    6.异步FIFO中格雷码的约束

    6.1.设置读写地址格雷码约束的原因

    6.1.1.格雷码各bit位间延时不一样

            格雷码各bit位延时不一致,导致afifo功能异常。

            假设3bit的gray码各比特位延时不一致,比如gray[1]延时比gray[0]多一个采样周期,比如gray[2]延时比gray[1]多一个采样周期,虽然源端格雷码是符合要求的,但是由于格雷码延时不一致,导致采样端采样的格雷码不符合要求(不正确)。

    6.1.2.格雷码到同步器的延时过大

            格雷码到同步器的延时越长,流水间隔越大,afifo性能越差。

            假如afifo深度为16,写地址waddr_gray码到同步器的延时为8个周期,加上同步器3个周期,写数据侧写入数据后,至少需要11个read_clk后读数据侧empty信号无效,也就是说至少11个read_clk后读侧才能读数据。而如果写地址waddr_gray码到同步器的延时为1个周期,则写数据侧写入数据后,只需要4个周期,读侧就能读数据了。

    6.2.具体约束

            为了保证异步fifo的功能和性能保证,需要在综合约束文件sdc中,约束异步FIFO格雷码的最大延时

            约束如图所示:从格雷码寄存器的时钟端口—->到同步器的输入端口的最大延时。写地址waddr和读地址raddr格雷码同步都需要设置set_max_delay。此处set_max_delay是为了保证源端信号到达目的端被采样时的格雷码唯一bit跳变特性。延时可设置为读写时钟中最快时钟周期的一半,也可以设置成源端时钟的一半,或者设置成源端时钟的倍数且bit间的skew明显小于一个源端时钟周期

    作者:CuteBaBaKiller

    物联沃分享整理
    物联沃-IOTWORD物联网 » 深入了解FIFO的工作原理及设计方法

    发表评论