1. FIFO 簡介
FIFO (先入先出, First In First Out )存儲器,在 FPGA 和數字 IC 設計中非常常用。 根據接入的時鐘信號,可以分為同步 FIFO 和異步 FIFO 。
**FIFO 底層基于雙口 RAM ** ,同步 FIFO 的讀寫時鐘一致,異步 FIFO 讀時鐘和寫時鐘不同。 同步時鐘主要應用于速率匹配(數據緩沖),類似于乒乓存儲提高性能的思想,可以讓后級不必等待前級過多時間; 異步 FIFO 主要用于多 bit 信號的跨時鐘域處理。
本文討論同步 FIFO 的結構及控制邏輯設計,并給出代碼。
2. 同步 FIFO 接口
對于同步 FIFO ,包含必要的接口如下圖所示:
(1) clk : 時鐘信號,讀寫共用;
(2) rst_n : 復位信號,視具體設計和芯片采用同步復位還是異步復位,此處默認使用異步低電平復位;
(3) wdata : 寫數據信號,信號后帶“ \\ ”表示是多 bit 信號;
(4) rdata : 讀數據信號,信號后帶“ \\ ”表示是多 bit 信號;
(5) wfull : 滿信號,指示 FIFO 寫滿了,不能再寫了,如果再寫會覆蓋掉還沒讀出的寫入數據,造成數據丟失;
(6) rempty : 空信號,指示 FIFO 讀空了,不能在讀了,如果再讀相當于有的數據重復讀了第二遍,造成數據錯誤;
(7) winc : 寫使能信號,寫使能有效時表示希望能寫入數據;
(8) rinc : 讀使能信號,讀使能有效時表示希望能讀出數據;
3. 雙口 RAM 接口
在實現 FIFO 時,無論是同步 FIFO 還是異步 FIFO ,通常會通過雙口 RAM ( Dual Port RAM )并添加一些必要的邏輯來實現。雙口 RAM 的接口如下圖所示。
**左側全部是寫時鐘域的,包括寫時鐘、寫數據、寫地址和寫使能信號;
**
右側全部是讀時鐘域的,包括讀時鐘、讀數據、讀地址和讀使能信號;
4. 基于雙口 RAM 的同步 FIFO 結構
根據同步 FIFO 的接口和雙口 RAM 的接口,在借助雙口 RAM 實現同步 FIFO 時,如下圖所示結構,只需要加入讀、寫控制邏輯即可。在寫邏輯中,用于產生寫地址和寫滿信號; 在讀邏輯中,用于產生讀地址和讀空信號。 讀寫控制邏輯還需要受到讀寫使能信號的控制。
5. 讀寫地址產生邏輯
讀寫地址什么時候能夠遞增?
顯然,對于寫地址必須滿足:
(1) 寫使能有效(要寫入);
(2) 沒寫滿(能寫入);
即:
always @ (posedge clk or negedge rst_n)
begin
if(~rst_n) begin
waddr <= 'b0;
end
else begin
if( winc && ~wfull ) begin
waddr <= waddr + 1'b1;
end
else begin
waddr <= waddr;
end
end
end
對于讀地址必須滿足:
(1) 讀使能有效(要讀出);
(2) 沒讀空(能讀出);
即:
always @ (posedge clk or negedge rst_n)
begin
if(~rst_n) begin
raddr <= 'b0;
end
else begin
if( rinc && ~rempty ) begin
raddr <= raddr + 1'b1;
end
else begin
raddr <= raddr;
end
end
end
6. 空滿信號產生邏輯
搞定了讀寫地址的控制邏輯,還差最后一步也是最關鍵的信號:空滿信號如何產生。
空: 讀空,讀地址追上寫地址;
滿: 寫滿,寫地址追上讀地址。
問題來了: 怎么判地址斷追上了呢? 如果地址相等那應該是追上了,即 raadr == waddr 或者 wddr == raddr 。 如果按照這種判斷,顯然這兩個地址追上對方的判斷是等效的,無法區分出來到底是寫追上讀還是讀追上寫。
可以考慮: 使用 1 個標志位 flag 來額外指示寫追上讀還是讀追上寫。
參考前人的文獻,判斷空滿的方式有多種,非常常用的一種是 Clifford E. Cummings 文章中提到的 擴展 1 bit 的讀寫地址方法 ,也就是說,將前面提到的 flag 指示信號和原本 N 位的讀寫地址結合,使用 N+1 位的讀寫地址,其中最高位用于判斷空滿信號,其余低位還是正常用于讀寫地址索引。
以一個 4 深度的 FIFO 實例來說明, 4 深度原本需要 2 bit 的讀寫地址,現在擴展成 3 bit 。
使用低 2 位來進行雙口 RAM 的地址索引,高位用于判斷空滿。 對于空信號,可以知道當 FIFO 里沒有待讀出的數據時產生。** 也就是說,此時讀追上了寫,把之前寫的數據剛剛全部都出,讀地址和寫地址此時指向相同的位置,讀地址 - 寫地址 =0** ,即
raddr == waddr
對于寫滿信號, **當寫入后還沒被讀出的數據恰好是 FIFO 深度的時候,產生滿信號,即寫地址 - 讀地址 = FIFO 深度 = 4 ** 。 對照下圖可以發現,此時對于雙口 RAM 的 2 bit 的地址來說,讀寫地址一致; 對于最高位來所,寫是 1 而讀是 0 。
再考慮下圖所示的一種情況,寫入待讀出的數據仍然是 4 個,此時也是 4 深度的 FIFO 已經滿了。 讀寫地址的低位相同,高位是寫 0 讀 1 。
對于寫滿的 2 種情況,總結下來,都是低位相同,最高位相反。
即:
raddr[N] = = ~waddr[N]
raddr[N-1:0] = = waddr[N-1:0]
也就是:
raddr == {~waddr[N], waddr[N-1:0]}
所以,空滿邏輯產生的代碼為:
always @ (posedge clk or negedge rst_n)
begin
if(~rst_n) begin
wfull <= 'b0;
rempty <= 'b0;
end
else begin
wfull <= (raddr == {~waddr[ADDR_WIDTH], waddr[ADDR_WIDTH-1:0]});
rempty <= (raddr == waddr);
end
end
7. 全部代碼
`timescale 1ns/1ns
/****************************/
// 作者:FPGA探索者
/****************************/
module sfifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk ,
input rst_n ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output reg wfull ,
output reg rempty ,
output wire [WIDTH-1:0] rdata
);
// 用localparam定義一個參數,可以在文件內使用
localparam ADDR_WIDTH = $clog2(DEPTH);
reg [ADDR_WIDTH:0] waddr;
reg [ADDR_WIDTH:0] raddr;
always @ (posedge clk or negedge rst_n) begin
if(~rst_n) begin
waddr <= 'b0;
end
else begin
if( winc && ~wfull ) begin
waddr <= waddr + 1'b1;
end
else begin
waddr <= waddr;
end
end
end
always @ (posedge clk or negedge rst_n) begin
if(~rst_n) begin
raddr <= 'b0;
end
else begin
if( rinc && ~rempty ) begin
raddr <= raddr + 1'b1;
end
else begin
raddr <= raddr;
end
end
end
always @ (posedge clk or negedge rst_n) begin
if(~rst_n) begin
wfull <= 'b0;
rempty <= 'b0;
end
else begin
wfull <= (raddr == {~waddr[ADDR_WIDTH], waddr[ADDR_WIDTH-1:0]});
rempty <= (raddr == waddr);
end
end
// 帶有 parameter 參數的例化格式
dual_port_RAM
#(
.DEPTH(DEPTH),
.WIDTH(WIDTH)
)
dual_port_RAM_U0
(
.wclk(clk),
.wenc(winc),
.waddr(waddr[ADDR_WIDTH-1:0]),
.wdata(wdata),
.rclk(clk),
.renc(rinc),
.raddr(raddr[ADDR_WIDTH-1:0]),
.rdata(rdata)
);
endmodule
/**************RAM 子模塊*************/
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度對2取對數,得到地址的位寬。
,input [WIDTH-1:0] wdata //數據寫入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度對2取對數,得到地址的位寬。
,output reg [WIDTH-1:0] rdata //數據輸出
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule