書接上回

(隔得有點久嘛)

從 IP 開始,學習數字邏輯:FIFO 篇(上)

為 FIFO 編寫 testbench

在使用各種手段測試我們的 FIFO ip 之前,我們首先得寫一個 testbench。

testbench 是什麼,Vivado 會告訴你就是一個普通的 v 檔案。在這個 v 檔案中,例項化需要被測試的模組,然後寫一些激勵語句:

FIFO,好好幹,年底升職加薪。。

。。激勵是不可能這麼激勵的。激勵語句指的是為待測試模組的輸入埠訊號指定電平狀態,觀察輸出埠的訊號是否滿足設計功能。比如

rst_n = 0;//好了,大家休息下,我們復位了

#100; //100ns 後

rst_n = 1;//好了 大家肯定休息好了 我們該幹活了。

testbench 唯一特殊的一點可能是他不需要真正的輸入輸出埠。只需要在模組中,將待測試模組的輸入埠連線到宣告的 reg 變數,將輸出埠連線到 wire 型變數。因為在 testbench 中需要改變待測試模組的輸入訊號,但只觀察而不需要更改輸出訊號。

那麼如何生成 testbench 呢,和之前新增頂層檔案的時候有一點小特殊:在 Add source 後選擇新增 sim 檔案而不是 design 檔案。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

這裡給 testbench 檔案的命名提個小建議,可以將 tb 檔案的名字加上字首 tb_ 這樣比較容易將 tb 檔案與原始檔區分。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

那麼如何編寫 testbench ,其實很簡單。悄悄說下 ISE 時代更簡單,只要滑鼠點點就行,現在還是要想點辦法的。

首先,自己寫,其實也很簡單,例項化 FIFO 模組頂層,然後將輸入埠宣告為 reg 變數,輸出埠宣告為 wire 變數即可。

第二種辦法:使用 Vivado Tcl 商店中的 Tcl 指令碼工具。(這個我沒用過)

第三種辦法:暫時還不能用,但這裡又要插一個廣告了:我想寫一個 VSCode 外掛(想寫,還沒開始寫外掛,只寫了 Python 生成 tb 的程式碼),可以很方便地生成 tb 檔案,以下就是用該外掛的程式碼生成的:

