FPGA基础学习(11) -- FIFO设计(style#1)

FIFO是跨时钟域数据传输中常用的缓存器。一般情况下,自己设计的异步FIFO(无特殊说明以下均简称FIFO)虽然能应付90~99%的场景,但是由于设计缺陷,导致在1%的极端情况下会出问题,还不容易发现,所以设计合理的FIFO至关重要。

对于同步FIFO,因为读写属于同一时钟域,可以直接采用计数的方式来计算FIFO存储空间的动态变化,但是异步FIFO不能这么操作,因为读写时钟域完全有可能频率差异比较大,并且会面临暂稳态的问题。其实FIFO的设计要点,归根结底是设计正确的空/满信号。即数据写满的时候,写时钟域能及时接收到满信号,停止写入;数据读空的时候,读时钟域能及时接收到空信号,停止读出。

学习《Clifford_E._Cummings》经典论文集中的有关异步FIFO的论文,其中介绍了2种FIFO设计方案,本文是源于论文《Simulation and Synthesis Techniques for Asynchronous FIFO Design》中介绍的style#1。论文中引入了一种跨时钟域同步格雷码进行比较的方式来判断空/满。

1. 异步指针

首先来建立对指针的基本认识。

我们知道FIFO实际上由一个异步RAM作为基本的存储单元,再配合外面的控制逻辑实现的。控制逻辑中最重要的就是指针,了解计算机体系结构的都知道,指针无非就是指向存储空间的一个标识。

如上图所示为一个深度为16的FIFO指针示意图。一个FIFO含有一个写指针raddr[3:0]和一个读指针waddr[3:0]。在FIFO中注意以下两点:

  • 读指针指向当前要读取的数据位置(空间)
  • 写指针指向下一个要写入的位置(空间),即下一个数据来的时候才写入该空间;

读写指针在工作中呈现“你追我赶”的情形。比如当raddr = 0,waddr = 7时,代表FIFO中存有7个数据。当raddr追上waddr时,即 raddr = waddr = 7,代表刚才写入的7个数据被读走,此时FIFO为空。在复位这种特殊的情况下,raddr和waddr的初始状态均为0,此时FIFO显然也为空。

假设此时只写入数据,当写到地址15(1111)时,继续写入数据,指针增加会翻转到地址0(0000),当写入到waddr = 7时,读指针也在该位置,即raddr = waddr = 7,此时显然存储空间已满。

那么当raddr = waddr时,到底是空还是满?所以设计中引入“补位”指针的概念,增加一位最高位,代表指针是否经过了一个轮询(翻转)。所以上述指针地址变为5位,当写指针从地址15翻转到0时,指针实际上是从01111变为10000。此时写指针最高位1 代表翻转一次,读指针的最高位依然是0。

因此,我们判断空满的条件是:空的时候全相等,满的时候最高位不等,其余位相等。

2. 格雷码计数

在上节的例子中,我们提到了二进制编码指针的翻转问题,即15->0(1111->0000),在一个写入周期中指针的数据位翻转了4次,在实际使用中这无疑会增加风险,因为4 bit的数据走线延迟不一致,导致同一采样时钟沿上可能在某一位上出现暂稳态。因此,论文提出了一种格雷码指针的编码的方式(格雷码在FIFO设计中还有其他优势,后面再讨论)。

如上图所示,给出了0-15的格雷码,按照上一节提到的最高位补位指示空满的原则,4位格雷码指针可以设计深度为8的FIFO。

假设存储空间位置为0-7,rptr = wptr = 7(0100)时,表示写入的8个数据全部被读出,此时FIFO为空。继续写入1个数据,写指针变为8(1100),按照上一节的结论,当读写指针的最高位不同,其余位相同时,FIFO为满。显然,这一结论在格雷码使用过程中出现了问题。因为从空间7到8,才写入了1个数据,它怎么可能满。

观察格雷码的编码形式可以发现,除最高位以为,0-7和8-15是关于中间位置的“镜像对称”,即除最高位外,7和8一致,6和9一致……0和15一致,如上图所示。假设我们把8-15的次高位取反会出现一种什么样的情况?继续按照上面的例子假设,当rptr = wptr = 7 时,FIFO为空。继续写数据,当格雷码变为1100(次高位取反后的15),表示FIFO中又写入了8个数据,这时候FIFO才是真正的满了。此时rptr =0100,wptr = 1100,此时满足最高位不同,其余位相同,则表示满的原则,但前提是次高位已经取了反。所以综上所述,使用格雷码采用的比较原则是“最高位和次高位不同,其余位相同,则表示FIFO满”。

总结一下,使用格雷码判断空满,原则是:

  • 格雷码各位完全相同,代表空;
  • 最高位和次高位不同,其余位相同,代表满

3. 如何操作空/满

上两节得出了通过判断补位格雷码的关系来操作空/满,具体操作肯定是读指针的格雷码和写指针的格雷码进行比较,但是因为读写时属于两个不同的时钟域,两者的时钟频率可能差异很大,具体如何实现呢?显然一个指针肯定要同步到对方时钟域上进行比较。先给出论文的设计:

  • “空”:读时钟域中,比较读指针和同步过来的写指针,如果两指针相同 ,为“空”;
  • “满”:写时钟域中,比较写时钟和同步过来的读指针,如果两指针最高位和次高位不同,其余位相同,为“满”;

因为是跨时钟域,所以会涉及到读写时钟差异的问题,论文中对两个读者感到疑惑的问题进行了解答

- 问题1:同步格雷码的过程中变化了2次,但只有一次被对方时钟域采集到,会不会造成多位同步出错的问题?

(ps:实际上对原位的问题和解答翻译理解不到位,此处只能我的理解简单说一下)

答案是不会出问题。当然如果在慢时钟上升沿采样的过程中,快时钟域的格雷码发生变化,比如会出一些问题。但是这是另外一个层面的问题(时序暂稳态问题)。实际的问题是,如果快时钟域的格雷码变化了两次或多次,但是慢时钟只采集到第二次的结果,是不产生问题的。因为第一次格雷码的变化已经代表操作正常完成(不管是读还是写),第二次变化仅仅表示当前的状态,就判断当前状态即可。(语无伦次了……算了,这个问题过掉,不清楚作者表达)

- 问题2:假设写时钟频率更高,写指针在与读指针比较的时候,还没来得及产生满信号,写这一头还在不停的写入数据,会不会造成上溢出?(读空是同样的道理)

(ps:这个问题是FIFO操作最常见的问题)

答案是不会出问题。解答这个问题,我们让深刻理解本节开头的原则,即“空信号是在读时钟域产生,满信号是在写时钟域产生”。

首先,我们要明确FIFO的使用场景,不会是连续数据的跨时钟域传输,因为这样必然会丢掉部分数据。所以必然是块数据传递,要么写快读慢,要么写慢读快。

在写快读慢的情形,担心满信号没有及时产生,导致写溢出。满信号是在写时钟域产生,即读指针同步到写时钟域,这个时候写指针是不可能越过读指针的,要么就最高位和次高位不同,其余位相等,产生满信号,这时候立刻停止写入数据了。

那就又有疑问了,在比较指针的时候,读时钟域继续再读,可能读出几个数据了,这个时候产生满信号合适吗?这就是文章中所说的“虚满”,虚满无法就是FIFO空出了几个空间而已,不会导致数据出问题,这是一种保守的设计方法。

同理,读的时候也不会出现,下溢出的情况。但也有“需空”情形。

4. 仿真结果

论文中有详细的源码。为了便于理解其中指针的变化,写了段测试代码用于仿真观察。testbench先写满,再读空,再边写边读。


   module fifo1_sim();
	parameter DSIZE = 8;
    parameter ASIZE = 3;
    
    wire  [DSIZE-1:0] rdata;
    wire              wfull;
    wire              rempty;
    
    reg   [DSIZE-1:0] wdata;
    reg   winc, wclk, wrst_n;
    reg   rinc, rclk, rrst_n;
    
    reg             wr_en;
    reg             rd_en;
    reg             wr_rd;
    
    initial begin
        wrst_n = 1'b0;
        rrst_n = 1'b0;
        #50;
        wrst_n = 1'b1;
        rrst_n = 1'b1;
    end

    initial begin
        wclk = 1'b0;
        #10;
        forever #5 wclk =~wclk;
    end
    
    initial begin
        rclk = 1'b0;
        #10;
        forever #10 rclk =~rclk;
    end

    
    initial begin
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_rd = 1'b0;
        #100;
        wr_en = 1'b1;
        rd_en = 1'b0;
        wr_rd = 1'b0;
        #150 
        wr_en = 1'b0;
        rd_en = 1'b1;
        wr_rd = 1'b0;
        #200
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_rd = 1'b1;
        #200 
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_rd = 1'b0;
    
    end
    
    always @(posedge wclk or negedge wrst_n) begin
        if(wrst_n == 1'b0) begin
            wdata <= 8'h10;
            winc <= 1'b0;
        end else if(wr_en) begin
            wdata <= wdata + 8'd1;
            winc <= 1'b1;
        end else if(wr_rd) begin
            wdata <= wdata + 8'd1;
            winc <= 1'b1;
        end else begin
            wdata <= wdata;
            winc <= 1'b0;
        end
    end
    
     always @(posedge rclk or negedge rrst_n) begin
        if(rrst_n == 1'b0) begin
            rinc <= 1'b0;
        end else if(rd_en) begin
            rinc <= 1'b1;
        end else if(wr_rd) begin
            rinc <= 1'b1;
        end else begin
            rinc <= 1'b0;
        end
    end
    
    
fifo1 #( DSIZE,ASIZE) 
fifo1_i
(
    .rdata  (rdata),
    .wfull  (wfull),
    .rempty (rempty),
    
    .wdata  (wdata),
    .winc   (winc), 
    .wclk   (wclk), 
    .wrst_n (wrst_n),
    .rinc   (rinc), 
    .rclk   (rclk), 
    .rrst_n (rrst_n)
);



endmodule

仿真图如下:

5. 特别说明

  • 原文有很多细节描述,实际上并不是理解很透彻。如问题1,还有比较二进制编码和格雷码优劣的论述。
  • 该篇论文(V1.2)感觉有问题,源代码实现的应该是style#2的框图,如下图所示,即二进制编码驱动RAM的地址,格雷码进行指针比较。多说一句,源码中格雷码和RAM的地址是一一对应的标识关系,格雷码不是地址,只是对地址空间的一个描述,用于跨时钟域比较。
  • 网上有帖子争论较大的是,要是读写时钟频率差距过大,比较1000倍,FIFO会不会出问题?从论文的角度分析,不会出问题(可以展开讨论)。从而牵扯另外的一个话题,FIFO最小深度的计算。

posted @ 2020-03-04 14:03  肉娃娃  阅读(1246)  评论(0编辑  收藏  举报