FIFO是英文First In First Out 的縮寫,是一種先進先出的數據緩存器,他與普通存儲器的區別是沒有外部讀寫地址線,這樣使用起來非常簡單,但缺點就是只能順序寫入數據,順序的讀出數據, 其數據地址由內部讀寫指針自動加1完成,不能像普通存儲器那樣可以由地址線決定讀取或寫入某個指定的地址。
作用: FIFO一般用於不同時鍾域之間的數據傳輸,比如FIFO的一端是AD數據采集, 另一端是計算機的PCI總線,假設其AD采集的速率為16位 100K SPS,那么每秒的數據量為100K×16bit=1.6Mbps,而PCI總線的速度為33MHz,總線寬度32bit,其最大傳輸速率為 1056Mbps,在兩個不同的時鍾域間就可以采用FIFO來作為數據緩沖。另外對於不同寬度的數據接口也可以用FIFO,例如單片機位8位數據輸出,而 DSP可能是16位數據輸入,在單片機與DSP連接時就可以使用FIFO來達到數據匹配的目的。
分類:FIFO的分類根均FIFO工作的時鍾域,可以將FIFO分為同步FIFO和異步FIFO。同步FIFO是指讀時鍾和寫時鍾為同一個時鍾。在時鍾沿來臨時同時發生讀寫操作。異步FIFO是指讀寫時鍾不一致,讀寫時鍾是互相獨立的。
若輸入輸出總線為同一時鍾域,FIFO只是作為緩存使用,用同步FIFO即可,此時,FIFO在同一時鍾下工作,FIFO的寫使能、讀使能、滿信號、空信號、輸入輸出數據等各種信號都在同一時鍾沿打入或輸出。
若輸入輸出為不同時鍾域,FIFO作時鍾協同作用,需要采用異步FIFO,此時,FIFO在讀與寫分別在各自時鍾下工作,FIFO的寫使能、寫滿信號、輸入數據等各種輸入信號都在同一輸入時鍾沿打入或輸出。讀使能、讀空信號、輸出數據等各種輸出信號都在同一輸出時鍾沿打入或輸出。
設計:FIFO設計的難點在於怎樣判斷FIFO的空/滿狀態。為了保證數據正確的寫入或讀出,而不發生益處或讀空的狀態出現,必須保證FIFO在滿的情況下,不 能進行寫操作。在空的狀態下不能進行讀操作。怎樣判斷FIFO的滿/空就成了FIFO設計的核心問題。
讀寫指針的工作原理
讀指針:總是指向下一個將要被寫入的單元,復位時,指向第1個單元(編號為0)。
寫指針:總是指向當前要被讀出的數據,復位時,指向第1個單元(編號為0)
FIFO的“空”/“滿”檢測
FIFO設計的關鍵:產生可靠的FIFO讀寫指針和生成FIFO“空”/“滿”狀態標志。
當讀寫指針相等時,表明FIFO為空,這種情況發生在復位操作時,或者當讀指針讀出FIFO中最后一個字后,追趕上了寫指針時,如下圖所示:
當讀寫指針再次相等時,表明FIFO為滿,這種情況發生在,當寫指針轉了一圈,折回來(wrapped around)又追上了讀指針,如下圖:
為了區分到底是滿狀態還是空狀態,可以采用以下方法:
在指針中添加一個額外的位(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為空;
.........................................................................................................................................
一、同步FIFO的Verilog代碼
在modlesim中驗證過。
/****************************************************** A fifo controller verilog description. ******************************************************/ module fifo(datain, rd, wr, rst, clk, dataout, full, empty); input [7:0] datain; input rd, wr, rst, clk; output [7:0] dataout; output full, empty; wire [7:0] dataout; reg full_in, empty_in; reg [7:0] mem [15:0]; reg [3:0] rp, wp; assign full = full_in; assign empty = empty_in; // memory read out assign dataout = mem[rp]; // memory write in always@(posedge clk) begin if(wr && ~full_in) mem[wp]<=datain; end // memory write pointer increment always@(posedge clk or negedge rst) begin if(!rst) wp<=0; else begin if(wr && ~full_in) wp<= wp+1'b1; end end // memory read pointer increment always@(posedge clk or negedge rst)begin if(!rst) rp <= 0; else begin if(rd && ~empty_in) rp <= rp + 1'b1; end end // Full signal generate always@(posedge clk or negedge rst) begin if(!rst) full_in <= 1'b0; else begin if( (~rd && wr)&&((wp==rp-1)||(rp==4'h0&&wp==4'hf))) full_in <= 1'b1; else if(full_in && rd) full_in <= 1'b0; end end // Empty signal generate always@(posedge clk or negedge rst) begin if(!rst) empty_in <= 1'b1; else begin if((rd&&~wr)&&(rp==wp-1 || (rp==4'hf&&wp==4'h0))) empty_in<=1'b1; else if(empty_in && wr) empty_in<=1'b0; end end endmodule
二、異步FIFO
(1)由於是異步FIFO的設計,讀寫時鍾不一樣,在產生讀空信號和寫滿信號時,會涉及到跨時鍾域的問題,如何解決?
跨時鍾域的問題:由於讀指針是屬於讀時鍾域的,寫指針是屬於寫時鍾域的,而異步FIFO的讀寫時鍾域不同,是異步的,要是將讀時鍾域的讀指針與寫時鍾域的寫指針不做任何處理直接比較肯定是錯誤的,因此我們需要進行同步處理以后仔進行比較
解決方法:加兩級寄存器同步 + 格雷碼(目的都是消除亞穩態)
1.使用異步信號進行使用的時候,好的設計都會對異步信號進行同步處理,同步一般采用多級D觸發器級聯處理,如下圖。這種模型大部分資料都說的是第一級寄存器產生亞穩態后,第二級寄存器穩定輸出概率為90%,第三極寄存器穩定輸出的概率為99%,如果亞穩態跟隨電路一直傳遞下去,那就會另自我修護能力較弱的系統直接崩潰。
2.將一個二進制的計數值從一個時鍾域同步到另一個時鍾域的時候很容易出現問題,因為采用二進制計數器時所有位都可能同時變化,在同一個時鍾沿同步多個信號的變化會產生亞穩態問題。而使用格雷碼只有一位變化,因此在兩個時鍾域間同步多個位不會產生問題。所以需要一個二進制到gray碼的轉換電路,將地址值轉換為相應的gray碼,然后將該gray碼同步到另一個時鍾域進行對比,作為空滿狀態的檢測。
那么,多位二進制碼如何轉化為格雷碼?
換一種描述方法:
verilog代碼實現就一句:assign gray_code = (bin_code>>1) ^ bin_code;
(2)在格雷碼域如何判斷空與滿?
這里直接給出結論:
判斷讀空時:需要讀時鍾域的格雷碼rgray_next和被同步到讀時鍾域的寫指針rd2_wp每一位完全相同;
判斷寫滿時:需要寫時鍾域的格雷碼wgray_next和被同步到寫時鍾域的讀指針wr2_rp高兩位不相同,其余各位完全相同;
(3)Verilog實現
這個是基於RAM的異步FIFO代碼,個人認為代碼結構簡單易懂,非常適合於考試中填寫。
module fifo #( parameter WSIZE = 8; parameter DSIZE = 32; ) ( input wr_clk, input rst, input wr_en, input [WSIZE-1 : 0]din, input rd_clk, input rd_en, output [WSIZE-1 : 0]dout, output reg rempty, output reg wfull ); //定義變量 reg [WSIZE-1 :0] mem [DSIZE-1 : 0]; reg [WSIZE-1 : 0] waddr,raddr; reg [WSIZE : 0] wbin,rbin,wbin_next,rbin_next; reg [WSIZE : 0] wgray_next,rgray_next; reg [WSIZE : 0] wp,rp; reg [WSIZE : 0] wr1_rp,wr2_rp,rd1_wp,rd2_wp; wire rempty_val,wfull_val; //輸出數據 assign dout = mem[raddr]; //輸入數據 always@(posedge wr_clk) if(wr_en && !wfull) mem[waddr] <= din; //1.產生存儲實體的讀地址raddr; 2.將普通二進制轉化為格雷碼,並賦給讀指針rp always@(posedge rd_clk or negedge rst_n) if(!rst_n) {rbin,rp} <= 0; else {rbin,rp} <= {rbin_next,rgray_next}; assign raddr = rbin[WSIZE-1 : 0]; assign rbin_next = rbin + (rd_en & ~rempty); assign rgray_next = rbin_next ^ (rbin_next >> 1); //1.產生存儲實體的寫地址waddr; 2.將普通二進制轉化為格雷碼,並賦給寫指針wp always@(posedge wr_clk or negedge rst_n) if(!rst_n) {wbin,wp} <= 0; else {wbin,wp} <= {wbin_next,wgray_next}; assign waddr = wbin[WSIZE-1 : 0]; assign wbin_next = wbin + (wr_en & ~wfull); assign wgray_next = wbin_next ^ (wbin_next >> 1); //將讀指針rp同步到寫時鍾域 always@(posedge wr_clk or negedge rst_n) if(!rst_n) {wr2_rp,wr1_rp} <= 0; else {wr2_rp,wr1_rp} <= {wr1_rp,rp}; //將寫指針wp同步到讀時鍾域 always@(posedge rd_clk or negedge rst_n) if(!rst_n) {rd2_wp,rd1_wp} <= 0; else {rd2_wp,rd1_wp} <= {rd1_wp,wp}; //產生讀空信號rempty assign rempty_val = (rd2_wp == rgray_next); always@(posedge rd_clk or negedge rst_n) if(rst_n) rempty <= 1'b1; else rempty <= rempty_val; //產生寫滿信號wfull assign wfull_val = ((~(wr2_rp[WSIZE : WSIZE-1]),wr2_rp[WSIZE-2 : 0]) == wgray_next); always@(posedge wr_clk or negedge rst_n) if(!rst_n) wfull <= 1'b0; else wfull <= wfull_val; endmodule