一、FIFO簡介
FIFO是英文First In First Out 的縮寫,是一種先進先出的數據緩存器,它與普通存儲器的區別是沒有外部讀寫地址線,這樣使用起來非常簡單,但缺點就是只能順序寫入數據,順序的讀出數據,其數據地址由內部讀寫指針自動加1完成,不能像普通存儲器那樣可以由地址線決定讀取或寫入某個指定的地址。
用途1
跨時鍾域:異步FIFO讀寫分別采用相互異步的不同時鍾。在現代集成電路芯片中,隨着設計規模的不斷擴大,一個系統中往往含有數個時鍾,多時鍾域帶來的一個問題就是,如何設計異步時鍾之間的接口電路。異步FIFO是這個問題的一種簡便、快捷的解決方案,使用異步FIFO可以在兩個不同時鍾系統之間快速而方便地傳輸實時數據。
用途2
位寬變換:對於不同寬度的數據接口也可以用FIFO,例如單片機位8位數據輸出,而DSP可能是16位數據輸入,在單片機與DSP連接時就可以使用FIFO來達到數據匹配的目的。
二、分類
同步FIFO 是指讀時鍾和寫時鍾為同一個時鍾,在時鍾沿來臨時同時發生讀寫操作;
異步FIFO 是指讀寫時鍾不一致,讀寫時鍾是互相獨立的。
三、FIFO的常見參數
FIFO的寬度:即FIFO一次讀寫操作的數據位;
FIFO的深度:指的是FIFO可以存儲多少個N位的數據(如果寬度為N)。
滿標志:FIFO已滿或將要滿時由FIFO的狀態電路送出的一個信號,以阻止FIFO的寫操作繼續向FIFO中寫數據而造成溢出(overflow)。
空標志:FIFO已空或將要空時由FIFO的狀態電路送出的一個信號,以阻止FIFO的讀操作繼續從FIFO中讀出數據而造成無效數據的讀出(underflow)。
讀時鍾:讀操作所遵循的時鍾,在每個時鍾沿來臨時讀數據。
寫時鍾:寫操作所遵循的時鍾,在每個時鍾沿來臨時寫數據。
四、 FIFO詳解
4.1 讀寫指針的工作原理
讀指針:總是指向下一個將要被寫入的單元,復位時,指向第1個單元(編號為0)。
寫指針:總是指向當前要被讀出的數據,復位時,指向第1個單元(編號為0)
4.2 FIFO的“空”/“滿”檢測
FIFO設計的關鍵:產生可靠的FIFO讀寫指針和生成FIFO“空”/“滿”狀態標志。
當讀寫指針相等時,表明FIFO為空,這種情況發生在復位操作時,或者當讀指針讀出FIFO中最后一個字后,追趕上了寫指針時,如下圖所示:
當讀寫指針再次相等時,表明FIFO為滿,這種情況發生在,當寫指針轉了一圈,折回來(wrapped around)又追上了讀指針,如下圖:
為了區分到底是滿狀態還是空狀態,可以采用以下方法:
方法1:在指針中添加一個額外的位(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為空;
方法2:
4.3 二進制FIFO指針的考慮
二進制轉格雷碼
將一個二進制的計數值從一個時鍾域同步到另一個時鍾域的時候很容易出現問題,因為采用二進制計數器時所有位都可能同時變化,在同一個時鍾沿同步多個信號的變化會產生亞穩態問題。而使用格雷碼只有一位變化,因此在兩個時鍾域間同步多個位不會產生問題。所以需要一個二進制到gray碼的轉換電路,將地址值轉換為相應的gray碼,然后將該gray碼同步到另一個時鍾域進行對比,作為空滿狀態的檢測。
4.4 使用gray碼進行對比,如何判斷“空”與“滿”
使用gray碼解決了一個問題,但同時也帶來另一個問題,即在格雷碼域如何判斷空與滿。
對於“空”的判斷依然依據二者完全相等(包括MSB);
而對於“滿”的判斷,如下圖,由於gray碼除了MSB外,具有鏡像對稱的特點,當讀指針指向7,寫指針指向8時,除了MSB,其余位皆相同,不能說它為滿。因此不能單純的只檢測最高位了,在gray碼上判斷為滿必須同時滿足以下3條:
- wptr和同步過來的rptr的MSB不相等,因為wptr必須比rptr多折回一次。
- wptr與rptr的次高位不相等,如上圖位置7和位置15,轉化為二進制對應的是0111和1111,MSB不同說明多折回一次,111相同代表同一位置。
- 剩下的其余位完全相等。
4.5 總體實現
系統的總體框圖如下:
四、同步化分析
由於是異步FIFO的設計,讀寫時鍾不一樣,在產生讀空信號和寫滿信號時,會涉及到跨時鍾域的問題,如何解決?
跨時鍾域的問題:由於讀指針是屬於讀時鍾域的,寫指針是屬於寫時鍾域的,而異步FIFO的讀寫時鍾域不同,是異步的,要是將讀時鍾域的讀指針與寫時鍾域的寫指針不做任何處理直接比較肯定是錯誤的,因此我們需要進行同步處理以后仔進行比較
解決方法:加兩級寄存器同步 + 格雷碼(目的都是消除亞穩態)
1.使用異步信號進行使用的時候,好的設計都會對異步信號進行同步處理,同步一般采用多級D觸發器級聯處理。這種模型大部分資料都說的是第一級寄存器產生亞穩態后,第二級寄存器穩定輸出概率為90%,第三極寄存器穩定輸出的概率為99%,如果亞穩態跟隨電路一直傳遞下去,那就會令自我修護能力較弱的系統直接崩潰。
2.將一個二進制的計數值從一個時鍾域同步到另一個時鍾域的時候很容易出現問題,因為采用二進制計數器時所有位都可能同時變化,在同一個時鍾沿同步多個信號的變化會產生亞穩態問題。而使用格雷碼只有一位變化,因此在兩個時鍾域間同步多個位不會產生問題。所以需要一個二進制到gray碼的轉換電路,將地址值轉換為相應的gray碼,然后將該gray碼同步到另一個時鍾域進行對比,作為空滿狀態的檢測。
那么,多位二進制碼如何轉化為格雷碼?
假設原始的值從0開始,格雷碼產生的規律是:
- 第一步,改變最右邊的位元值;
- 第二步,改變右起第一個為1的位元的左邊位元;
- 第三步,第四步重復第一步和第二步,直到所有的格雷碼產生完畢(換句話說,已經走了(2^n) - 1 步)。
用一個例子來說明:
假設產生3位元的格雷碼,原始值位 000
第一步:改變最右邊的位元值: 001
第二步:改變右起第一個為1的位元的左邊位元: 011
第三步:改變最右邊的位元值: 010
第四步:改變右起第一個為1的位元的左邊位元: 110
第五步:改變最右邊的位元值: 111
第六步:改變右起第一個為1的位元的左邊位元: 101
第七步:改變最右邊的位元值: 100
采用格雷碼每次就只有一個位變化。格雷碼和自然二進制碼如下。圖片來自百度百科。
換一種描述方法:
二進制的數據右移一位與二進制數據進行取反
assign gray_code = (bin_code>>1) ^ bin_code;
使用gray碼解決了一個問題,但同時也帶來另一個問題,即在格雷碼域如何判斷空與滿。
這里直接給出結論:
判斷讀空時:需要 讀時鍾域的格雷碼rgray_next 和 被同步到讀時鍾域的寫指針rd2_wp 每一位完全相同;
判斷寫滿時:需要 寫時鍾域的格雷碼wgray_next 和 被同步到寫時鍾域的讀指針wr2_rp 高兩位不相同,其余各位完全相同;
1 assign full = (wr_addr_gray == {~(rd_addr_gray_d2[addr_width-:2]),rd_addr_gray_d2[addr_width-2:0]}) ;//高兩位不同 2 assign empty = ( rd_addr_gray == wr_addr_gray_d2 );
五、Verilog實現
module fifo_async#( parameter data_width = 16, parameter data_depth = 256, parameter addr_width = 8 ) ( input rst, input wr_clk, input wr_en, input [data_width-1:0] din, input rd_clk, input rd_en, output reg valid, output reg [data_width-1:0] dout, output empty, output full ); reg [addr_width:0] wr_addr_ptr;//地址指針,比地址多一位,MSB用於檢測在同一圈 reg [addr_width:0] rd_addr_ptr; wire [addr_width-1:0] wr_addr;//RAM 地址 wire [addr_width-1:0] rd_addr; wire [addr_width:0] wr_addr_gray;//地址指針對應的格雷碼 reg [addr_width:0] wr_addr_gray_d1; reg [addr_width:0] wr_addr_gray_d2; wire [addr_width:0] rd_addr_gray; reg [addr_width:0] rd_addr_gray_d1; reg [addr_width:0] rd_addr_gray_d2; reg [data_width-1:0] fifo_ram [data_depth-1:0]; //=========================================================write fifo genvar i; generate for(i = 0; i < data_depth; i = i + 1 ) begin:fifo_init always@(posedge wr_clk or posedge rst) begin if(rst) fifo_ram[i] <= 'h0;//fifo復位后輸出總線上是0,並非ram中真的復位。可無 else if(wr_en && (~full)) fifo_ram[wr_addr] <= din; else fifo_ram[wr_addr] <= fifo_ram[wr_addr]; end end endgenerate //========================================================read_fifo always@(posedge rd_clk or posedge rst) begin if(rst) begin dout <= 'h0; valid <= 1'b0; end else if(rd_en && (~empty)) begin dout <= fifo_ram[rd_addr]; valid <= 1'b1; end else begin dout <= 'h0;//fifo復位后輸出總線上是0,並非ram中真的復位,只是讓總線為0; valid <= 1'b0; end end assign wr_addr = wr_addr_ptr[addr_width-1-:addr_width]; assign rd_addr = rd_addr_ptr[addr_width-1-:addr_width]; //=============================================================格雷碼同步化 always@(posedge wr_clk ) begin rd_addr_gray_d1 <= rd_addr_gray; rd_addr_gray_d2 <= rd_addr_gray_d1; end always@(posedge wr_clk or posedge rst) begin if(rst) wr_addr_ptr <= 'h0; else if(wr_en && (~full)) wr_addr_ptr <= wr_addr_ptr + 1; else wr_addr_ptr <= wr_addr_ptr; end //=========================================================rd_clk always@(posedge rd_clk ) begin wr_addr_gray_d1 <= wr_addr_gray; wr_addr_gray_d2 <= wr_addr_gray_d1; end always@(posedge rd_clk or posedge rst) begin if(rst) rd_addr_ptr <= 'h0; else if(rd_en && (~empty)) rd_addr_ptr <= rd_addr_ptr + 1; else rd_addr_ptr <= rd_addr_ptr; end //========================================================== translation gary code assign wr_addr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr; assign rd_addr_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr; assign full = (wr_addr_gray == {~(rd_addr_gray_d2[addr_width-:2]),rd_addr_gray_d2[addr_width-2:0]}) ;//高兩位不同 assign empty = ( rd_addr_gray == wr_addr_gray_d2 ); endmodule
六、重要補充
關於異步FIFO的關鍵技術,有兩個,一個是格雷碼減小亞穩態,另一個是指針信號跨異步時鍾域的傳遞。
我在自己寫異步FIFO的時候也很疑惑,地址指針在同步化的時候,肯定會產生至少兩個周期的延遲,如果是從快時鍾域到慢時鍾域,快時域的地址指針並不能都被慢時域的時鍾捕獲,同步后的指針比起實際的指針延遲會更大。如果以此來產生fifo_empty和fifo_full 信號會非常不准器。
查找資料和仿真后發現,數字電路的世界真的很神奇,還有很多的東西需要去學習。非常巧妙,FIFO中的一個潛在的條件是write_ptr總是大於或者等於read_ptr;分為兩種情況,寫快讀慢和寫慢讀快。
1.在寫時鍾大於讀時鍾時,產生fifo_empty信號,需要將write_ptr同步到讀時鍾域,寫指針會有延時,可能比實際的寫地址要小,如果不滿足fifo_empty的產生條件,沒問題。如果滿足fifo_empty的觸發條件,說明此時同步后的write_ptr == read_ptr,即實際的write_ptr >= read_ptr,最壞的情況就是write_ptr > read_ptr,像這種FIFO非空而產生空標志信號的情況稱為“虛空”,但是也並不影響FIFO的功能。
2.在寫時鍾大於讀時鍾時,產生fifo_full信號,需要將read_ptr同步到寫時鍾域,讀指針會有延時,可能比實際的讀地址要小,如果不滿足fifo_full的產生條件,沒問題。如果滿足fifo_full的觸發條件,說明此時同步后的read_ptr == write_ptr - fifo_depth,即實際的read_ptr >= read_ptr - fifo_depth,最壞的情況就是read_ptr > read_ptr - fifo_depth,像這種FIFO非滿而產生滿標志信號的情況稱為“虛滿”,但是也並不影響FIFO的功能。
寫慢讀快的情況也同上,並沒有大的差異,不再分析。
關於格雷碼減小亞穩態,如果讀寫時鍾差距過大,從快時鍾域同步到慢時鍾域的信號,時鍾捕獲的相鄰兩個數據變化並不是只有一個bit位的改變,可能導致格雷碼失去原來的意義。