前言
在數據流級描述中已經將硬件建模從比較底層的門級結構提升到了數據流級。但數據流級描述除了個別語句外,主要的部分還是使用操作符來描述電路的邏輯操作或者計算公式,沒有實現真正意義上的功能描述。行為級描述則可以實現從抽象層次更高的級別來描述功能電路。
initial與always語句
在Verilog 中有兩種結構化過程語句:initial語句和always語句,它們是行為級建模的兩種語句
。其他所有的行為語句只能出現在這兩種結構化過程語句里。
Verilog中各個執行流程並發執行,而不是順序執行的
,每個initial語句和 always語句代表一個獨立的執行過程,每個執行過程從仿真時間0開始執行,並且這兩種語句不能嵌套使用。
initial語句
所有在initial語句內的語句構成了一個initial 塊。
initial塊從仿真0時刻開始執行,在整個仿真過程中只執行一次
。
如果一個模塊中包括若干個initial 塊,則這些initial塊從仿真0時刻開始並發執行,且每個塊的執行是各自獨立的。
如果在塊內包含多條行為語句,那么需要將這些語句組成一組
一般是使用關鍵字
begin
和end
將它們組合為一個塊語句;
如果塊內只有一條語句,則不必使用begin和 end。
舉例:
module stimulus;
reg x,y, a,b,m;
initial
m=1'b0;
initial
begin
#5 a=1'b1;
#25 y=1'b1;
end
initial
begin
#10 x=1'b0;
#25 y=1'b1;
end
initial
#50 $finish;
endmodule
三條initial語句在仿真0時刻開始並行執行。
如果在某一條語句前面存在延遲#
always 語句
always語句包括的所有行為語句構成了一個always語句塊。
該always語句塊從仿真0時刻開始順序執行其中的行為語句
;
在最后一條執行完成之后,再次開始執行其中的第一條語句,如此循環往復,直至整個仿真結束。
因此 always語句通常用於對數字電路中一組反復執行的活動進行建模。
例子:用always語句為時鍾發生器建立模型的一種方法
module clock_gen(output reg clock);
initial
clock = 1'b0;
always
#10 clock=~clock;
initial
#1000 $finish;
endmodule
在這個例子中,clock信號是在initial語句中進行初始化的,如果將初始化放在always塊內,那么always語句的每次執行都會導致clock被初始化。如果沒有\(stop或\)finish語句停止仿真,那么這個時鍾發生器將一直工作下去。
控制方式
基於事件的控制在實際建模中使用最多,也是行為級建模的一個重要控制方式,其控制方式為“@”
引導的事件列表,也稱為敏感列表
使用方法為:
always @(敏感事件列表);
敏感事件列表是由設計者來指定的。在模塊中,任何信號的變化都可以稱為事件,一旦這些事件發生了,always 結構中的語句就會被執行。
換言之,always結構時刻觀察敏感事件列表中的信號,等待敏感事件出現,然后執行本結構中的語句。如果敏感時間有多個,可以使用or或“,”來隔開,這些事件是或的關系,只需要滿足一個就會觸發並執行 always語句結構。
基於事件控制的always語句
always @(a,b)
begin
e=c&d;
f=c|d;
end
這段代碼中都是以信號的名稱
作為敏感事件,表示的是對信號的電平敏感
,即信號只要發生了變化,就要執行always結構,這個變化指的是仿真器可以識別的任意變化,例如,從0變到1或從1變到0。
使用這種控制方式可以設計對電平信號敏感的電路,所有的組合邏輯電路
采用的都是這種控制方式。
敏感事件列表中的事件可以用“”代替,這個“”表示的是該always結構中所有的輸入信號。
時序電路采用的敏感列表一般是邊沿敏感的,信號的邊沿用posedge(上升沿)和 negedge(下降沿)來表示,但邊沿敏感不能使用“*”來省略。
順序塊和並行塊
塊語句包含兩種類型,順序塊和並行塊
順序塊
關鍵字begin和end
用於將多條語句組成順序塊。順序塊具有以下特點。
- (1)順序塊中的語句是一條接一條按順序執行的;只有前面的語句執行完成之后才能執行后面的語句(帶有內嵌延遲控制的非阻塞賦值語句除外)。
- (2)如果語句
包括延遲或事件控制
,那么延遲總是相對於
前面那條語句執行完成
的仿真時間的。
//eg1:
reg x,y;
reg [1:0] z,w;
initial
begin
x=1'b0;
y=1'b1;
z={x,y};
w={y,x};
end
//eg2:
reg x,y;
reg [1:0] z,w;
initial
begin
x=1'b0; //在仿真時刻0完成
#5 y=1'b1; //在仿真時刻5完成
#10 z={x,y}; //在仿真時刻15完成
#20 w={y,x}; //在仿真時刻35完成
end
在eg1中在仿真0時刻,x,y,z和 w的最終值分別為0,1,l和2。
在eg2中,這4個變量的最終值也是0,1,1和2。但是塊語句完成時的仿真時刻為35。
並行塊
並行塊由關鍵字fork和 join
聲明。並行塊具有以下特性。
-
(1)並行塊內的語句並發執行。
-
(2)語句執行的順序是由各自語句中的延遲或事件控制決定的。
-
(3)語句中的延遲或事件控制是
相對於塊語句開始執行的時刻
而言的。
注意,順序塊和並行塊之間的根本區別
在於:
並行塊中所有的語句同時開始執行,語句之間的先后順序是無關緊要的。
將程序3.14中帶有延遲的順序塊轉換為一個並行塊,
reg x,y;
reg [1:0] z,w;
initial
fork
x=1'b0; //在仿真時刻0完成
#5 y=1'b1; //在仿真時刻5完成
#10 z={x,y}; //在仿真時刻10完成
#20 w={y,x}; //在仿真時刻20完成
join
除了所有語句在仿真0時刻開始執行以外,仿真結果是完全相同的。
這個並行塊執行結束的時間是仿真時刻20,而不是仿真時刻35。
並行塊提供了並行執行語句的機制
可以將並行塊的關鍵字fork看成是將一個執行流分成多個獨立的執行流,而關鍵字join則是將多個獨立的執行流合並為一個執行流。
每個獨立的執行流之間是並發執行的。在使用並行塊時需要注意,如果兩條語句在同一時刻對同一個變量產生影響,那么將會引起隱含的競爭,這種情況是需要避免的。
塊語句的特點
塊語句具有三個特點:嵌套塊、命名塊和命名塊的禁用
嵌套塊
順序塊和並行塊可以嵌套使用,使用時要注意每一個嵌套塊開始的時間
舉例:
initial
begin
x=1'b0;
fork
#5 y=1'b1;
#10 z={x,y};
join
#20 w={y,x};
end
命名塊
塊可以具有自己的名字,稱為命名塊。
命名塊具有如下特點:
- 命名塊中可以聲明局部變量。
- 命名塊是設計層次的一部分,命名塊中聲明的變量可以通過層次名引用進行訪問。
- 命名塊可以被禁用,例如停止其執行。
命名塊和命名塊的層次名引用:
module top;
initial
begin b1 //名字為b1的順序命名塊
integer i; //整型變量i是block1命名塊的靜態局部變量
…… //可以通過top.b1.i被其他模塊所引用
end
initial
fork:b2 //名字為b2的並行命名塊
reg i; //寄存器變量i是b2命名塊的靜態局部變量
…… //可以通過層次名,top.b2.i被其他模塊引用
join
命名塊的禁用:Verilog通過關鍵字disable
提供了一種終止命名塊執行的方法。
disable可以用來從循環中退出,處理錯誤條件以及根據控制信號來控制某些代碼段是否被執行。
對塊語句的禁用導致緊接在塊后面的那條語句被執行。
對於C程序員來說,這一點非常類似於使用break退出循環。
兩者的區別在於 break 只能退出當前所在的循環,而使用disable則可以禁用設計中的任意一個命名塊
舉例:
//從標志寄存器的最低有效位開始查找第一個值為1的位
reg [15:0] flag;
integer i;
initial
begin
flag=16'b0010_0000_0000_0000;
i=0; //用於計數的整數
begin:b1
while(i<16) //while循環聲明中的主模塊是命名塊b1
begin
if(flag[i])
begin
$display(Encountered a TRUE bit at element number % d",i);
disable b1; //在標志寄存器中找到了值為真(1)的位,禁用b1
end
i=i+1;
end
end
end
選擇分支語句
if語句
條件語句用於根據某個條件來確定是否執行其后的語句,關鍵字if和 else
用於表示條件語句。
Verilog語言共有三種類型的條件語句:
//第一類條件語句:沒有else 語句
if(!lock)buffer = data;
if(enable)out = in;
//第二類條件語句:有一個else語句
if(number_queued<MAX_Q_DEPTH)
begin
data_queue = data;
number_queued = number_queued + 1;
end
else
$display("Queue Full.Try again");
//第三類語句:嵌套的if-else-if語句
if(alu_control == 0)
y=x+z;
else if(alu_control == 1)
y=x-z;
else if(alu_control == 2)
y=x*z;
else
$display( " Invalid ALU control signal " );
if條件成立或不成立時,執行的語句可以是一條語句,也可以是一組語句。
如果是一組語句,則通常使用begin和 end關鍵字將它們組成一個塊語句。
case語句
case語句使用關鍵字case、default和endcase來表示。
舉例:
case(expression)
alternative1:statement1;
alternative2:statement2;
alternative3:statement3;
……
default:default_statement;
endcase
case語句中的每一條分支語句都可以是一條語句或一組語句,多條語句需要使用關鍵字begin和 end
組合為一個塊語句。
在執行時,首先計算條件表達式的值,然后按順序將它和各個候選項進行比較。如果等於第一個候選項,則執行對應的語句statementl;如果和全部候選項都不相等,則執行default_statement語句。
注意, default_statement 語句是可選的,而且在一條case語句中不允許有多條default_statement。
另外, case語句可以嵌套使用。
舉例:
reg [1:0]alu_control;
……
case(alu_control)
2'd0:y=x+z;
2'd1:y=x-z;
2'd2=y=x*z;
default:$display( "Invalid ALU control signal");
endcase
case語句逐位比較表達式的值和候選項的值,每一位的值可能是0,l,x或z。
如果兩者的位寬不相等,則使用0填補空缺位來使兩者的位寬相等。
若選擇信號中有不確定值x,則輸出為x;若選擇信號中沒有不確定值x,但有高阻值z,則輸出為z。
帶x,z的case語句舉例

除了上面講述的case語句之外,case語句還有兩個變形,分別使用關鍵字casex和casez來表示。
- casex語句將條件表達式或候選項表達式中的x作為無關值。
- casez語句將條件表達式或候選項表達式中的z作為無關值,所有值為z的位也可以用“?”來代表;
- casex和 casez的使用可以讓我們在case表達式中只對非x或非z的位置進行比較。
reg [3:0] encoding;
integer state;
casex(encoding)
4'b1xxx:next_state = 3;
4'bx1xx:next_state = 2;
4'bxx1x:next_state = 1;
4'bxxx1:next_state = 0;
default:next_state = 0;
endcase
casez的使用與casex的使用類似
循環語句
Verilog語言中有4種類型的循環語句:while,for,repeat和forever。這些循環語句的語法與C語言中的循環語句類似。循環語句只能在always或initial 塊中使用
,循環語句可以包含延遲表達式。
while語句
while循環使用關鍵字 while
來表示。
while循環執行的中止條件是while表達式的值為假。
如果遇到while語句時while表達式的值已經為假,那么循環語句一次也不執行。
如果循環中有多條語句,則必須將它們組合成為begin和end塊。
舉例:

for語句
for循環使用關鍵字for來表示,它由以下三個部分組成。
- 初始條件
- 檢查終止條件是否為真
- 改變控制變量的過程賦值語句
用while循環語句描述的計數器也可以用for循環語句來描述

由於初始條件和完成自加操作的過程賦值語句都包括在for循環中,無須另外說明,因此for循環的寫法較while循環更為緊湊。
但是要注意 while循環比 for循環更為通用,並不是在所有情況下都能使用for循環來代替while循環。
for循環一般用於具有固定開始和結束條件的循環。
如果只有一個執行循環的條件,最好還是使用while循環。
repeat語句
用關鍵字repeat
來表示。
repeat循環的功能是執行固定次數的循環
,它不能像while循環那樣根據一個邏輯表達式來確定循環是否繼續進行。
repeat循環的次數必須是一個常量、一個變量或者一個信號。
如果循環重復次數是變量或者信號,循環次數是循環開始執行時變量或者信號的值,而不是循環執行期間的值
。

上述程序給出了如何使用repeat循環對數據緩沖區建模,這個數據緩沖區的功能是在收到開始信號之后第8個時鍾上升沿處鎖存輸入數據。
forever循環
關鍵字forever
用來表示永久循環。
在永久循環中不包含任何條件表達式,只執行無限的循環,直到遇到系統任務$finish為止。
forever循環等價於條件表達式永遠為真的 while循環,例如 while(1)。
如果需要從forever循環中退出,可以使用disable語句
。
通常情況下, forever循環是和時序控制結構結合使用的
:如果沒有時序控制結構,那么仿真器將無限次地執行這條語句,並且仿真時間不再向前推進,使得其余部分的代碼無法執行。
//時鍾發生器
reg clock;
initial
begin
clock=1'b0;
forever #10 clock = ~clock; //時鍾周期為20個單位時間
end
//在每個時鍾正跳變沿處使兩個寄存器的值一值
reg clock;
reg x,y;
initial
forever @(posedge clock) x = y;
過程賦值語句
過程賦值語句的更新對象是寄存器、整數、實數或時間變量。
這些類型的變量在被賦值后,其值將保持不變,直到被其他過程賦值語句賦予新值。
這與連續賦值語句是不同的
連續賦值語句總是處於活動狀態,任意一個操作數的變化都會導致表達式的重新計算以及重新賦值,
但過程賦值語句只有在執行到的時候才會起作用。
Verilog包括兩種類型的過程賦值語句:阻塞賦值語句和非阻塞賦值語句。
阻塞賦值語句
順序塊語句
中的阻塞賦值語句按順序執行,它不會阻塞其后並行塊中語句的執行
。
阻塞賦值語句使用“=”
作為賦值符。由於阻塞賦值語句是按順序執行的,因此如果在一個begin-end塊中使用了阻塞賦值語句,那么這個塊語句表現的是串行行為。
例子:

只有在語句x=0執行完成之后,才會執行y = 1,而語句count = count+ 1按順序在最后執行。
begin-end塊中各條語句執行的仿真時間如下。
- x=0 到 reg_b = reg_a 之間的語句在仿真0時刻執行;
- 語句reg_a[2] = 0在仿真時刻15進行
- 語句reg_b[15:13]={x,y,z}在仿真時刻25執行;
- 語句count=count+1在仿真時刻25執行。
注意,在對寄存器類型變量進行過程賦值時,如果賦值符兩側的位寬不相等,則采用以下原則。
- (1)如果右側表達式的位寬較寬,則將保留從最低位開始的右側值,把超過左側位寬的高位丟棄;
- (2)如果左側位寬大於右側位寬,則不足的高位補0。
阻塞賦值(=)
always @ (<event-expression>)
begin
<LHS1 = RHS1 assignments > //阻塞賦值語句1
<LHS2 = RHS2 assignments > //阻塞賦值語句2
...
end
阻塞賦值語句在每個右端表達式計算完后,立即賦給左端變量,即賦值語句 LHS1=RHS1執行完后 LHS1 是立即更新的,同時只有 LHS1=RHS1 執行完后才可執行語句 LHS2=RHS2,依次類推。前一條語句的執行結果直接影響到后面語句的執行結果。
非阻塞賦值
非阻塞賦值語句允許賦值調度
,但它不會阻塞位於同一個順序塊中其后語句的執行
。
非阻塞賦值使用“≤=”
作為賦值符,它與“小於等於”關系操作符是同一個符號,但在表達式中它被解釋為關系操作符,而在非阻塞賦值的環境下被解釋成非阻塞賦值。
為了說明非阻塞賦值的意義以及與阻塞賦值的區別,看下面程序

在這個例子中,從x=0到reg_b = reg_a之間的語句是在仿真0時刻順序執行的,之后的三條非阻塞賦值語句在reg_b = reg_a執行完成后**並發執行**
。
(1) reg_a[2]=0被調度到15個時間單位之后執行,即仿真時刻為15;
(2) reg_b[15:13]={x,y,z}被調度到10個時間單位之后執行,即仿真時刻為10;
(3) count = count+1被調度到無任何延遲執行,即仿真時刻為0。
從上面的分析中可以看到,仿真器將非阻塞賦值調度到相應的仿真時刻,然后繼續執行后面的語句,而不是停下來等待賦值的完成。
一般情況下,非阻塞賦值是在當前仿真時刻的最后一個時間步,即阻塞賦值完成之后才執行的。
在上面的例子中,我們把阻塞和非阻塞賦值語句混合在一起使用,目的是想更清楚地比較和說明它們的行為。
需要提醒讀者注意的是,不要在同一個always塊中混合使用阻塞和非阻塞賦值語句。
非阻塞賦值可以被用來為常見的硬件電路行為建立模型,例如當某一事件發生后,多個數據並發傳輸的行為。
非阻塞賦值(<=)
always @ (<event-expression>)
begin
<LHS1 <= RHS1 assignments > //非阻塞賦值語句1
<LHS2 <= RHS2 assignments > //非阻塞賦值語句2
...
end
非阻塞賦值語句右端表達式計算完后並不立即賦給左端,而是同時啟動下一條語句繼續執行
,我們可以將其理解為所有的右端表達式 RHS1、RHS2 在進程開始時同時計算,計算完后在進程結束時,同時分別賦給左端變量 LHS1、LHS2。
舉個栗子
假設已經有 m = 1,n = 2,i= 3
- 阻塞賦值
點擊查看代碼
//阻塞賦值
always @ (posedge clk)
begin
m = n;
n = i;
i = m;
end
//運行結果為 m = 2,n = 3,i = 2
- 非阻塞賦值
點擊查看代碼
//非阻塞賦值
always @ (posedge clk)
begin
m <= n;
n <= i;
i <= m;
end
//運行結果為 m = 2,n = 3,i = 1
- 選擇賦值方式的原則
- 當用 always 塊來描述組合邏輯時,既可以用阻塞賦值,也可以采用非阻塞賦值
- 設計時序邏輯電路,盡量使用非阻塞賦值方式
- 描迷鎖存器(Latch),盡量使用非阻塞賦值
- 若在同一個 always 過程塊中既為組合邏輯建模,又為時序邏輯建模,最好使用非阻塞賦值方式
- 在一個 always 過程中,最好不要混合使用阻塞賦值和非阻塞賦值,雖然同時使用這兩種賦值方式在綜合時並不一定會出錯;對同一個變量,不能既進行阻塞賦值,又進行非阻塞賦值,這樣在綜合時會報錯
- 不能在兩個或兩個以上的 always 過程中對同一個變量賦值,這樣會引發沖突,在綜合時會報錯
任務與函數
在程序設計過程中,設計者經常需要在程序的許多不同地方實現相同的功能,此時可以把這些公共的部分提取出來
,寫成子程序供重復使用
,在需要的位置直接調用子程序
。
Verilog HDI語法中也提供了類似的語法,就是任務和函數,設計者可以把所需的代碼編寫成任務和函數的形式,使代碼更簡潔。
任務
任務的彈性程度比函數大
,在任務中可以調用其他任務或函數,還可以包含延遲,時間控制等語法。
從一個模塊的代碼結構上來講,任務應該和 initial ,always結構同處於一個層次,嚴格來說它屬於行為級建模,所以只要行為級可以使用的語法在任務中都是支持的,這一點要和后面的函數區分。
任務的聲明格式:
task 任務名稱
input [寬度聲明] 輸入信號名;
output [寬度聲明] 輸出信號名;
inout [寬度聲明] 雙向信號名;
reg 任務所用變量聲明;
begin
………… //任務包含語句
end
endtask
針對任務格式的語法要求,依次解釋如下:
(1)任務聲明以task
開始,以endtask
結束,中間部分是任務包含的語句。
(2)任務名稱就是一個標識符
,滿足標識符語法要求即可。
(3)任務可以有輸入信號input 、輸出信號output,雙向信號inout和供本任務使用的變量
,變量不僅包括上面寫出的reg型,行為級中支持的類型如 integer , time等都可以使用。
(4) 任務從整體形式上看和模塊十分相似,task和 endtask類似於module和endmodule,但任務雖然有輸入輸出信號,卻沒有端口列表
。
(5)完成信號和變量的聲明后,可以用begin和end封裝task功能描述語句,也可以使用fork和 join並行塊來封裝,但要注意此語句塊結構前沒有initial和 always結構
。
(6)對於begin…end 所包含的任務語句部分,遵循行為級建模語法即可。
//4位全加器的任務
task add4;
input [3:0] x,y;
input cin;
output [3:0] s;
output cout;
begin
{cout,s}=x+y+cin;
end
endtask
任務調用時應采用如下格式:
任務名(信息對照表);
例如,對add4任務進行調用,就可以使用如下語句:add4(a, b, c, d, e);
任務的調用要注意以下幾點:
(1) 任務調用時要寫出任務調用的名稱來進行調用,這一點與模塊實例化過程相似,但是任務調用不需要使用實例化名稱,像add4這個任務名可直接寫出調用對應任務。
(2) 任務的功能描述雖然和 always、 initial處於同一層次,但是任務調用必須發生在initial , always , task中。
(3)任務中如果有輸入,輸出或雙向信號,按照類似實例化語句中按名稱連接的方式連接信號。
(4)任務的信號連接也要遵循基本的連接要求。
(5)任務調用后需要添加分號,作為行為級語句的一個語句處理。
(6)任務不能實時輸出內部值,而是只能在整個任務結束時得到一個最終的結果,輸出的值也是這個最終結果的值。
函數
函數與任務不同,任務其實沒有太多的語法限制,可以把組合邏輯編寫成任務,也可以使用時序控制等語法來完成任務。
但對函數來說,僅僅可以把組合邏輯編寫成函數,因為函數中不能有任何的時序語句
,而且函數不能調用任務
,這是受函數自身語法要求限制的。
函數的聲明格式:
function 返回值的類型和范圍函數名;
input [端口范圍] 端口聲明;
reg 、 integer等變量聲明;
begin
阻塞賦值語句塊
end
endfunction
函數的基本要求和注意事項如下:
(1)函數以關鍵字
function
開頭,以關鍵字endfunction
結尾。
(2)在關鍵字function
后和函數名稱之間 ,要添加返回值的類型和范圍,定義返回值類型時如果不指定類型,則會默認定義為reg類型,如果沒有指定范圍﹐默認為1位。
(3)函數至少需要一個輸入信號,沒有輸出信號,所以 output之類的聲明是無效的,函數的運算結果就是通過上一步定義的返回值進行傳遞的,也就是說函數只能得到一個運算結果,相當於只有一個輸出。
(4)函數內部可以定義自身所需的變量。
(5)函數的功能語句也可以用begin…end進行封裝。雖然使用fork…join在語法上是允許的,但出於可綜合的角度考慮,一般還是使用順序塊。
(6)函數的 begin…end塊內部有一些要求。首先不能有任何時間相關的語法,如@引導的事件、#引導的延遲等,而且用於時序電路描述的非阻塞語句也不能使用,但if語句、case語句或循環語句等與時序電路沒有直接關系的語句仍然可以使用;其次必須要有語句明確規定函數中的返回值是如何得到賦值的。
具體實例:
//階乘計算函數
function integer factorial;
input [3:0] a;
integer i;
begin
factorial = 1;
for(i=2;i<=a;i=i+1)
factorial = i*factorial;
end
endfunction
函數調用格式如下:
待賦值變量=函數名稱(信號對照表);
函數的調用需要注意以下事項:
(1)函數的調用不像任務調用一樣可以只出現任務名,函數調用之后必須把返回值賦給某個變量。任務有輸出信號,直接通過輸出信號的連接就可以把任務所得的結果進行輸出。而函數沒有直接定義的輸出信號,是通過返回值,采用把函數的返回值賦值給某個變量的形式完成輸出。
(2)信號對照列表部分需要按照函數內部聲明的順序出現。
(3)函數調用也作為行為級建模的一條語句,出現在initial ,always ,task , function結構中,即函數可以被任務調用,但任務不能被函數調用。
Verilog HDL除了可以允許設計者自己編寫任務和函數外,還提供了可以直接使用的系統任務和系統函數。系統任務和函數都以\(作為開頭,如Smonitor、\)finish、$time等,其調用方法和設計者自己編寫的任務和函數完全相同。