異步FIFO 原理及verilog仿真(保姆級)


1 異步FIFO結構

文章轉自:

https://baijiahao.baidu.com/s?id=1724030588865450475            感謝老鐵!

在上篇文章中我們給出了FIFO的基本接口圖

並且指出,該圖適用於所有的FIFO,這次我們先看看異步FIFO內部的大體框圖

異步FIFO主要由五部分組成:寫控制端、讀控制端、FIFO Memory和兩個時鍾同步端

寫控制端用於判斷是否可以寫入數據

讀控制端用於判斷是否可以讀取數據

FIFO Memory用於存儲數據

兩個時鍾同步端用於將讀寫時鍾進行同步處理

介紹完內部結構,我們在和基本接口圖做個聯動

剛才說過,讀/寫控制端用於判斷能否寫入/讀取數據,判斷能否寫入/讀取數據關鍵在於:

  • 寫操作時,寫使能有效且FIFO未滿
  • 讀操作時,讀使能有效且FIFO未空

因此兩個使能信號和空滿判斷信號都連接到控制端上

最后我們再加上時鍾信號和復位信號

這便是完整的異步FIFO簡化框圖

2 空滿判斷

在同步FIFO篇中,我們給出了兩個判斷空滿狀態的圖

並且也有指出,讀空狀態可以理解為讀地址指針追上寫地址指針,寫滿狀態可以理解為寫地址指針再次追上讀地址指針

在同步FIFO中,因為讀寫都是在同一個時鍾信號下進行的,因此兩個地址指針可以直接進行比較

但在異步FIFO中,讀寫是在不同的時鍾信號下進行的,因此在進行比較之前,應當先進行跨時鍾與同步

在時鍾同步之前,我們應當先將二進制地址轉換為格雷碼,因為格雷碼相鄰的兩個狀態之間,只有1 bit數據發生翻轉

下面給出二進制數與格雷碼的對照圖

上面也有說到,讀指針追上寫指針是讀空,寫指針再次追上讀指針是寫滿,為了便於理解,我們做一個環形圖

假設內圈為讀,外圈為寫,讀空時是讀寫指針應當指向同一個地址,就像這樣

此時,讀地址應當和寫地址完全相同,就以0010為例,0010的格雷碼為0011,可以看出對於讀空狀態,無論是二進制還是格雷碼均是所有位都相同

寫滿和讀空略有不同,應當是下面這樣

細心的小伙伴應該可以發現,上面在提到寫滿時,說的是寫指針再次追上讀指針,也就是說,寫滿時,寫指針比讀指針多走一圈,為了便於區分,將地址位寬從3 bit拓寬到4 bit,因此此時的寫指針地址可以認為是1010

1010的格雷碼是1111, 0010的格雷碼是0011,對比兩個格雷碼是不是可以發現,此時高兩位相反,低兩位相同,這便是格雷碼下寫滿的判斷條件

Verilog中表示為

 //寫滿判斷
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
fifo_full <= 0;
else if((wr_ptr_g[$clog2(DEPTH)] != rd_ptr_grr[$clog2(DEPTH)]) && (wr_ptr_g[$clog2(DEPTH) - 1] != rd_ptr_grr[$clog2(DEPTH) - 1]) && (wr_ptr_g[$clog2(DEPTH) - 2 : 0] == rd_ptr_grr[$clog2(DEPTH) - 2 : 0]))
fifo_full <= 1;
else
fifo_full <= 0;
end

//讀空判斷
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn)
fifo_empty <= 0;
else if(wr_ptr_grr[$clog2(DEPTH) : 0] == rd_ptr_g[$clog2(DEPTH) : 0])
fifo_empty <= 1;
else
fifo_empty <= 0;
end

3 時鍾同步

在同步FIFO設計中,因為讀寫指針在同一個時鍾下,因此可以直接進行比較

但在異步FIFO中,由於讀寫指針在不同的時鍾下,因此需要將兩個地址指針進行時鍾同步操作

