文章目錄
說明
翻譯自:FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version的第7章第一節
內容阻塞賦值VS非阻塞賦值
有兩種指派陳述式被用在always塊內:阻塞賦值與非阻塞賦值。關於阻塞與非阻塞複製有3條簡單的準則:
- 將電路分為兩部分:寄存器電路和組合電路
- 在寄存器電路中使用非阻塞賦值
- 在組合電路中使用阻塞賦值
1 概覽
阻塞賦值 基本文法如下:
[var] = [expression];
當該條語句被執行時,右手邊的運算式將被賦給左手邊的變數,期間不允許其他語句的幹擾。因此,就阻塞了其他語句,直到該條語句執行完畢為止。阻塞賦值的行為與C語言中的變數賦值類似。
非阻塞賦值 基本文法如下:
[var] <= [expression];
非阻塞賦值的行為非常令人難以琢磨。當always塊被啟用(在time step的開始),右手邊的運算式被賦初值。當運行到always塊的結尾(即time step的結尾),運算所得的值被賦給左手邊的變數。
以x變數執行非阻塞賦值為例。因為Verilog模型的實際流程比較複雜,我們將非阻塞賦值的行為翻譯成一下幾個步驟:
- 在alway塊的開始,x值傳遞給x_entry;
- 右手邊的變數x的值被x_entry取代;
- 左手邊變數x的值被x_exit取代;
- 在always塊的結束,x_exit的值傳遞給x。
在下面的程式碼片段內,上述四個步驟被呈現在代碼的注釋中。
always@*begin // x_entry = x y <= x & ... // y = x_entry & ... x <= ... // x_exit = ...end // x = x_exit
範例 為了瞭解阻塞賦值和非阻塞賦值的區別,我們用三輸入的的電路來做討論。
代碼1 使用阻塞賦值的電路
module and_blocking( input a, input b, input c, output reg y); always@*begin y = a; y = y & b; y = y & c;endendmodule
阻塞賦值的欣慰類似於C語言中的順序賦值。y最終得到的值為a & b & c。注意,此代碼僅用於示範,使用順序語意學來描述電路是比較差勁的行為。
下面給出的代碼,其中的阻塞賦值被替換為非阻塞賦值。代碼注釋詳細說明了y的賦值動作。
代碼2 使用非阻塞賦值的電路
module and_nonblocking( input a, input b, input c, output reg y); always@*begin // y_entry = y y <= a; // y_exit = a y <= y & b; // y_exit = y_entry & b y <= y & c; // y_exit = y_entry & cend // y = y_exitendmodule
注意always塊內的前2條語句將不會產生任何效果。上述always塊等價與:
always@* y <= y & c;
2 組合電路
上一個小節的範例屬於極端的情況。除了預設值,大部分的組合電路並不會多次賦值同一變數。阻塞賦值和非阻塞賦值都可以用於描述同一電路。然而,它們有一些微妙的區別。下面的範例用於解釋這些不同。讓我們以一位同或(異或非)電路為例。我們將詳細列出敏感列表中的變數。
代碼3 使用阻塞賦值的一位同或電路
module eq1_blocking( input i0, input i1, output reg eq); reg p0, p1;always@(i1, i2) // 只有i0和i1在敏感列表begin // 語句的順序非常重要 p0 = ~i0 & i1; p1 = i0 & i1; eq = p0 | p1;endendmodule
注意到敏感列表僅包括i0和i1,。當其中之一變化時,always塊被啟用,p0、p1和ep被順序運算,ep在第一個time step的結尾被更新。語句的順序非常重要。假設性我們移動最後面的語句到最前面。
always@(i1, i2) begin eq = p0 | p1; p0 = ~i0 & i1; p1 = i0 & i1;end
在第一條語句中,由於p0和p1還沒有被指定新的值,因此先前被啟用的值將會被用到。而先前的值將意味著鎖存器的存在,故此代碼是不正確的。
下面將使用非阻塞賦值替換阻塞賦值。
代碼4 使用非阻塞賦值的一位同或電路
module eq1_nonblocking( input i0, input i1, output reg eq); reg p0, p1;always@(i1, i2, p0, p1) // p0、p1也在敏感列表中 // 語句的順序不重要 begin // p0_entry = p0; p1_entry = p1 p0 <= ~i0 & i1; // p0_exit = ~i0 & ~i1 p1 <= i0 & i1; // p1_exit = i0 & i1 eq <= p0 | p1; // eq_exit = p0_entry | p1_entryend // eq = eq_exit; p0 = p0_exit; p1 = p1_exit endmodule
注意p0和p1也包括在敏感列表中。當i0或i1變化時,always塊被啟用;在第一個time step的結尾,p0和p1被賦以新值。既然ep取決於p0和p1(p0_entry和p1_entry)的舊值,那麼其值在第一個time step的結尾保持不變。噹噹前的time step執行完畢,always塊重新被啟用,因為p0和p1發生了變化(這便於為何p0和p1也要置于敏感列表之中的原因)。注意語句的順序不影響結果。
3 儲存單元
使用非阻塞賦值來引用儲存空間。例如,D觸發器:
always@(posede clk) q <= d;
當然也可以用阻塞賦值來引用D觸發器,如下:
always@(posede clk) q = d;
雖然在單個D-FF情況下面,上面的代碼工作正常,但是當多個寄存器互相動作的時候,這裡就出現許多微妙的問題。
考慮兩個寄存器在每個刻度交換資料。使用阻塞賦值,代碼為:
always@(posede clk) a = b; always@(posede clk) b = a;
在clk的上升沿,兩個always塊都被啟用,並行操作。這兩個操作應該在同一time step結束。根據Verilog的標準,兩個always塊的執行可以以任何順序列入。若第一個always塊先執行,則由於阻塞賦值的緣故a立即得到b的值。當第二個always塊執行的時候,b得到a的重新整理值,及b的原始值,因此b保持不變。類似的,若第二個always塊先執行,a得到的也是其初始值。這就是Verilog中的競爭冒險(race condition)。從Verilog的角度看,兩種結果都是有效。
下面我們修改代碼中的阻塞賦值為非阻塞賦值。
always@(posede clk)begin // b_entry = b a <= b; // a_exit = b_entryend // a = a_exit always@(posede clk)begin // a_entry = a b <= a; // b_exit = a_entryend // b = b_exit
通過注釋我們看到,因為輸入(entry)值都被用於賦值,所以無論執行的順序,a和b都得到正確的值。
因此為了避免競爭冒險,我們使用非阻塞賦值來引用D-FF和觸發器。
4 混用阻塞和非阻塞賦值的時序電路
在同一個always塊內,有可能混用阻塞賦值和非阻塞賦值。下面我們使用簡單的常式來解釋不同組合的行為,以加強對賦值的理解。
圖4.1 通過混合賦值來引用電路
考慮圖4.1(b)所示的原理圖。當時鐘上升沿來臨之時,a、b與運算所得的值被存入D-FF。基於前面的講解,我們可以將儲存和組合電路分配到兩段代碼中。如代碼4.1所示。
代碼4.1 兩段實現
module ab_dff_2seg( input clk, input a, input b, output reg q);reg q_next;// D-FFalways@(posedge clk) q <= q_next;// 組合電路always@* q_next = a & b;endmodule
我們可以變換一下,將兩段組合在一起,使用單個always塊來描述電路。下面通過六次嘗試,來描述阻塞和非阻塞賦值的不同組合的區別。如代碼4.2所示。
代碼4.2 混合賦值常式
module ab_dff_mix( input clk, input a, input b, output reg q0, output reg q1, output reg q2, output reg q3, output reg q4, output reg q5);reg ab0, ab1, ab2, ab3, ab4, ab5;// 嘗試0always@(posedge clk)begin ab0 = a & b; q0 <= ab0;end// 嘗試1always@(posedge clk)begin // ab1_entry = ab1; q1_entry = q1 ab1 <= a & b; // ab1_exit = a & b q1 <= ab1; // q1_exit = ab1_entryend // ab1 = ab1_exit; q1 = q1_exit// 嘗試2always@(posedge clk)begin ab2 = a & b; q2 = ab2;end// 嘗試3(調換嘗試1的順序)always@(posedge clk)begin q0 <= ab0; ab0 = a & b;end// 嘗試4(調換嘗試2的順序)always@(posedge clk)begin // ab4_entry = ab4; q4_entry = q4 q4 <= ab4; // q4_exit = ab4_entry ab4 <= a & b; // ab4_exit = a&bend // ab4 = ab4_exit; q4 = q4_exite// 嘗試5(調換嘗試3的順序)always@(posedge clk)begin q5 = ab5; ab5 = a & b;endendmodule
在嘗試0中,起初賦值給ab0和q0將引用兩個寄存器,一個用於儲存寄存器ab0,另一個用於儲存寄存器q0。因為ab0在塊賦值時被立即更新,所以q0得到了a&b的值。對應的原理圖4.1(a)所示。由於ab0在always塊外沒有被使用,因此寄存器ab0的輸出就不是必需存在的,即相應的寄存器可以被移除。這樣,結果電路就4.1(b)所示,也就是所需的電路。
在嘗試1中,對ab1使用了非阻塞賦值,對應的闡述寫到了注釋裡面。注意q1得到的是ab1_entry而非ab1_exit。而ab1_entry是先前儲存的ab值,即對應一個寄存器的輸出。相應的原理圖4.1(c)所示。一個不確定的輸入緩衝被引用,同時a&b的值延遲一個刻度後才被儲存到q1中。
在嘗試2中,ab2和q2都是用了阻塞賦值。該代碼所引用的電路,與嘗試1等同,4.1(a)和(b)所示。由於使用阻塞賦值來引用D-FF,有可能產生競爭冒險,因此不推薦使用這種類型的代碼。
出於示範的目的,讓我們來測試一下調換嘗試0、1和2的賦值順序會發生什麼。其結果代碼如嘗試4、5和6所示。在嘗試3中,ab3未更新便被使用,因此q3得到的是先前啟用區塊所產生的值。所引用的電路如4.1(c)所示。而嘗試4,交換語句的順序不影響綜合的效果,因此等同於嘗試1。嘗試5中,由於ab5未更新值便被使用,因此q5得到的寄存器a&b的值,等同於嘗試3。
簡而言之,只有嘗試0描述的電路正確且可靠。在嘗試0中,我們可以將ab0移除,合并代碼如下:
// 嘗試0always@(posedge clk)begin q0 <= a & b;end
推薦閱讀
1 SNUG.Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!
參考
1 Pong P. Chu.FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version.Wiley
另見
[與艾米一起學FPGA/SOPC].[邏輯實驗文檔連載計劃]