【FPGA——基礎篇】同步FIFO與非同步FIFO——Verilog實現
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