在異步FIFO中,常用的同步方法是兩級同步打拍延遲,同步地址指針的大致過程如下:

寫操作時,先將寫地址指針轉換成格雷碼,然后通過兩級同步(兩級同步在讀時鍾下進行),將寫地址指針同步到讀時鍾域下;讀操作類似

根據這個過程圖,也可以看出空滿判斷的方式:

  1. 寫滿在寫時鍾下判斷,將寫地址指針的格雷碼與同步過來的讀地址指針格雷碼進行比較,符合寫滿條件,即FIFO虛滿
  2. 讀空在讀時鍾下判斷,將讀地址指針的格雷碼與同步過來的寫地址指針格雷碼進行比較,符合讀空條件,即FIFO虛空

留意下,這里我說的是虛空/滿,並不是輸入錯誤喲,具體解釋我放在文章最后,愛思考的朋友現在可以思考一下原因

下面給出時鍾同步的Verilog代碼

assign wr_ptr_g = wr_ptr ^ (wr_ptr >> 1); //B2G
assign rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);

//寫指針同步到讀時鍾域
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) 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

//讀指針同步到寫時鍾域
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn) 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

4 異步FIFO設計

下面給出整體Verilog代碼

module asy_fifo#(
parameter WIDTH = 8,
parameter DEPTH = 8
)(
input [WIDTH - 1 : 0] wr_data,
input wr_clk,
input wr_rstn,
input wr_en,
input rd_clk,
input rd_rstn,
input rd_en,
output fifo_full,
output fifo_empty,
output [WIDTH - 1 : 0] rd_data
);
//定義讀寫指針
reg [$clog2(DEPTH) : 0] wr_ptr, rd_ptr;

//定義一個寬度為WIDTH,深度為DEPTH的fifo
reg [WIDTH - 1 : 0] fifo [DEPTH - 1 : 0];

//定義讀數據
reg [WIDTH - 1 : 0] rd_data;

//寫操作
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
wr_ptr <= 0;
else if(wr_en && !fifo_full) begin
fifo[wr_ptr] <= wr_data;
wr_ptr <= wr_ptr + 1;
end
else
wr_ptr <= wr_ptr;
end

//讀操作
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
rd_ptr <= 0;
rd_data <= 0;
end
else if(rd_en && !fifo_empty) begin
rd_data <= fifo[rd_ptr];
rd_ptr <= rd_ptr + 1;
end
else
rd_ptr <= rd_ptr;
end

//定義讀寫指針格雷碼
wire [$clog2(DEPTH) : 0] wr_ptr_g;
wire [$clog2(DEPTH) : 0] rd_ptr_g;

//讀寫指針轉換成格雷碼
assign wr_ptr_g = wr_ptr ^ (wr_ptr >>> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >>> 1);


//定義打拍延遲格雷碼
reg [$clog2(DEPTH) : 0] wr_ptr_gr, wr_ptr_grr;
reg [$clog2(DEPTH) : 0] rd_ptr_gr, rd_ptr_grr;

//寫指針同步到讀時鍾域
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) 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

//讀指針同步到寫時鍾域
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn) 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

//聲明空滿信號數據類型
reg fifo_full;
reg fifo_empty;

//寫滿判斷
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
fifo_full <= 0;
else if((wr_ptr_g[$clog2(DEPTH)] != rd_ptr_grr[$clog2(DEPTH)]) && (wr_ptr_g[$clog2(DEPTH) - 1] != rd_ptr_grr[$clog2(DEPTH) - 1]) && (wr_ptr_g[$clog2(DEPTH) - 2 : 0] == rd_ptr_grr[$clog2(DEPTH) - 2 : 0]))
fifo_full <= 1;
else
fifo_full <= 0;
end

//讀空判斷
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn)
fifo_empty <= 0;
else if(wr_ptr_grr[$clog2(DEPTH) : 0] == rd_ptr_g[$clog2(DEPTH) : 0])
fifo_empty <= 1;
else
fifo_empty <= 0;
end
endmodule

下面是tb

