同步 FIFO 的實現
學習目的:
- 1.給大家介紹FIFO的基本原理和組成。
- 2.學習同步單時鐘FIFO的分析以及編寫。
- 3.以實例體驗如何將實際需求分析轉換成代碼。
FIFO(First In First Out)隊列通常在數字系統中實現兩個模塊間數據的暫存,達到減少兩模塊之間的速度依賴,使得兩個模塊能夠相互相對獨立的運行。
FIFO是FPGA設計的一個非常基本的單元。FIFO一般用一個雙口RAM和分別指示讀寫地址的指針,以及狀態生成和指示的邏輯組成,下圖是典型的FIFO結構圖:
我們看到中上部是一個雙口RAM,所謂雙口RAM是分別有兩套讀寫數據線地址線以及控制線,用以實現讀寫的同時操作。
再看到讀寫指針和讀寫控制界面,左邊是寫控制界面和寫指針,實現對寫指針的更新。右邊是讀控制界面和讀指針,實現對讀指針的更新。我們還有比較邏輯,這裏產生空empty信號和full信號。
我們來看一下存儲器的工作方式,實際是一個循環隊列:我們來跟蹤一下情景:
以下存儲器有8個存儲單元,在初始化的時候,讀指針RP和WP都指向0地址,當向存儲器裏寫入了8個數據後,寫指針會從新回到0,這就是循環隊列。
這裏地址是0-7是3個BIT寬度,WP和RP也要對應的是3個bit,加1就能實現從0,1,2.。6.7.0.1.2…這樣循環。下圖中陰影的部分標示存儲區已經佔用。
以上分析了WP和RP的行爲,還需要產生空和滿信號的邏輯,實現方法我們這裏設置一個計數器 FIFO_CNTR,用以表示有效可讀數據的個數,初始化的時候是0,標示存儲器內沒有數據可供讀。
當寫入接口進行一次有效的寫操作(這裏規定有效的寫操作是FIFO不滿的時候實施的寫操作)計數器FIFO_CNTR加1,
當讀出接口進行一次有效的讀操作(這裏規定有效的讀操作是FIFO不空的時候實施的讀操作)計數器FIFO_CNTR減1;
而當同時實施有效的寫操作和讀操作時,FIFO_CNTER不發生變化(從圖上可以分析出實際讀指針和寫指針都往前走了,他們間距還是一樣的,有效可讀數據的存儲個數還是保持不變)。
另外關於FIFO_CONTER的個數,我們這裏看到範圍是是0-8,(0時爲空,8時爲滿),因此需要4個bit來定義FIFO_CNTR.因此通常情況下FIFO_CNTR要比地址位數多1位。
至於說到full和empty很簡單的由FIFO_COUNTER產生:當且僅當FIFO_COUNTER=0時候,empty=1;當且僅當FIFO_CNTER爲最多可容納存儲個數時,這裏說FIFO_CNTR = 8時候,full=1;
我們再總結以上的分析,具體細化一下:
1,需要一個buff雙口存儲器做存儲載體,當地址有n位,就有個存儲單元,標號從0到()。三位地址存儲地址就是從0到7。
2,需要wr_ptr指針,與地址位數相同,指向下個可以寫的地址。
3,需要rd_ptr指針,與地址位數相同,指向下個可以讀的地址。
4,需要fifo_cntr計數器,比地址位數多一位,指示當前可讀的數據個數。
5,full和empty由fifo_cntr生成。
6,區別讀寫操作是否有效。
7,實現上述功能。
代碼呼之欲出了,我們來寫代碼,首先是定義端口,之後就開始寫代碼。寫完之後進行檢查。
module sc_fifo(
input clk,rst,
input [7:0] din,
input wr,
output full,
output reg [7:0] dout,
input rd,
output empty
);
//1,需要一個buff雙口存儲器做存儲載體,當地址有n位,就有2^N個存儲單元,標號從0到(2^N-1)。三位地址存儲地址就是從0到7。
reg [7:0] buff[0:7] ;
//2,需要wr_ptr指針,與地址位數相同,指向下個可以寫的地址。
reg [2:0] wr_ptr ;
//3,需要rd_ptr指針,與地址位數相同,指向下個可以讀的地址。
reg [2:0] rd_ptr ;
//4,需要fifo_cntr計數器,比地址位數多一位,指示當前可讀的數據個數。
reg [3:0] fifo_cntr ;
//5,full和empty由fifo_cntr生成。
assign full = fifo_cntr ==8 ;
assign empty= fifo_cntr==0;
//6,區別讀寫操作是否有效。
wire valid_rd = ~empty & rd ;
wire valid_wr = ~full & wr ;
//7,實現上述功能。
always@(posedge clk)
if (rst)
wr_ptr <= 0;
else if(valid_wr)
wr_ptr<=wr_ptr+1;
always@(posedge clk)
if (rst)
rd_ptr <= 0 ;
else if (valid_rd)
rd_ptr <= rd_ptr+1;
/*
always@(posedge clk) if (rst)fifo_cntr<=0;else
if ((valid_rd==0) &&(valid_wr==1))fifo_cntr<=fifo_cntr+1;
else if ((valid_rd==1)&&(valid_wr==0))fifo_cntr<fifo_cntr-1;
*/
always@(posedge clk)
casex ({rst,valid_wr,valid_rd})
3'b1xx : fifo_cntr<=0;
3'b010 : fifo_cntr<=fifo_cntr+1;
3'b001 : fifo_cntr<=fifo_cntr-1;
3'b011 ,3'b000 :fifo_cntr<=fifo_cntr ;
endcase
always@(posedge clk)
if (valid_wr)
buff[wr_ptr] <=din ;
always@(posedge clk)
if (valid_rd)
dout<= buff[rd_ptr] ;
endmodule
異步 FIFO 的實現
其實FIFO是也是在RAM的基礎上增加了許多功能,FIFO的典型結構如下,主要分爲讀和寫兩部分,另外就是狀態信號,空和滿信號,同時還有數據的數量狀態信號,與RAM最大的不同是FIFO沒有地址線,不能進行隨機地址讀取數據,什麼是隨機讀取數據呢,也就是可以任意讀取某個地址的數據。而FIFO則不同,不能進行隨機讀取,這樣的好處是不用頻繁地控制地址線。
最終將在 AX-7020 FPGA 上實現。
雖然用戶看不到地址線,但是在FFO內部還是有地址的操作的,用來控制RAM的讀寫接口。其地址在讀寫操作時如下圖所示,其中深度值也就是一個FIFO裏最大可以存放多少個數 據。初始狀態下,讀寫地址都爲0,在向FIFO中寫入一個數據後,寫地址加1,從FFO中讀出 一個數據後,讀地址加1。此時FFO的狀態即爲空,因爲寫了一個數據,又讀出了一個數據。
可以把 FIFO 想象成一個水池,寫通道即爲加水,讀通道即爲放水,假如不間斷的加水和放水,如果加水速度比放水速度快,那麼FIFO就會有滿的時候,如果滿了還繼續加水就會溢出overflow,如果放水速度比加水速度快,那麼FIFO就會有空的時候,所以把握好加水與放水的時機和速度,保證水池一直有水是一項很艱鉅的任務。也就是判斷空與滿的狀態,擇機寫數據或讀數據。
下面主要介紹異步 FFO 的控制,其中讀時鐘爲 25 MHz,寫時鐘爲 50 MHz。實驗中會通過 vivado 集成的在想邏輯分析儀 ila ,我們可以觀察FIFO的讀寫時序。
FIFO IP 核的端口列表如下:
FIFO 的數據寫入和讀出都是按時鐘的上升沿操作的,當 wr_en 信號爲高時寫入 FIFO 數據,當 almost_full 信號有效時,表示 FIFO 只能再寫入一個數據,一旦寫入一個數據了,full 信號就會拉高,如果在 full 的情況下 wr_en 仍然有效,也就是繼續向 FIFO 寫數據,則 FIFO 的 overflow 就會有效,表示溢出。(如下圖所示)
當 rd_en 信號爲高時讀FIFO數據,數據在下個週期有效。valid爲數據有效信號,almost_empty表示還有一個數據讀,當再讀一個數據,empty信號有效,如果繼續讀,則underflow有效,表示下溢,此時讀出的數據無效。(如下圖所示)
而從 FWFT 模式讀數據時序圖可以看出, rden 信號有效時,有效數據D已經在數據線上準備好有效了,不會再延後一個週期。這就是與標準FIFO的不同之處。(如下圖所示)
有一點需要注意的是,FFO設置默認爲採用 safety circuit,此功能是保證到達內部RAM的輸入信號是同步的,在這種情況下,如果異步復位後,則需要等待60個最慢時鐘週期。(如下圖所示,更詳細的還請參考文檔)
在有了上面的基礎知識後,我們開始對異步 FIFO 進行代碼的編寫,要實現以下幾個功能:
-
- 對輸入時鐘進行分頻(二分頻)PS:分頻操作也可以調用鎖相環 IP 核
-
- 調用 Xilinx 的FIFO IP核
-
- 需要編寫對FIFO 的控制邏輯(狀態機)
-
- 調用 XIlinx 的 ILA IP 覈對讀到的輸出進行捕捉
代碼如下:
`timescale 1ns / 1ps
module fifo(
input clk,
input rst_n
);
wire div2;
wire wr_clk;
wire rd_clk;
wire [15:0] dout;
assign rd_clk = div2;
assign wr_clk = clk;
reg [6:0] wcn;
reg [6:0] rcn;
reg [15:0] din_reg;
wire wr_en;
wire rd_en;
wire full;
wire empty;
wire [9:0] rd_data_count;
wire [9:0] wr_data_count;
localparam IDLE = 0;
localparam WRITE = 1;
localparam READ = 2;
// write FSM
reg [1:0] write_state;
assign wr_en = (write_state == WRITE) ? ~full : 1'b0;
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
write_state <= IDLE;
din_reg <= 16'd0;
end
else
begin
case(write_state)
IDLE:
begin
if(wcn >= 7'd60)
write_state <= WRITE;
else
write_state <= IDLE;
end
WRITE:
begin
if (wr_en)
begin
write_state <= WRITE;
din_reg <= din_reg + 16'd1;
end
else
write_state <= write_state;
end
default:
write_state <= write_state;
endcase
end
end
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wcn <= 7'd0;
else if(write_state == IDLE)
wcn <= wcn + 7'd1;
else
wcn <= 7'd0;
end
// read fsm
reg [1:0] read_state;
assign rd_en = (read_state == READ) ? ~empty : 1'b0;
always @ (posedge rd_clk or negedge rst_n) begin
if (!rst_n)
begin
read_state <= IDLE;
end
else
begin
case(read_state)
IDLE:
begin
if (rcn >= 7'd60)
begin
read_state <= READ;
end
else
read_state <= IDLE;
end
READ:
begin
read_state <= READ;
end
default:
read_state <= read_state;
endcase
end
end
always @ (posedge rd_clk or negedge rst_n) begin
if (!rst_n)
rcn <= 7'd0;
else if(read_state == IDLE)
rcn <= rcn + 7'd1;
else
rcn <= 7'd0;
end
div2 diver2(
.clk(clk),
.rst_n(rst_n),
.div2_o(div2)
);
fifo_ip fifo_wr (
.rst(~rst_n), // input wire rst
.wr_clk(wr_clk), // input wire wr_clk
.rd_clk(rd_clk), // input wire rd_clk
.din(din_reg), // input wire [15 : 0] din
.wr_en(wr_en), // input wire wr_en
.rd_en(rd_en), // input wire rd_en
.dout(dout), // output wire [15 : 0] dout
.full(full), // output wire full
.empty(empty), // output wire empty
.rd_data_count(rd_data_count), // output wire [9 : 0] rd_data_count
.wr_data_count(wr_data_count), // output wire [9 : 0] wr_data_count
.wr_rst_busy(), // output wire wr_rst_busy
.rd_rst_busy() // output wire rd_rst_busy
);
ila_0 ila_ip (
.clk(rd_clk), // input wire clk
.probe0(dout) // input wire [15:0] probe0
);
endmodule
// 分頻操作
module div2(
input clk ,
input rst_n ,
output div2_o
);
reg div2_o_r ;
assign div2_o = div2_o_r;
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
div2_o_r <= 0;
else
div2_o_r <= ~div2_o_r;
end
endmodule
仿真文件如下:
module fifo_tb;
reg clk;
reg rst_n;
always
begin
clk = 0;
#10;
clk = 1;
#10;
end
initial
begin
rst_n = 0;
#20;
rst_n = 1;
end
fifo fifo_tb(
.clk(clk),
.rst_n(rst_n)
);
endmodule
最終經過綜合、比特文件生成,在板子上跑的結果如下:
另附上約束文件:
set_property PACKAGE_PIN N15 [get_ports rst_n]
set_property PACKAGE_PIN U18 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
create_clock -period 20.000 -name clk -waveform {0.000 10.000} [get_ports clk]
set_property C_CLK_INPUT_FREQ_HZ 300000000 [get_debug_cores dbg_hub]
set_property C_ENABLE_CLK_DIVIDER false [get_debug_cores dbg_hub]
set_property C_USER_SCAN_CHAIN 1 [get_debug_cores dbg_hub]
connect_debug_port dbg_hub/clk [get_nets div2]