FIFO(First In First Out)是異步數據傳輸時經常使用的存儲器。該存儲器的特點是數據先進先出(后進后出)。其實,多位寬數據的異步傳輸問題,無論是從快時鐘到慢時鐘域,還是從慢時鐘到快時鐘域,都可以使用 FIFO 處理。
FIFO 原理
工作流程
復位之后,在寫時鐘和狀態信號的控制下,數據寫入 FIFO 中。RAM 的寫地址從 0 開始,每寫一次數據寫地址指針加一,指向下一個存儲單元。當 FIFO 寫滿后,數據將不能再寫入,否則數據會因覆蓋而丟失。
FIFO 數據為非空、或滿狀態時,在讀時鐘和狀態信號的控制下,可以將數據從 FIFO 中讀出。RAM 的讀地址從 0 開始,每讀一次數據讀地址指針加一,指向下一個存儲單元。當 FIFO 讀空后,就不能再讀數據,否則讀出的數據將是錯誤的。
FIFO 的存儲結構為雙口 RAM,所以允許讀寫同時進行。典型異步 FIFO 結構圖如下所示。端口及內部信號將在代碼編寫時進行說明。
讀寫時刻
關于寫時刻,只要 FIFO 中數據為非滿狀態,就可以進行寫操作;如果 FIFO 為滿狀態,則禁止再寫數據。關于讀時刻,只要 FIFO 中數據為非空狀態,就可以進行讀操作;如果 FIFO 為空狀態,則禁止再讀數據。不管怎樣,一段正常讀寫 FIFO 的時間段,如果讀寫同時進行,則要求寫 FIFO 速率不能大于讀速率。
讀空狀態
開始復位時,FIFO 沒有數據,空狀態信號是有效的。當 FIFO 中被寫入數據后,空狀態信號拉低無效。當讀數據地址追趕上寫地址,即讀寫地址都相等時,FIFO 為空狀態。
因為是異步 FIFO,所以讀寫地址進行比較時,需要同步打拍邏輯,就需要耗費一定的時間。所以空狀態的指示信號不是實時的,會有一定的延時。如果在這段延遲時間內又有新的數據寫入 FIFO,就會出現空狀態指示信號有效,但是 FIFO 中其實存在數據的現象。
嚴格來講該空狀態指示是錯誤的。但是產生空狀態的意義在于防止讀操作對空狀態的 FIFO 進行數據讀取。產生空狀態信號時,實際 FIFO 中有數據,相當于提前判斷了空狀態信號,此時不再進行讀 FIFO 數據操作也是安全的。所以,該設計從應用上來說是沒有問題的。
寫滿狀態
開始復位時,FIFO 沒有數據,滿信號是無效的。當 FIFO 中被寫入數據后,此時讀操作不進行或讀速率相對較慢,只要寫數據地址超過讀數據地址一個 FIFO 深度時,便會產生滿狀態信號。此時寫地址和讀地址也是相等的,但是意義是不一樣的。
此時經常使用多余的 1bit 分別當做讀寫地址的拓展位,來區分讀寫地址相同的時候,FIFO 的狀態是空還是滿狀態。當讀寫地址與拓展位均相同的時候,表明讀寫數據的數量是一致的,則此時 FIFO 是空狀態。如果讀寫地址相同,拓展位為相反數,表明寫數據的數量已經超過讀數據數量的一個 FIFO 深度了,此時 FIFO 是滿狀態。當然,此條件成立的前提是空狀態禁止讀操作、滿狀態禁止寫操作。
同理,由于異步延遲邏輯的存在,滿狀態信號也不是實時的。但是也相當于提前判斷了滿狀態信號,此時不再進行寫 FIFO 操作也不會影響應用的正確性。
FIFO 設計
設計要求
為設計應用于各種場景的 FIFO,這里對設計提出如下要求:
- (1) FIFO 深度、寬度參數化,輸出空、滿狀態信號,并輸出一個可配置的滿狀態信號。當 FIFO 內部數據達到設置的參數數量時,拉高該信號。
- (2) 輸入數據和輸出數據位寬可以不一致,但要保證寫數據、寫地址位寬與讀數據、讀地址位寬的一致性。例如寫數據位寬 8bit,寫地址位寬為 6bit(64 個數據)。如果輸出數據位寬要求 32bit,則輸出地址位寬應該為 4bit(16 個數據)。
- (3) FIFO 是異步的,即讀寫控制信號來自不同的時鐘域。輸出空、滿狀態信號之前,讀寫地址信號要用格雷碼做同步處理,通過減少多位寬信號的翻轉來減少打拍法同步時數據的傳輸錯誤。 格雷碼與二進制之間的轉換如下圖所示。
雙口 RAM 設計
RAM 端口參數可配置,讀寫位寬可以不一致。建議 memory 數組定義時,以長位寬地址、短位寬數據的參數為參考,方便數組變量進行選擇訪問。
Verilog 描述如下。
module ramdp#( parameter AWI = 5 ,parameter AWO = 7 ,parameter DWI = 64 ,parameter DWO = 16)(input CLK_WR , //寫時鐘input WR_EN , //寫使能input [AWI-1:0] ADDR_WR ,//寫地址input [DWI-1:0] D , //寫數據input CLK_RD , //讀時鐘input RD_EN , //讀使能input [AWO-1:0] ADDR_RD ,//讀地址output reg [DWO-1:0] Q //讀數據);//輸出位寬大于輸入位寬,求取擴大的倍數及對應的位數parameter EXTENT = DWO/DWI ;parameter EXTENT_BIT = AWI-AWO > 0 ? AWI-AWO : 'b1 ;//輸入位寬大于輸出位寬,求取縮小的倍數及對應的位數parameter SHRINK = DWI/DWO ;parameter SHRINK_BIT = AWO-AWI > 0 ? AWO-AWI : 'b1;genvar i ;generate//數據位寬展寬(地址位寬縮小)if (DWO >= DWI) begin//寫邏輯,每時鐘寫一次reg [DWI-1:0] mem [(1<<AWI)-1 : 0] ;always @(posedge CLK_WR) beginif (WR_EN) beginmem[ADDR_WR] <= D ;endend//讀邏輯,每時鐘讀 4 次for (i=0; i<EXTENT; i=i+1) beginalways @(posedge CLK_RD) beginif (RD_EN) beginQ[(i+1)*DWI-1: i*DWI] <= mem[(ADDR_RD*EXTENT) + i ] ;endendendend//=================================================//數據位寬縮小(地址位寬展寬)else begin//寫邏輯,每時鐘寫 4 次reg [DWO-1:0] mem [(1<<AWO)-1 : 0] ;for (i=0; i<SHRINK; i=i+1) beginalways @(posedge CLK_WR) beginif (WR_EN) beginmem[(ADDR_WR*SHRINK)+i] <= D[(i+1)*DWO -1: i*DWO] ;endendend//讀邏輯,每時鐘讀 1 次always @(posedge CLK_RD) beginif (RD_EN) beginQ <= mem[ADDR_RD] ;endendendendgenerateendmodule
計數器設計
計數器用于產生讀寫地址信息,位寬可配置,不需要設置結束值,讓其溢出后自動重新計數即可。Verilg 描述如下。
module ccnt#(parameter W )(input rstn ,input clk ,input en ,output [W-1:0] count);reg [W-1:0] count_r ;always @(posedge clk or negedge rstn) beginif (!rstn) begincount_r <= 'b0 ;endelse if (en) begincount_r <= count_r + 1'b1 ;endendassign count = count_r ;endmodule
FIFO 設計
該模塊為 FIFO 的主體部分,產生讀寫控制邏輯,并產生空、滿、可編程滿狀態信號。
鑒于篇幅原因,這里只給出讀數據位寬大于寫數據位寬的邏輯代碼。
module fifo#( parameter AWI = 5 ,parameter AWO = 3 ,parameter DWI = 4 ,parameter DWO = 16 ,parameter PROG_DEPTH = 16) //可設置深度(input rstn, //讀寫使用一個復位input wclk, //寫時鐘input winc, //寫使能input [DWI-1: 0] wdata, //寫數據input rclk, //讀時鐘input rinc, //讀使能output [DWO-1 : 0] rdata, //讀數據output wfull, //寫滿標志output rempty, //讀空標志output prog_full //可編程滿標志);//輸出位寬大于輸入位寬,求取擴大的倍數及對應的位數parameter EXTENT = DWO/DWI ;parameter EXTENT_BIT = AWI-AWO ;//輸出位寬小于輸入位寬,求取縮小的倍數及對應的位數parameter SHRINK = DWI/DWO ;parameter SHRINK_BIT = AWO-AWI ;//==================== push/wr counter ===============wire [AWI-1:0] waddr ;wire wover_flag ; //多使用一位做寫地址拓展ccnt #(.W(AWI+1)) u_push_cnt(.rstn (rstn),.clk (wclk),.en (winc && !wfull), //full 時禁止寫.count ({wover_flag, waddr}));//============== pop/rd counter ===================wire [AWO-1:0] raddr ;wire rover_flag ; //多使用一位做讀地址拓展ccnt #(.W(AWO+1)) u_pop_cnt(.rstn (rstn),.clk (rclk),.en (rinc & !rempty), //empyt 時禁止讀.count ({rover_flag, raddr}));//==============================================//窄數據進,寬數據出
generateif (DWO >= DWI) begin : EXTENT_WIDTH//格雷碼轉換wire [AWI:0] wptr = ({wover_flag, waddr}>>1) ^ ({wover_flag, waddr}) ;//將寫數據指針同步到讀時鐘域reg [AWI:0] rq2_wptr_r0 ;reg [AWI:0] rq2_wptr_r1 ;always @(posedge rclk or negedge rstn) beginif (!rstn) beginrq2_wptr_r0 <= 'b0 ;rq2_wptr_r1 <= 'b0 ;endelse beginrq2_wptr_r0 <= wptr ;rq2_wptr_r1 <= rq2_wptr_r0 ;endend//格雷碼轉換wire [AWI-1:0] raddr_ex = raddr << EXTENT_BIT ;wire [AWI:0] rptr = ({rover_flag, raddr_ex}>>1) ^ ({rover_flag, raddr_ex}) ;//將讀數據指針同步到寫時鐘域reg [AWI:0] wq2_rptr_r0 ;reg [AWI:0] wq2_rptr_r1 ;always @(posedge wclk or negedge rstn) beginif (!rstn) beginwq2_rptr_r0 <= 'b0 ;wq2_rptr_r1 <= 'b0 ;endelse beginwq2_rptr_r0 <= rptr ;wq2_rptr_r1 <= wq2_rptr_r0 ;endend//格雷碼反解碼//如果只需要空、滿狀態信號,則不需要反解碼//因為可編程滿狀態信號的存在,地址反解碼后便于比較reg [AWI:0] wq2_rptr_decode ;reg [AWI:0] rq2_wptr_decode ;integer i ;always @(*) beginwq2_rptr_decode[AWI] = wq2_rptr_r1[AWI];for (i=AWI-1; i>=0; i=i-1) beginwq2_rptr_decode[i] = wq2_rptr_decode[i+1] ^ wq2_rptr_r1[i] ;endendalways @(*) beginrq2_wptr_decode[AWI] = rq2_wptr_r1[AWI];for (i=AWI-1; i>=0; i=i-1) beginrq2_wptr_decode[i] = rq2_wptr_decode[i+1] ^ rq2_wptr_r1[i] ;endend//讀寫地址、拓展位完全相同是,為空狀態assign rempty = (rover_flag == rq2_wptr_decode[AWI]) &&(raddr_ex >= rq2_wptr_decode[AWI-1:0]);//讀寫地址相同、拓展位不同,為滿狀態assign wfull = (wover_flag != wq2_rptr_decode[AWI]) &&(waddr >= wq2_rptr_decode[AWI-1:0]) ;//拓展位一樣時,寫地址必然不小于讀地址//拓展位不同時,寫地址部分比如小于讀地址,實際寫地址要增加一個FIFO深度assign prog_full = (wover_flag == wq2_rptr_decode[AWI]) ?waddr - wq2_rptr_decode[AWI-1:0] >= PROG_DEPTH-1 :waddr + (1<<AWI) - wq2_rptr_decode[AWI-1:0] >= PROG_DEPTH-1;//雙口 ram 例化ramdp#( .AWI (AWI),.AWO (AWO),.DWI (DWI),.DWO (DWO))u_ramdp(.CLK_WR (wclk),.WR_EN (winc & !wfull), //寫滿時禁止寫.ADDR_WR (waddr),.D (wdata[DWI-1:0]),.CLK_RD (rclk),.RD_EN (rinc & !rempty), //讀空時禁止讀.ADDR_RD (raddr),.Q (rdata[DWO-1:0]));end//==============================================//big in and small out/*else begin: SHRINK_WIDTH……end*/endgenerate
endmodule
FIFO 調用
下面可以調用設計的 FIFO,完成多位寬數據傳輸的異步處理。
寫數據位寬為 4bit,寫深度為 32。
讀數據位寬為 16bit,讀深度為 8,可配置 full 深度為 16。
module fifo_s2b(input rstn,input [4-1: 0] din, //異步寫數據input din_clk, //異步寫時鐘input din_en, //異步寫使能output [16-1 : 0] dout, //同步后數據input dout_clk, //同步使用時鐘input dout_en ); //同步數據使能wire fifo_empty, fifo_full, prog_full ;wire rd_en_wir ;wire [15:0] dout_wir ;//讀空狀態時禁止讀,否則一直讀assign rd_en_wir = fifo_empty ? 1'b0 : 1'b1 ;fifo #(.AWI(5), .AWO(3), .DWI(4), .DWO(16), .PROG_DEPTH(16))u_buf_s2b(.rstn (rstn),.wclk (din_clk),.winc (din_en),.wdata (din),.rclk (dout_clk),.rinc (rd_en_wir),.rdata (dout_wir),.wfull (fifo_full),.rempty (fifo_empty),.prog_full (prog_full));//緩存同步后的數據和使能reg dout_en_r ;always @(posedge dout_clk or negedge rstn) beginif (!rstn) begindout_en_r <= 1'b0 ;endelse begindout_en_r <= rd_en_wir ;endendassign dout = dout_wir ;assign dout_en = dout_en_r ;endmodule
testbench
`timescale 1ns/1ns
`define SMALL2BIG
module test ;`ifdef SMALL2BIGreg rstn ;reg clk_slow, clk_fast ;reg [3:0] din ;reg din_en ;wire [15:0] dout ;wire dout_en ;//resetinitial beginclk_slow = 0 ;clk_fast = 0 ;rstn = 0 ;#50 rstn = 1 ;end//讀時鐘 clock_slow 較快于寫時鐘 clk_fast 的 1/4//保證讀數據稍快于寫數據parameter CYCLE_WR = 40 ;always #(CYCLE_WR/2/4) clk_fast = ~clk_fast ;always #(CYCLE_WR/2-1) clk_slow = ~clk_slow ;//data generateinitial begindin = 16'h4321 ;din_en = 0 ;wait (rstn) ;//(1) 測試 full、prog_full、empyt 信號force test.u_data_buf2.u_buf_s2b.rinc = 1'b0 ;repeat(32) begin@(negedge clk_fast) ;din_en = 1'b1 ;din = {$random()} % 16;end@(negedge clk_fast) din_en = 1'b0 ;//(2) 測試數據讀寫#500 ;rstn = 0 ;#10 rstn = 1 ;release test.u_data_buf2.u_buf_s2b.rinc;repeat(100) begin@(negedge clk_fast) ;din_en = 1'b1 ;din = {$random()} % 16;end//(3) 停止讀取再一次測試 empyt、full、prog_full 信號force test.u_data_buf2.u_buf_s2b.rinc = 1'b0 ;repeat(18) begin@(negedge clk_fast) ;din_en = 1'b1 ;din = {$random()} % 16;endendfifo_s2b u_data_buf2(.rstn (rstn),.din (din),.din_clk (clk_fast),.din_en (din_en),.dout (dout),.dout_clk (clk_slow),.dout_en (dout_en));`else
`endif//stop siminitial beginforever begin#100;if ($time >= 5000) $finish ;endendendmodule