module asy_fifo_tb;
parameter width = 8;
parameter depth = 8;

reg wr_clk, wr_en, wr_rstn;
reg rd_clk, rd_en, rd_rstn;

reg [width - 1 : 0] wr_data;

wire fifo_full, fifo_empty;

wire [width - 1 : 0] rd_data;

//實例化
asy_fifo myfifo (
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.wr_rstn(wr_rstn),
.rd_rstn(rd_rstn),
.wr_en(wr_en),
.rd_en(rd_en),
.wr_data(wr_data),
.rd_data(rd_data),
.fifo_empty(fifo_empty),
.fifo_full(fifo_full)
);


//時鍾
initial begin
rd_clk = 0;
forever #25 rd_clk = ~rd_clk;
end

initial begin
wr_clk = 0;
forever #30 wr_clk = ~wr_clk;
end

//波形顯示
initial begin
$fsdbDumpfile("wave.fsdb");
$fsdbDumpvars(0, myfifo);
$fsdbDumpon();
end

//賦值
initial begin
wr_en = 0;
rd_en = 0;
wr_rstn = 1;
rd_rstn = 1;

#10;
wr_rstn = 0;
rd_rstn = 0;

#20;
wr_rstn = 1;
rd_rstn = 1;

@(negedge wr_clk)
wr_data = {$random}%30;
wr_en = 1;

repeat(7) begin
@(negedge wr_clk)
wr_data = {$random}%30;
end

@(negedge wr_clk)
wr_en = 0;

@(negedge rd_clk)
rd_en = 1;

repeat(7) begin
@(negedge rd_clk);
end

@(negedge rd_clk)
rd_en = 0;

#150;

@(negedge wr_clk)
wr_en = 1;
wr_data = {$random}%30;

repeat(15) begin
@(negedge wr_clk)
wr_data = {$random}%30;
end

@(negedge wr_clk)
wr_en = 0;

#50;
$finish;
end

endmodule

下面貼上運行結果

這里有一點需要說明

藍色框的位置,已經開始寫入數據,但fifo_empty信號並沒有被拉低,而是在第三個rd_clk上升沿被拉低,這是因為在判斷FIFO是否讀空時,是在讀時鍾下判斷,並且,進行判斷時,寫地址格雷碼需要在讀時鍾域進行兩次同步,最后進行比較

為了更清晰的解釋,可以將所有的地址指針也加入到波形圖中

注意剛才提到的位置

在位置1,有數據寫入,此時wr_ptrwr_ptr_g都發生了變化,wr_ptr_grwr_ptr_grr保持不變

在位置2,wr_ptr_gr變化,wr_ptr_grr保持不變

在位置3,wr_ptr_grr才開始發生變化

在位置4,wr_ptr_grrrd_ptr_g進行比較,判定此時FIFO非空

還有一個有意思的地方

注意wr_ptr_grwr_ptr_grr,這兩個保持了兩個rd_clk時鍾周期,返回查看完整波形圖的話,這種情況只出現在寫指針的格雷碼

這tb文件中,我們設定的是,rd_clk比wr_clk快,打兩拍同步的方式,慢時鍾域同步到快時鍾域和快時鍾域同步到慢時鍾域處理方式是不同的,后面有時間再做介紹。

有興趣的小伙伴,可以試下如果rd_clk比wr_clk慢,這種情況會出現在rd_ptr_grrd_ptr_grr

5 一個我在面試中被問到的問題

我在面試的時候被問到,如果跨時鍾域同步時,同步過來的是一個亞穩態的數據,那么FIFO還能否正常工作?

在空滿判斷部分說過,讀空或寫滿可以理解為:

  • 讀空時:可以理解為是讀指針在追寫指針
  • 寫滿時:可以理解為是寫指針在追讀指針

為了方便理解,我們將其抽象成一個追逐運動,以讀空為例

先確定讀空的判斷,讀空判斷是在rd_clk下,讀指針的格雷碼與寫指針同步過來的格雷碼進行比較,實際上,讀指針追的是wr_ptr_grr

