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