`timescale 1ns / 1ps

////////////////////////////////////////////////////////////////////////////////

// Company:

// Engineer:

//

// Create Date:

// Design Name: fifo_top

// Module Name:

// Project Name:

// Target Device:

// Tool versions:

// Description:

//

//

//

// Dependencies:

//

// Revision:

// Revision 0。01 - File Created

// Additional Comments:

//

////////////////////////////////////////////////////////////////////////////////

module tb_fifo_top;

//inputs

reg clk;

reg rst;

reg [7:0] din;

reg wr_en;

reg rd_en;

//outputs

wire [7:0] dout;

wire full;

wire almost_full;

wire empty;

wire almost_empty;

wire [3:0] data_count;

wire prog_full;

wire prog_empty;

// Instantiate the Unit Under Test (UUT)

fifo_top uut (

。clk(clk),

。rst(rst),

。din(din),

。wr_en(wr_en),

。rd_en(rd_en),

。dout(dout),

。full(full),

。almost_full(almost_full),

。empty(empty),

。almost_empty(almost_empty),

。data_count(data_count),

。prog_full(prog_full),

。prog_empty(prog_empty) );

initial begin

// Initialize Inputs

clk = 0;

rst = 0;

din = 0;

wr_en = 0;

rd_en = 0;

// Wait 100 ns for global reset to finish

#100;

// Add stimulus here

end

always #10 clk = ~clk ;

endmodule

Testbench 的骨架還差一些工作:為 tb 的時鐘新增時鐘。我們假設時鐘為 50M,即每 10 ns 時鐘翻轉一次。注意這實際上是一個 always 塊,所以要寫到 initial 塊外部。

always #10 clk = ~clk ;

這個實際上等同一種更完整的寫法:always 塊在模擬中會不斷無條件地觸發,觸發後翻轉 clk,延時 10 秒,結束 always 塊,但實際上結束後又進入下一次無條件觸發。

always begin

clk = ~ clk;

#10;

end

開始模擬

終於,我們可以用一些騷操作來仔細觀察我們的 FIFO 了。

復位特性

首先關注 FIFO 的復位特性,我們的 FIFO 復位為高電平有效。在模擬開始時候復位電平設為高,100ns 後拉低復位電平,FIFO 開始工作。從下圖中可以觀察到 FIFO 的一些復位特性:

從 IP 開始,學習數字邏輯:FIFO 篇(下)

在 100 ns 時刻後,empty 訊號 和 almost_empty 訊號因為 FIFO 為空,所以為高電平有效。但我們可以觀察到 full 以及 almost full 訊號確仍然保持高電平,實際上此時,FIFO 顯然沒有滿,所以這兩個訊號是不正確的。他們需要一段時間,也就是直到 260 ns 時刻,恢復到正常的低電平,這說明這兩個狀態訊號在復位後需要一段時間才能恢復正常。

接下來我們依次向 FIFO 寫入 16 個數據,再依次讀取。FIFO 的深度為 16。我們透過編寫 testbench,連續產生 16 次 wr_en 寫有效訊號,並每次 wr_en 寫有效時,寫入資料加一。延遲一段時候後,再連續產生 16 次 rd_en 讀有效訊號,將之前寫入的資料全數讀取出來。testbench 的寫法如下(修改 initial 塊)

initial

begin

// Initialize Inputs

clk

=

0

rst

=

1

din

=

0

wr_en

=

0

rd_en

=

0

// Wait 100 ns for global reset to finish

#

100

// Add stimulus here

rst

=

0

#

200

repeat

16

begin

wr_en

=

1

#

20

wr_en

=

0

#

20

din

=

din

+

1

‘b1

end

#

100

repeat

16

begin

rd_en

=

1

#

20

rd_en

=

0

#

20

end

end

這裡使用了 repeat,這個在 testbench 中的常用語法。repeat begin 塊之間的語句會被多次重複執行,重複執行次數寫在括號中。

將我們要模擬的 testbench 檔案設定為 simulation 中的頂層檔案,在檔案上右擊,選擇 Set as Top 即可,頂層檔案的左側就會出現那個小小的,綠綠的圖案。一般在只有一個 testbench 檔案時,會被預設設為頂層。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

在左側導航欄中,選擇 SIMULATION 中的 Run simulation - behavioral 開始模擬,那麼問題來了:會對哪個檔案進行模擬?自然是我們上一步中設定的模擬頂層檔案了,這裡不會給你選擇的機會,會直接對頂層檔案進行模擬。

在開始模擬之前,可以設定選用的模擬器。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

我這裡推薦初學者使用 Vivado 自帶的模擬器,因為不需要多餘的設定,開箱即用。雖然功能相比 Modelsim 等模擬軟體確實有所不足,但等熟悉模擬之後,察覺 vivado 模擬器功能有限時,再轉而使用 Modelsim 應該也不遲。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

狀態訊號

嗯,從上方這張平淡無奇的模擬結果圖中,我們似乎還是能找到一些亮點。首先來看三個空狀態訊號。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

第一個空狀態訊號,在第一個 wr_en 訊號結束後的第一個時鐘上升沿置低。almost_empty 訊號在第二個寫使能訊號後的時鐘上升沿置低,代表此時 FIFO 中已經有超過一個數據。而 prog_empty 我們自定義的“幾乎”空訊號,在寫入三個資料後置低,因為我們設定的自定義閾值是 2,FIFO 中有超過兩個資料後訊號不再有效。不過我們可以觀察到可程式設計訊號和原生訊號相比有一個週期的延時,如果對週期敏感的應用應當注意到這個小小的週期時延。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

full 滿訊號和 empty 訊號的特性完全相同,我們來看下 full 訊號的置高與置低的過程。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

在復位一段時間後,full 訊號恢復正常。當寫入第 14 個數據後,prog_full 訊號置起。寫入第15 個數據後,在寫有效訊號高電平之後的第一個上升沿,almost_full 訊號置起。最後是 full 訊號,prog_full 訊號仍然有一個時鐘的延遲。

FIFO 提供了一組介面用於顯示當前 FIFO 中的資料個數。在第一個資料寫入後,data_count 就變化為 1,之後每寫入一個數據增長 1 。在某些情況下,我們需要記錄寫入 FIFO 的資料數量,比如我們需要在 FIFO 中快取一幀 16 byte 長的資料,我們的 FIFO 出於多幀資料緩衝的需求,深度肯定遠大於一幀資料的長度,那麼我們顯然無法依靠空,滿訊號進行判斷。一方面可以自己使用邏輯對寫使能進行計數,或者我們可以使用 FIFO 核提供的計數功能,該功能我沒有驗證過,但在同步的情況下,資料計數應該是完全準確的。

值得注意的是在寫入第 16 個數據後,計數輸出變為 0 ,這是個小失誤,因為我們的四位計數值顯然在記錄 16 時溢位了,因此我們一般需要 log2(深度) + 1 位寬的計數值。

讀延遲與 First Word Fall Through 特性

接下來我們求證一件配置 IP 核時看到的一行小字:

從 IP 開始,學習數字邏輯:FIFO 篇(下)

不知道大家對這行小字還有沒有印象,沒有的話可以看下上篇的ip核配置

所謂“讀延遲:1”指的究竟是怎樣的延遲?我們來看讀取的時序波形:

從 IP 開始,學習數字邏輯:FIFO 篇(下)

第一行是讀取的資料,第二行是讀使能訊號,最後一行是時鐘。我們從第二個讀使能訊號來看會比較清晰,因為資料通道的復位值是 0x0,但第一個寫入的資料也是 0x0,所以第一個讀使能訊號看不太清晰。第二個讀使能訊號在黃線處的時鐘上升沿置起,直到下一個時鐘上升沿,資料 0x01 才會出現在資料線上,這就是讀訊號時的一個時鐘延遲,一個時鐘的長度是相對於讀使能有效的第一個時鐘上升沿而言。

1個時鐘的固定延遲對於簡單的同步系統來說問題不大,只要在讀訊號有效之後固定延遲一個週期再讀取或者使用讀資料線上的資料即可。比如使用狀態機時,在上一個狀態置起讀有效,等到下一個狀態再讀取資料。

那麼有沒有辦法消除這個延遲,這就又要說說我們上篇中配置 ip 核時見到的 First Word Fall Through 特性。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

當你勾選該項功能時,延時轉為顯示 0

該特性的主要功能是,哪怕你還沒送出讀使能訊號,我就把FIFO 中下一個資料準備到資料線上。比如 FIFO 中有兩個資料 0x1,0x2,當你什麼都還沒做時,讀資料線上已經是 0x1了,當你讀取一個數據後,資料線上就變成了 0x2 ——下一個等待讀取的資料。但注意到為了實現這個特性,FIFO 真正的深度已經擴充套件為 18 位。(那麼計滿,計空是按照 18 位還是 16 位呢?)。

在開啟了 First Word Fall Through 特性後的波形圖如下:

從 IP 開始,學習數字邏輯:FIFO 篇(下)

可以看到和上文描述相符的特性。在第一個資料 0x80 寫入後,經過三個時鐘的延遲後,dout 輸出 0x80,0x80 是第一個等待讀取的資料,也就是接下來一個等待讀取的資料。可以在圖中右側看到,當讀使能有效,0x80 在第一個時鐘上升沿被讀取後,接下來一個等待讀取的資料就出現在 dout 訊號中,即消除了一個週期的讀延遲。當然這是有代價的。(小問題:請問代價有哪些?注意 empty 訊號,比較未開啟 Fall Through 時的情況)

當我們寫溢位會怎樣,是拋棄最早的資料還是無視最新的資料?

FIFO 使用中最需要注意的問題在於溢位,我們需要藉助空/滿訊號來判定 FIFO 的狀態,儘量避免 FIFO 的讀寫溢位。但如果我們寫溢位了會怎樣?

我們向深度為 16 (開啟 First Word Fall Through 特性後實際深度為 18 )的FIFO ,嗯,一口氣寫 20 個數據和使能訊號。當我們讀取 20 個訊號時,我們是會讀到前 20 個,還是後 20個數據?

從 IP 開始,學習數字邏輯:FIFO 篇(下)

答案是前 18 個數據,讀取到的最後一個數據是 0x66 ,在 0x66 之後的兩個寫入資料 0x00 和 0x78 並沒有進入 FIFO。

所以結論是 FIFO 在寫滿之後,會保證之前寫入的資料,而拒絕新寫入的資料。另外,能夠容納的資料並不是名義上的 FIFO 深度,而是 IP 核配置介面顯示的實際深度,本例中是 18 。

資料因為 FIFO 寫滿而丟失,很有可能造成嚴重的系統問題,需要認真選取合適的 FIFO 深度。(作者也在學習此道)

當我們的 FIFO 沒有資料,但頭鐵硬要讀取會怎樣?

在寫入 16 個數據後,我們閒來無事,決定讀取個 20 次資料。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

你讀取到了 16 個數據,並沒有什麼特別的事情發生。

當我們同時讀寫會怎樣?

當 FIFO 沒有資料時,在開啟 Fall Through 的情況下,同時讀取和寫入資料。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

可以發現,這種情況下存在問題:

在前三個讀使能週期,讀取到的都是 FIFO 中的初始值 0x00,直到第 3 個讀使能訊號,才讀取到 FIFO 中的第一個資料 0x80,最終 16 個讀使能訊號實際上只讀到了 14 個有效資料。

但如果先寫入 3 個數據後,再同時讀寫

從 IP 開始,學習數字邏輯:FIFO 篇(下)

此時就不會出現問題,所以開啟 Fall Through 的情況下,前 2 個週期是無法讀取資料的,但在之後的時鐘中,同時讀取也是不會有問題的。

從 IP 開始,學習數字邏輯:FIFO 篇(下)

沒有開啟Fall Through 的情況下,第一個讀使能會因為一個週期的讀延遲無法讀到資料。也就是說會少讀取一個數據。

結束語

到這裡這篇有關 FIFO ,或者說有關同步 FIFO 的教程就到這裡結束了。你可能覺得意猶未盡(太長不看),但沒辦法,同步 FIFO 作為常用的,基礎的 IP 核,可玩的花樣並不多。以後我們講講非同步 FIFO ,那才有意思呢。(其實我現在還不會用,等我先學習下先)

本文中簡要地介紹瞭如何在 Vivado 環境中配置,新增一個 FIFO ip 核,構建頂層檔案與 testbench 檔案。編寫激勵,並透過模擬瞭解 FIFO 的諸項特性。

如果讀者能讀到這裡,我會告訴你本文的閱讀完成率感人,你已經擊敗了 98% 的玩家,嗯,希望讀者你有所收穫。