wr_ptr_grr又是寫指針留在原地的殘影(寫指針經過兩個讀時鍾周期后得來的),在兩個讀時鍾周期的這段時間,寫指針可能會原地不動,也可能繼續前行,這便是虛空的概念

虛滿的概念同理

 

 

 

 

 

 

 

疑問1:進行數據空滿的對比時,讀寫時鍾域都有讀寫指針,此時應該怎么對比?為什么?

另外一篇博客中有這樣的話:

同步rd_cntr至clk_write時鍾域,再與wr_cntr進行對比來決定FIFO是否滿;(判斷滿是wr_cntr - rd_cntr)
同步wr_cntr至clk_read時鍾域,再與rd_cntr進行對比來決定FIFO是否空;(判斷空是rd_cntr == wr_cntr)

這是結論,那現在我們必須要仔細的思考這句話為什么是這么做的。

首先畫個示意圖。

 

 

 

那現在我們來設想各種情況下會發生什么,在此我們暫時不考慮亞穩態的事情,認為信號通過同步模塊都被采到了但是會有信號丟失。

第一種情況我們假定寫時鍾特別快,讀時鍾都采不齊寫指針。

那么此時空邏輯會不會出錯呢?假設寫指針已經跑了1 2 3 4 5 6 7 8 9,而讀時鍾采到了1 2 6 8;那么如果此時讀指針就在8,兩邊一對比發現一樣(當然了這只是假設的一種情況)則會報“空”!那么實際空沒空呢,沒有因為我寫到9了寫進去了一個數,不過沒關系之后必然會采到9(或者采到10一類的),狀態會很快恢復正常,或者說沒有空而報了空我們還可以接受,因為這樣對於一個將要空的FIFO會停止讀數旋即恢復正常,不會使其數據發生紊亂。那會不會有空了而報不空使得讀出數據出現問題的情況呢?不會的,你想想看寫指針跑的比你采樣的快,是趨向與“不空”(越寫數據越多嘛)的,因此不會出現這樣的錯誤。

讀指針被同步到寫時鍾域本身不會出現漏采的情況,因此“滿”邏輯的判斷不會出現問題。

第二種情況我們假定讀時鍾域特別快,寫時鍾都采不齊讀指針。

此時的滿邏輯會出錯么?我們來看下。我們同樣假設讀指針從1跑到了9,而只被采樣到了7。如果此時寫指針也寫到了7,那么二者一比較發現寫“滿”了,實際呢沒有滿,不過此時也會停止外部寫入(傳出了滿的信號),這是不會對FIFO中的數據產生影響的,並且很快會恢復到“不滿”。如果此時寫指針到了5,那么二者對比會得出“不滿”的邏輯傳出,真實情況呢同樣是不滿,因為讀到7就已經不滿了真實情況讀到了9自然更加“不滿”。這樣就解釋清楚了。

寫指針被同步到讀時鍾域本身不會出現漏采的情況,因此“空”邏輯的判斷不會出現問題。

受到HR的點播或許我們可以選擇一種記法:

“滿”邏輯是要給誰的——給寫信號告訴他你別寫了——那么寫指針能多跑出去么?跑多了不就把數據覆蓋了!——所以必須在寫時鍾域進行對比;

“空”邏輯是要給誰的——給讀信號告訴他你別讀了——那么讀指針能多跑出去么?跑多了不就讀出來錯誤的數了!——所以在讀時鍾域進行對比;

我現在先理解到這一步吧。

疑問2:為什么要用格雷碼進行同步傳輸?

我覺得使用格雷碼的優勢體現在讀寫時鍾差異不是特別大的時候,不能一個是1000M一個是10M那誰也救不了了,無限加長FIFO深度吧。那么我們假定讀寫時鍾頻率差異沒有過大,例如一個133M一個100M這樣的。

