寫在前面
一開始是想既然是極簡教程,就應(yīng)該只給出FIFO的概念巍虫,沒想到還是給出了同步以及異步FIFO的設(shè)計,要不然總感覺內(nèi)容不完整毁葱,也好垫言,自己設(shè)計的FIFO模塊不用去擔(dān)心因IP核跨平臺不通用的缺陷!那我們開始吧倾剿。
- 個人微信公眾號: FPGA LAB
- 個人博客首頁
- 注:學(xué)習(xí)交流使用筷频!
正文
同步FIFO回顧
上一篇博客講了同步FIFO的概念以及同步FIFO的設(shè)計問題,并給出了同步FIFO的Verilog代碼以及VHDL代碼前痘,并經(jīng)過了行為仿真測試凛捏,鏈接如下:
FPGA基礎(chǔ)知識極簡教程(3)從FIFO設(shè)計講起之同步FIFO篇
$clog2()系統(tǒng)函數(shù)使用
這里簡單提一下,同步FIFO的代碼中用到了一個系統(tǒng)函數(shù)$clog2()芹缔,這個系統(tǒng)函數(shù)的使用方法很簡單:
parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 8;
reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;
例如我定義了FIFO緩沖區(qū)的深度為DATA_DEPTH = 8坯癣,那么其地址(指針)位寬是多少呢?
這時候就可以使用系統(tǒng)函數(shù)$clog2()了最欠,位寬可以表示為:
$clog2(DATA_DEPTH) // = 3;
指針就可以定義為:
reg [$clog2(DATA_DEPTH) - 1 : 0] wr_pointer = 0;
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_pointer = 0;
綜合屬性控制資源使用
還有一點需要提的是示罗,我們都知道在FPGA中FIFO的實現(xiàn)可以使用分布式資源或者BLOCK RAM惩猫,那么如何掌控呢?
當(dāng)使用FIFO緩沖空間較小時蚜点,我們選擇使用Distributed RAM轧房;當(dāng)使用FIFO緩沖空間較大時,我們選擇使用BLOCK RAM資源绍绘;這是一般的選擇原則奶镶。
我們可以通過在設(shè)計代碼中加入約束條件來控制,之前有寫過
Vivado 隨筆(1) 綜合屬性之 ram_style & rom_style?
就上述同步FIFO而言陪拘,我們可以在緩沖區(qū)定義時候添加如下約束:
(*ram_style = "distributed"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
或者:
(*ram_style = "block"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
為了驗證是否有用厂镇,我們在Vivado中進(jìn)行驗證如下:
當(dāng)設(shè)計中使用BLOCK RAM約束:
(*ram_style = "block"*)reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
綜合后的電路圖如下,可見FIFO緩存區(qū)使用的資源為BLOCK RAM左刽;
同時給出資源利用率報告:
可見存在BLOCK RAM 捺信,由于我僅僅綜合了一個同步FIFO,因此這個Block RAM一定是FIFO緩沖區(qū)消耗的欠痴。
當(dāng)使用Distributed RAM約束時:
(*ram_style = "distributed"*)reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
綜合后電路圖FIFO緩沖區(qū)部分:
資源利用率情況残黑;
可見 ,并未使用BLOCK RAM斋否,而是使用了LUT RAM梨水,也即分布式RAM。
綜上茵臭,驗證了這條約束的有效性疫诽。
異步FIFO設(shè)計
FIFO用途回顧
再設(shè)計異步FIFO電路之前,有必要說明一下FIFO的用途旦委,上篇博文提到:
- 跨時鐘域
FPGA或者ASIC設(shè)計內(nèi)部電路多位數(shù)據(jù)在不同的時鐘域交互奇徒,為了數(shù)據(jù)安全、正確缨硝、穩(wěn)定交互摩钙,我們需要設(shè)計異步FIFO進(jìn)行跨時鐘域交互。正如之前博客所寫:漫談時序設(shè)計(1)跨時鐘域是設(shè)計出來的查辩,而非約束出來的胖笛!
我們在時序分析時候,通常都將跨時鐘域路徑進(jìn)行偽路徑約束宜岛,因此我們必須在設(shè)計時候解決跨時鐘域數(shù)據(jù)傳輸問題长踊,異步FIFO在此起到關(guān)鍵作用。
- 在將數(shù)據(jù)發(fā)送到芯片外之前將其緩沖(例如萍倡,發(fā)送到DRAM或SRAM)
- 緩沖數(shù)據(jù)以供軟件在以后查看
- 存儲數(shù)據(jù)以備后用
這三條大概講的都是一個意思身弊,總結(jié)起來就是FIFO可以起到數(shù)據(jù)緩沖或緩存的作用,例如突然數(shù)據(jù),我們就需要先將其緩存起來阱佛,之后再從FIFO中讀出出來進(jìn)行處理帖汞,這樣也可以保證數(shù)據(jù)不會丟失。
引用互聯(lián)網(wǎng)上其他說法就是:數(shù)據(jù)寫入過快凑术,并且間隔時間長涨冀,也就是突發(fā)寫入。那么通過設(shè)置一定深度的FIFO麦萤,可以起到數(shù)據(jù)暫存的功能,且使得后續(xù)處理流程平滑扁眯。
異步FIFO原理回顧
無論是同步FIFO還是異步FIFO壮莹,其大致原理都是一致的,先入先出自然不必多說姻檀,關(guān)于空滿的判斷都是通過讀寫指針之間的關(guān)系來判斷命满;還有就是異步FIFO的指針需要進(jìn)行一定的處理,例如格雷碼處理绣版,這樣可以減小讀指針同步到寫指針時鐘域胶台,或者寫指針同步到讀指針時鐘域時出現(xiàn)亞穩(wěn)態(tài)的概率,這是因為格雷碼每次只有一位變化杂抽,這樣一位數(shù)據(jù)在進(jìn)行跨時鐘域傳輸?shù)臅r候亞穩(wěn)態(tài)出現(xiàn)的概率會大大減小诈唬。同步之后便進(jìn)行對比,以此來判斷FIFO的空滿缩麸。
那異步FIFO如何判斷空滿呢铸磅?
回答這個問題之前,我想先統(tǒng)一的說明FIFO(同步或者異步)是如何判斷空滿的杭朱?
起始阅仔,讀寫指針都是0,F(xiàn)IFO一定為空弧械;之后對FIFO進(jìn)行一系列的讀寫操作八酒,導(dǎo)致讀寫指針關(guān)系發(fā)生了變化,可以分為下面兩種情況:
- 讀比寫要快刃唐,或者說讀指針追寫指針羞迷,如果追上了,也即二者再次相等画饥,則FIFO讀空闭树;
- 寫比讀快,或者說寫指針彎道超越追讀指針荒澡,當(dāng)寫指針再次繞到讀指針背后并與讀指針重合报辱,也即二者相等時,F(xiàn)IFO寫滿!
在同步FIFO中碍现,我們使用計數(shù)的方式進(jìn)行判斷空滿幅疼,運用的也是這個原理,寫一個數(shù)據(jù)時昼接,計數(shù)值加1爽篷,讀出一個數(shù)據(jù)時,計數(shù)值減1慢睡,如下圖:
我最喜歡用這幅圖來分析FIFO逐工,下面一行一行的分析:
- 第一行:寫入1個數(shù)據(jù),計數(shù)值為1漂辐;
- 第二行:寫入5個數(shù)據(jù)泪喊,計數(shù)值為6;
- 第三行:讀出3個數(shù)據(jù)髓涯,計數(shù)值為3袒啼;
- 第四行:寫入3個數(shù)據(jù),計數(shù)值為6纬纪;
- 第五行:寫入2個數(shù)據(jù)蚓再,計數(shù)值為8,等于FIFO深度包各,則表示寫滿摘仅;
- 第六行:讀出6個數(shù)據(jù),計數(shù)值為2问畅,表示還剩下兩個數(shù)據(jù)緩存在FIFO中实檀。
如果再接著讀2個 數(shù)據(jù),則計數(shù)值為0按声,F(xiàn)IFO就被讀空了膳犹。
好了,我們分析完了同步FIFO是如何判斷空滿的签则,下面重點放在異步FIFO的原理上须床。
我曾寫過一篇CDC問題的博客,談到了異步FIFO的設(shè)計:
談?wù)効鐣r鐘域傳輸問題(CDC)
這篇博客中說渐裂,同步FIFO可以使用計數(shù)方式來判斷空滿豺旬,但是異步FIFO不能,因為寫指針和讀指針根本不在同一個時鐘域柒凉,計數(shù)器無法處理這樣的計數(shù)族阅。
那么怎么處理呢?
博客里采用的方法是對讀寫指針的位寬多添1位膝捞,這樣可以在讀寫指針相等時坦刀,表示FIFO空,而在寫指針和讀指針最高位不同,而其他位相等時鲤遥,也即寫指針大于讀指針一個FIFO深度的數(shù)值沐寺,表示FIFO滿,這不就是意味著寫指針繞了一圈盖奈,又追上了讀指針了嗎混坞?
恰是如此,用來解決不用計數(shù)而具體判斷FIFO空滿的問題钢坦。
這只是解決了判斷空滿的一個問題究孕,也就是確定指針的關(guān)系!
那下一個問題就是如何判斷爹凹?
由于讀寫指針不在同一個時鐘域厨诸,二者需要同步到同一個時鐘域后進(jìn)行判斷大小。
具體的操作就是在各自的時鐘域內(nèi)進(jìn)行讀寫操作逛万,同時:
- 判斷是否寫滿時,需要將讀指針轉(zhuǎn)換成格雷碼形式批钠,再同步到寫時鐘域宇植,與寫指針比較,判斷是否寫滿埋心!
細(xì)心的人恐怕能否發(fā)現(xiàn)指郁,這里存在的一個小插曲,當(dāng)讀指針轉(zhuǎn)換成格雷碼以及同步到寫時鐘域的過程中拷呆,讀寫指針可能還都在遞增闲坎,這樣的話,等同步后的讀指針與寫指針相等時(不包括最高位)茬斧,實際的讀指針可能已經(jīng)變了腰懂,這樣的話其實還有幾個空間沒有寫滿!但這樣設(shè)計就有問題嗎项秉?沒有問題绣溜!這叫保守設(shè)計,可以增加FIFO的安全性娄蔼。
下面是判斷是否寫滿的示意圖:
上面是寫滿判斷的情況怖喻,下面給出讀空判斷的可能情形分析:
- 當(dāng)判斷是否讀空時,需要把寫指針同步到讀時鐘域岁诉,具體過程是先將寫指針轉(zhuǎn)換為格雷碼锚沸,再同步到讀時鐘域,之后和讀指針比較涕癣,如果二者相等哗蜈,則空標(biāo)志置位!
還是和第一種情況有同樣的插曲,當(dāng)寫指針轉(zhuǎn)換成格雷碼以及同步到讀時鐘域的過程中恬叹,寫指針和讀指針都可能還在遞增候生,這樣當(dāng)二者判斷相等的時候,則寫指針可能還多寫了幾個空間绽昼,實際上并沒有讀空唯鸭。
那問題來了,這樣操作就有問題了嗎硅确?同樣沒有問題目溉,這樣也保證來了FIFO的安全,防止被讀空菱农。
下面給出手繪示意圖:
到此缭付,這一種設(shè)計方式的異步FIFO算是講完了,下面就是設(shè)計的問題了循未。
異步FIFO設(shè)計
如果你認(rèn)真分析了上述異步FIFO的實現(xiàn)方式陷猫,那么你會分分鐘寫出實現(xiàn)代碼,我的版本如下:
`timescale 1ns / 1ps
////////////////////////////////////////////////////
// Engineer: Reborn Lee
// Module Name: asy_fifo
// https://blog.csdn.net/Reborn_Lee
////////////////////////////////////////////////////
module asy_fifo#(
parameter DATA_WIDTH = 8,
parameter DATA_DEPTH = 32
)(
input wr_clk,
input wr_rst,
input wr_en,
input [DATA_WIDTH - 1 : 0] wr_data,
output reg full,
input rd_clk,
input rd_rst,
input rd_en,
output reg [DATA_WIDTH - 1 : 0] rd_data,
output reg empty
);
// define FIFO buffer
reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
//define the write and read pointer and
//pay attention to the size of pointer which should be greater one to normal
reg [$clog2(DATA_DEPTH) : 0] wr_pointer = 0, rd_pointer = 0;
//write data to fifo buffer and wr_pointer control
always@(posedge wr_clk) begin
if(wr_rst) begin
wr_pointer <= 0;
end
else if(wr_en) begin
wr_pointer <= wr_pointer + 1;
fifo_buffer[wr_pointer] <= wr_data;
end
end
//read data from fifo buffer and rd_pointer control
always@(posedge rd_clk) begin
if(rd_rst) begin
rd_pointer <= 0;
end
else if(rd_en) begin
rd_pointer <= rd_pointer + 1;
rd_data <= fifo_buffer[rd_pointer];
end
end
//wr_pointer and rd_pointer translate into gray code
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g, rd_ptr_g;
assign wr_ptr_g = wr_pointer ^ (wr_pointer >>> 1);
assign rd_ptr_g = rd_pointer ^ (rd_pointer >>> 1);
//wr_pointer after gray coding synchronize into read clock region
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_gr, wr_ptr_grr, rd_ptr_gr, rd_ptr_grr;
always@(rd_clk) begin
if(rd_rst) begin
wr_ptr_gr <= 0;
wr_ptr_grr <= 0;
end
else begin
wr_ptr_gr <= wr_ptr_g;
wr_ptr_grr <= wr_ptr_gr;
end
end
//rd_pointer after gray coding synchronize into write clock region
always@(wr_clk) begin
if(wr_rst) begin
rd_ptr_gr <= 0;
rd_ptr_grr <= 0;
end
else begin
rd_ptr_gr <= rd_ptr_g;
rd_ptr_grr <= rd_ptr_gr;
end
end
// judge full or empty
always@(posedge rd_clk) begin
if(rd_rst) empty <= 0;
else if(wr_ptr_grr == rd_ptr_g) begin
empty <= 1;
end
else empty <= 0;
end
always@(posedge wr_clk) begin
if(wr_rst) full <= 0;
else if( (rd_ptr_grr[$clog2(DATA_DEPTH) - 1 : 0] == wr_ptr_g[$clog2(DATA_DEPTH) - 1 : 0])
&& ( rd_ptr_grr[$clog2(DATA_DEPTH)] != wr_ptr_g[$clog2(DATA_DEPTH)] ) ) begin
full <= 1;
end
else full <= 0;
end
endmodule
注意事項
- 讀寫指針寬度要是$clog2(DATA_DEPTH) + 1的妖,定義的時候應(yīng)該定義為:
reg [$clog2(DATA_DEPTH) : 0] wr_pointer = 0, rd_pointer = 0;
- 其次绣檬,判斷空的時候要拿轉(zhuǎn)換為格雷碼并且同步到讀時鐘域之后的寫指針與讀指針比較,比較代碼如下:
always@(posedge rd_clk) begin
if(rd_rst) empty <= 0;
else if(wr_ptr_grr == rd_ptr_g) begin
empty <= 1;
end
else empty <= 0;
end
一定要二者相等的下一個讀周期empty信號為1嫂粟;
- 對于滿full信號娇未,一定要用轉(zhuǎn)換為格雷碼且同步到寫時鐘域之后的讀指針與轉(zhuǎn)換為格雷碼之后的寫時鐘比較,比較的條件是最高位不同星虹,但是其他位相同零抬。
always@(posedge wr_clk) begin
if(wr_rst) full <= 0;
else if( (rd_ptr_grr[$clog2(DATA_DEPTH) - 1 : 0] == wr_ptr_g[$clog2(DATA_DEPTH) - 1 : 0])
&& ( rd_ptr_grr[$clog2(DATA_DEPTH)] != wr_ptr_g[$clog2(DATA_DEPTH)] ) ) begin
full <= 1;
end
else full <= 0;
end
- 最后提出的是轉(zhuǎn)換為格雷碼的方式是組合邏輯的方式,即:
//wr_pointer and rd_pointer translate into gray code
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g, rd_ptr_g;
assign wr_ptr_g = wr_pointer ^ (wr_pointer >>> 1);
assign rd_ptr_g = rd_pointer ^ (rd_pointer >>> 1);
當(dāng)然你用時序邏輯也可以哦宽涌。
異步FIFO仿真
我們對上述設(shè)計進(jìn)行行為仿真平夜,先給出我的測試文件:
`timescale 1ns/1ps
module asy_fifo_tb;
parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 16;
reg wr_clk;
reg wr_rst;
reg wr_en;
reg [DATA_WIDTH - 1 : 0] wr_data;
wire full;
reg rd_clk;
reg rd_rst;
reg rd_en;
wire [DATA_WIDTH - 1 : 0] rd_data;
wire empty;
initial begin
wr_clk = 0;
forever begin
#5 wr_clk = ~wr_clk;
end
end
initial begin
rd_clk = 0;
forever begin
#10 rd_clk = ~rd_clk;
end
end
initial begin
wr_rst = 1;
rd_rst = 1;
wr_en = 0;
rd_en = 0;
#30
wr_rst = 0;
rd_rst = 0;
//write data into fifo buffer
@(negedge wr_clk)
wr_data = $random;
wr_en = 1;
repeat(7) begin
@(negedge wr_clk)
wr_data = $random; // write into fifo 8 datas in all;
end
// read parts
@(negedge wr_clk)
wr_en = 0;
@(negedge rd_clk)
rd_en = 1;
repeat(7) begin
@(negedge rd_clk); // read empty
end
@(negedge rd_clk)
rd_en = 0;
//write full
# 150
@(negedge wr_clk)
wr_en = 1;
wr_data = $random;
repeat(15) begin
@(negedge wr_clk)
wr_data = $random;
end
@(negedge wr_clk)
wr_en = 0;
#50 $finish;
end
asy_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.DATA_DEPTH(DATA_DEPTH)
) inst_asy_fifo (
.wr_clk (wr_clk),
.wr_rst (wr_rst),
.wr_en (wr_en),
.wr_data (wr_data),
.full (full),
.rd_clk (rd_clk),
.rd_rst (rd_rst),
.rd_en (rd_en),
.rd_data (rd_data),
.empty (empty)
);
endmodule
仿真波形為:
仿真通過,且功能符合預(yù)期卸亮。
由于本博客寫的時候有點長褥芒,幾乎一天了,所以就到這里吧嫡良!不得不說的是锰扶,異步FIFO的實現(xiàn)方式肯定不只有這一種,還有很多其他實現(xiàn)方式寝受,各位可以自行嘗試坷牛。
后面如果有更多有關(guān)FIFO的有趣知識或者心得體會,我會繼續(xù)補充很澄!
參考資料
交個朋友
- 個人微信公眾號:FPGA LAB京闰;
- FPGA/IC技術(shù)交流2020