使用邏輯門和連續賦值對電路建模,是相對詳細的描述硬件的方法。使用過程塊可以從更高層次的角度描述一個系統,稱作行為級建模(behavirol modeling)。
1. 過程賦值
阻塞賦值和非阻塞賦值的區別都很熟悉了。這里記錄兩個特性。
1.1 特性1
絕大多數情況下,非阻塞賦值都是一個時間點處最后執行的賦值語句??聪旅娴氖纠a:
?
module test ( input clk, output reg a, b ); always @ (posedge clk) begin a = 0; b = 1; a <= b; b <= a; end endmodule
?
非阻塞賦值可以認為包括兩步:
(1)求值和調度(evaluate and schedule),先得到非阻塞賦值等式右側的值,并將這次賦值安排在當前時間點的結束時刻。
(2)當前時間點結束時,更新左側的值。因此這段代碼的結果是 a = 1,b = 0。
1.2 特性2
如果過程塊內,有針對同一個變量的多個非阻塞賦值,那么這些非阻塞賦值會按順序執行(但我認為不能簡單地說過程塊內是“順序執行的”,容易造成誤導,應該說具有一定的“順序性”特點)。
看下面的示例代碼:
?
module test ( input clk, output reg a, b ); always @ (posedge clk) begin a <= 0; a <= 1; end endmodule
?
always塊內有兩條對于變量a的賦值語句,但由于順序性特點,a的賦值結果應該是1。利用這個特性,會經常見到下面這種代碼寫法:
?
always @ (posedge clk) begin a <= 0; if (flag == 1) a <= 1; end
?
只有當flag=1時,a才為1。
2. 過程連續賦值
這種賦值方式允許在過程塊中連續地驅動網絡或變量。但這種建模方法不可綜合,因此這里只簡單記錄一下兩種過程連續賦值方式的作用。
assign和deassign:assign連續賦值會優先占用一個變量,讓其它對這個變量進行賦值的過程塊無效。deassign連續賦值會解除占用關系。
看下面的示例代碼:
?
`timescale 1ns / 1ps module sim(); reg clk = 0, rst_n = 0, d = 1; reg q; //test i1 //( // .clk (clk), // .rst_n(rst), // .q(q), // .d(d) //); always @ (rst_n) if (!rst_n) assign q = 0; else deassign q; always @ (posedge clk) q <= d; always #5 clk <= ~clk; initial begin #50 rst_n = 1; end endmodule
?
當rst_n=0時,asssign連續賦值占用了q,q的值恒為0;當rst_n=1時,deassign解除了占用,q的值由其它過程賦值決定,在clk上升沿隨d變化而變化。
force和release:功能和assign、deassign相同,只是賦值對象可以是變量也可以是網絡。force過程賦值的對象為網絡時,會使其它所有對該網絡的驅動無效。
3. case語句
case語句的default分支不是必須的,只要設計者清楚設計意圖即可。記錄一下case兩個比較少見但有時候特別有用的用法。
3.1 do-not-cares
包括兩種:
? ? ?casez表示不關心高阻狀態(z);
? ? ?casex表示不關心高祖狀態(z)和未知狀態(x)。
在不關心的bit位上使用“?”表示要更加方便。casex和casez完全是可以綜合的,例如下面的代碼可以實現優先編碼器:
?
module test ( input clk, input [3:0] d, output reg [15:0] q ); always @ (posedge clk) casez (d) 4'b1??? : q <= 1; 4'b01?? : q <= 2; 4'b001? : q <= 3; 4'b0001 : q <= 4; endcase endmodule
?
如果想使用casex和casez,還是要從“設計上”能否綜合的角度考慮一下,并且做好綜合后的仿真。
比如上面的代碼,使用case語句+16條分支可以實現同樣的效果,這個設計完全是可以綜合實現的。使用casez更多還是起到簡化代碼設計的作用。
3.2 常數case
case語句中可以使用常數表達式,這個常數會和每個分支中的表達式進行比較。如下面的代碼:
?
module test ( input clk, input [3:0] d, output reg [15:0] q ); always @ (posedge clk) casez (1) d[0] : q <= 1; d[1] : q <= 2; d[2] : q <= 3; d[3] : q <= 4; endcase endmodule
?
可以實現,根據d中的哪個bit為1,執行相應的代碼。如果d中多個bit同時為1,此時多條分支同時滿足,會執行順序最前面的一條。總之還是要清楚設計意圖,做好仿真工作。
4. 循環語句
forever 和 repeat 是完全不可綜合的,只用于仿真文件的設計。
循環語句 for 和 while 并不是完全不能綜合的,但因為Verilog是對硬件進行建模,for和while的使用肯定不像在軟件編程語言中使用一樣靈活。還是上面的老話,要從“設計上”能否綜合的角度進行考慮。
如果循環語句的使用出現問題,綜合工具會給出提示,如Vivado的提示信息如下:
?
[Synth 8-3380] loop condition does not converge after 2000 iterations
?
用好循環,可以簡化代碼設計。一個寄存器鏈的示例如下(使用for也能達到同樣的效果):
?
module test ( input clk, rst_n, input [15:0] d, output reg [15:0] q ); integer i; (* keep = "true" *)reg [15:0] mem [7:0]; always @ (posedge clk or negedge rst_n) begin if (!rst_n) begin i = 0; while(i < 8) begin mem[i] <= 0; i = i + 1'b1; end end else begin i = 0; mem[0] <= d; while(i < 7) begin mem[i+1] <= mem[i]; i = i + 1'b1; end end q <= mem[7]; end endmodule
?
相應的RTL原理圖如下 :
按軟件編程思維考慮,循環語句是一條一條執行的一個過程。而從上面的設計結果來說,顯然while循環中的所有語句是“同時”執行的,代碼只是將很多具有重復性特點的賦值語句改用 while/for 的形式來編寫。
5. 過程塊
過程塊(procedure)包括四種:initial結構、always結構、任務(task)、函數(function)。這里只記錄兩個不太熟悉的特性。
5.1 零延遲無限循環
always塊在仿真文件中,都要與一些時序控制配合使用。如果always塊中沒有任何推動仿真時間的控制,仿真會卡在一個時間點。比如經常用如下語句創建時鐘信號:
?
always #10 clk = ~clk;
?
如果寫成了如下的形式:
?
always clk = ~clk;
?
相當于形成了一個零延遲的無限循環,仿真時間會卡在0s無法前進。如果運行這個代碼,輕則程序卡死,重則系統奔潰只能重啟。
5.2 initial用于初始化
initial塊是可以綜合的,只不過不能添加時序控制語句,因此作用有限,一般用于變量的初始化。如下面代碼:
?
reg [15:0] mem [7:0]; integer i; initial begin for (i = 0; i < 8; i=i+1) mem[i] = i; end
?
6. 過程塊時序控制
此特性主要用于仿真文件中,部分在硬件設計中也會涉及。Verilog有兩種明確的時序控制類型:延時控制和事件表達式。仿真時間正是靠過程塊中的延時控制、事件控制以及wait語句來推動的。
6.1 延時控制
用于控制語句的執行時間,比如描述激勵的波形。延遲值可以是表達式,比如“#d rega = regb;”,這條賦值語句會在延遲d個時間單位后執行。
(1)如果d的計算結果是高阻(z)或未知(x),則當作0處理;
(2)如果d的計算結果為負數,也會將其視作無符號數來看待,如下面的代碼:
?
parameter [7:0] delay = -50; initial begin rst_n = 0; #(-delay) rst_n = 1; #delay rst_n = 0; end
?
rst_n先延遲50個時鐘后變為1;由于8bit -50的二進制補碼當作無符號數看時值為206,因此在延時206個時鐘后,rst_n值又變為0。
6.2 事件表達式
直到某些仿真事件發生時,語句才會只執行。網絡或變量的值發生變化,稱作隱式事件(implict event);設計者設置一些命名事件,可能會由其它過程塊觸發,稱作顯式事件(explicit event)。
值的變化、或變化的方向(上升沿posedge或下降沿negedge)都是隱式事件。雖然在硬件設計中經常和always配合使用(比如 always @ (posedge clk) ),但在仿真文件中有更多靈活的使用方法。看下面的代碼示例:
?
// example1:clk上升沿,語句執行 reg [7:0] delay = 0; initial begin forever @(posedge clk) delay = delay + 1'b1; end // example2:clk的值發生變化,語句執行 reg [7:0] delay = 0; always begin @(clk) delay = delay + 1'b1; end
?
如果 posedge 和 negedge 檢測的對象是一個表達式或多位寬的數據,則只會檢測LSB上的邊沿變化。如下:
?
// example3 reg [2:0] cnt = 0; always @ (posedge clk) cnt <= cnt + 1'b1; reg [7:0] delay = 0; always begin @(posedge cnt) delay = delay + 1'b1; end
?
檢測3bit變量cnt的上升沿,相當于檢測cnt[0]的邊沿事件。
事件(event) 是除了變量和網絡外Verilog中的另一種數據類型,如果一個標識符被申明為事件類型,則稱作“命名事件”,需要顯示地觸發。雖然事件是一種數據類型,但它本身又沒有任何“數據”。如下面的示例:
?
event trig; // 命名事件申明 reg [2:0] cnt = 0; always @ (posedge clk) begin cnt <= cnt + 1'b1; if (cnt == 7) -> trig; // 事件顯示觸發 end reg [7:0] delay = 0; always begin @(trig) delay = delay + 1'b1; // 事件捕獲 end
?
使用命名事件可以有效的實現多個過程塊之間的通信和同步。
如果過程塊語句的執行同時對多個事件敏感,可以使用事件的邏輯或特性。在事件敏感列表中使用 “or” 或 “,”(這兩個符號含義等價),如“always @ (posedge clka or posedge clkb, trig)”。
還有一個特性稱作隱式事件表達式,符號為“@*”,會把過程時序控制語句中所有讀取的變量和網絡添加到事件表達式中
6.3 wait語句
上面的事件控制方法都是邊沿敏感型的。還可以使用wait語句控制過程塊的時序,直到某項條件為true時才執行相應語句,這種方法稱作電平敏感型。
如果wait中的條件為false,則過程塊會一直阻塞,直到條件變為真時,才會執行后面的語句。比如下面的代碼:
?
reg [7:0] cnta = 0, cntb = 0; initial begin wait(en) #10 cnta <= 60; #10 cntb <= 70; end
?
對于begin…end(順序塊) 而言,wait會阻止順序塊的執行,直到en為1時,cnta和cntb的兩條賦值語句才會執行。如果使用fork…join(并行塊),則上述代碼中的wait只會對cnta的賦值語句有效,此時最好也為wait語句加上塊聲明(begin…end或fork…join)。
6.4 賦值間(Intra-assignment)時序控制
賦值間延遲和事件控制是另一種時序控制方法,如
?
a = #5 b;
?
與“ #5 a = b; ”不同,賦值語句右邊的表達式會馬上求值,延遲和事件只是控制這個值賦值給賦值語句左邊的時間。比如上面的代碼等效于:
?
begin temp = b; #5 a = temp; end
?
利用賦值間時序控制的特性,可以巧妙地完成一些行為建模。比如下面的代碼可以避免賦值語句間的“競爭”,達到數據交換的效果:
?
fork // 并行塊,存在競爭 #5 a = b; #5 b = a; join fork // 數據交換 a = #5 b; b = #5 a; join
?
賦值間延遲之前會先求等式右邊的值,延遲后才會把這個值賦到左邊,因此上面代碼相當于交換了a和b的值。很多工具在實現Verilog的賦值間時序控制這個特性時,都會使用臨時存儲來存放右邊表達式的值。
也可以用事件控制:
?
a = @(posedge clk) b; //等效于 begin temp = b; @(posedge clk) a = temp; end
?
賦值間時序控制還有一個特點是可以用repeat來控制延遲或事件執行的次數,如:
?
a = repeat(3) @(posedge clk) b; //等效于 begin temp = b; @(posedge clk); @(posedge clk); @(posedge clk); a = temp; end
?
要注意如果采用變量的形式 “ repeat (num) ”:
? ? ? 若num是無符號數:當num為負數時,相當于二進制補碼對應的無符號數。比如num = -1,repeat(num) 相當于 repeat(7) 。
? ? ? 若num是帶符號數:當num為0或負數時,這條語句將永遠不會被執行。
7. 塊(block)
塊(block)是一些賦值語句的組合,包括:
? ? ? 順序塊begin-end:塊中語句按照給定的順序執行,因此塊中的延遲、事件控制相當于起到了隔斷的作用。順序塊的開始時間是第一條語句開始執行的時刻,結束時間是最后一條語句執行完的時刻。
? ? ? 并行塊fork-join:塊中語句同時執行,即所有語句的開始時間相同。并行塊的結束時間是所有語句都執行完的時刻。
7.1 嵌套塊
通常要使用多個塊的嵌套實現更復雜的控制邏輯,因此最好要理解各個塊的開始時間和結束時間。下面給出幾個例子:
?
// Example1 begin fork @Aevent; @Bevent; join areg = breg; end
?
由于fork-join的并行性,A和B兩個事件可以以任意的順序出現,fork-join塊結束后執行賦值語句 areg = breg。
?
// Example2 begin begin @Aevent; @Bevent; end areg = breg; end
?
如果換成begin-end,事件的觸發必須按照給定的順序。如果B事件先出現,再出現A,那么內部嵌套的begin-end還要再等待B事件的發生。
?
// Example3 fork @Aevent; begin #ta wa = 0; #ta wa = 1; end @Bevent; begin #tb wb = 1; #tb wb = 0; end join
?
fork-join中的兩個順序塊的執行分別受到兩個事件的控制。由于fork-join的并行性,兩個begin-end的觸發和執行同樣也是并行的。
7.2 命名塊
每個塊都可以在begin和fork后面為其附加名字,稱為命名塊。其它語句可以通過這個名字來引用命名塊,最常見的是“命名塊+disable”的用法。
disable語句可以終止命名塊的運行,一般用于處理異常情況,比如下面的代碼:
?
begin : block_name ... if (a == 0) disable block_name; ... end
?
當滿足a == 0時,begin-end塊會終止運行。disable會終止整個命名塊的運行,包括命名塊中的其它所有塊和已調用的任務。利用這個特性可以實現兩個功能:
? ? ? 中止一個循環語句(相當于C語言中的break)
? ? ? 跳過循環中的某些狀態(相當于C語言中的continue)
雖然Verilog沒有直接提供類似于C語言中break和continue的關鍵詞,但可以使用“命名塊+disable”來實現此特性??聪旅娴氖纠a:
?
reg [7:0] cnt = 0; always @ (posedge clk) cnt <= cnt + 1'b1; reg [7:0] data; integer i; initial begin : break for (i = 0; i < 100; i = i + 1) begin : continue @(posedge clk) if (cnt == 5) disable break; data <= cnt; end end
?
for循環中,當滿足一定條件時," disable break; "會終止initial之后的begin-end塊的執行,整個循環也就終止了。
如果改成" disable continue; ",當滿足條件時,會終止for之后的begin-end塊的執行,這樣只會終止當前的循環狀態,而不會影響循環的下一次迭代。
??
審核編輯:劉清
評論
查看更多