我們要知道這個異步時鍾采樣,再不經過特殊處理的情況下采錯了是在所難免的。單個信號可能采錯或者沒采到,那多個信號的讀寫指針就更加有可能出問題了。我們來看下如果此時的指針是1011(二進制=2),那么在向1100(2)跳變時候,由於信號走的距離不一樣啦觸發事件或者邏輯門延時不同啦等等原因,在另外一段時鍾域就可能采出多種情況例如1011(2)、1110(2)、1101(2)等,總之每個信號都可能是正確值或者未跳變時候的值。那么在得到“空”“滿”邏輯時候很大可能概率會出錯,這個我們不能忍。過程如下圖所示。

所以說我們就要選擇格雷碼了,來看下格雷碼發生了什么事。1011(2)=1110(g),1100(2)=1010(g),因此格雷碼跳變為1110(g)->1010(g)。同樣考慮采樣出問題了,由於我們提前說好了讀寫時鍾頻率差距不是太大,因此采樣可能得到兩種情況:1110(g)和1010(g),到另外的時鍾域后會轉換為二進制的1011(2)和1100(2)。看到這里是不是想到了什么!你看如果得到的是1100那沒問題呀,這就是真是的值。如果是得到1011呢?我實際跑到了1100你采到了1011是不是類似於上一個問題的“指針實際已經跑到了9而你只采到了7,會不會出問題”,答案是不會呀!原因就在於我們進行對比時時鍾域的選擇已經解決了這個問題。因此可知使用格雷碼即時出現了采樣錯誤的情況,也不會時“空滿”判定出現問題。

 

 

判斷滿的方法是拿write_pointer和做完CDC轉換過來的read_pointer作比較。CDC轉換的方法是采用格雷碼過兩級flip-flop的同步器。由於read_pointer屬於100M的頻率, write時鍾域有500M,屬於快采慢,CDC轉換不會有什么問題。因此滿的判斷不會有問題。 判斷空的方法是拿read_pointer和做完CDC轉換過來的write_pointer做比較。CDC轉換的方法也是采用格雷碼過兩級flip-flop的同步器。由於write_pointer的變化頻率是 500M,而read時鍾域只有100M的clk,屬於慢采快,CDC的轉換就會出問題。 因為read時鍾的頻率過低,所以write_pointer做完CDC之后得到的將是零星的離散的采樣值,而非連續的值,比如說可能會采樣到格雷碼的5->10->15,而非連續的格雷碼的5->6->7->8............ 那么該怎么解決這個問題? 因為快時鍾往慢時鍾傳數據,要想一個不漏,唯一的辦法就是hold住(通常來講,至少hold住目標時鍾域2T)。把當前數據牢牢保持住,等到數據被取走了再傳下一個。但是很明顯write_pointer屬於自增型的,只要fifo沒有滿,它都可以持續增加,並不會hold不變,所以必然會漏采。 為什么不需要解決? 因為只要fifo夠大,即便讀到的write_pointer是離散的,也不會影響到fifo判空。只要fifo判空不出錯,異步fifo的行為就沒有問題,只不過效率可能略微降低。 舉個栗子: 當read時鍾域采樣到write_pointer的值是5的時候,真正的write_pointer的值可能是多少?可能已經是5,6,7,8,9等等了。但絕對不可能比5小。 為什么不可能比5小? 因為對於write_pointer而言,它從4變到5,這個沿變在跨CDC的時候,只有沒采着和采着兩種可能,沒采着的話,跨完時鍾域得到4,采着了就得到5。絕對不可能得到比5大的值。 也就是說,只要是格雷碼跨CDC,跨過去的值只可能比真實值相等或者小,不可能比真實值大! 所以如果在read時鍾域已經看到write_pointer等於5了,那么真實的write_pointer必然不小於5。那么基於write_pointer等於5來判斷空,將會得到一個很保守的空(也可以叫假空)!也就是說異步fifo明明還有數,讀時鍾域就判斷出空,暫時停止讀取數據。 但是這並不會導致出錯,因為這種保守的空判斷,只是降低了讀的效率,並沒有導致讀出錯誤的數據或者不存在的數據。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM