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最小深度的計算。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM