本章主要講解Verilog語句中的指派部分。
Verilog中的指派包括對線網變量的連續指派和對寄存器變量的過程指派。連續指派用assign語句描述,過程事件用initial和always語句描述,過程指派包含阻塞指派和非阻塞指派兩種運算。
時序控制通過時延控制和事件控制兩種方式實作。時延控制可以分為正常時延與内嵌時延。事件控制主要分為邊沿觸發事件控制與電平敏感事件控制。
文章目錄
- 3.1 Verilog 連續指派
-
- assign 語句
- 連續指派時延
- 全加器
- 3.2 Verilog 過程結構、指派和時序控制
-
- Verilog 過程結構
-
- initial語句
- always 語句
- Verilog 過程指派
-
- 阻塞指派
- 非阻塞指派
- 使用非阻塞指派避免競争冒險
- Verilog 過程時序控制
-
- 時延控制
-
- 正常時延
- 内嵌時延
- 事件控制
-
- 邊沿觸發事件控制
- 電平敏感事件控制
3.1 Verilog 連續指派
assign 語句
連續指派語句是 Verilog 資料流模組化的基本語句,用于對 wire 型變量進行指派。格式如下:
assign LHS_target = RHS_expression ;
LHS(left hand side) 指指派操作的左側,RHS(right hand side)指指派操作的右側。
assign 為關鍵詞,任何已經聲明 wire 變量的連續指派語句都是以 assign 開頭,例如:
wire Cout, A, B ;
assign Cout = A & B ; //實作計算A與B的功能
需要說明的是:
- LHS_target 必須是一個标量或者線型向量,而不能是寄存器類型。
- RHS_expression 的類型沒有要求,可以是标量或線型或存器向量,也可以是函數調用。
- 隻要 RHS_expression 表達式的操作數有事件發生(值的變化)時,RHS_expression 就會立刻重新計算,同時指派給 LHS_target。
Verilog 還提供了另一種對 wire 型指派的簡單方法,即在 wire 型變量聲明的時候同時對其指派。wire 型變量隻能被指派一次,是以該種連續指派方式也隻能有一次。例如下面指派方式和上面的指派例子的指派方式,效果都是一緻的。
wire A, B ;
wire Cout = A & B ;
連續指派時延
連續指派延時語句中的延時,用于控制任意操作數發生變化到語句左端賦予新值之間的時間延時。
時延一般是不可綜合的。
寄存器的時延也是可以控制的,這部分在時序控制裡加以說明。
連續指派時延一般可分為普通指派時延、隐式時延、聲明時延。
下面 3 個例子實作的功能是等效的,分别對應 3 種不同連續指派時延的寫法。
//普通時延,A&B計算結果延時10個時間機關指派給Z
wire Z, A, B ;
assign #10 Z = A & B ;
//隐式時延,聲明一個wire型變量時對其進行包含一定時延的連續指派。
wire A, B;
wire #10 Z = A & B;
//聲明時延,聲明一個wire型變量是指定一個時延。是以對該變量所有的連續指派都會被推遲到指定的時間。除非門級模組化中,一般不推薦使用此類方法模組化。
wire A, B;
wire #10 Z ;
assign Z =A & B
慣性時延
在上述例子中,A 或 B 任意一個變量發生變化,那麼在 Z 得到新的值之前,會有 10 個時間機關的時延。如果在這 10 個時間機關内,即在 Z 擷取新的值之前,A 或 B 任意一個值又發生了變化,那麼計算 Z 的新值時會取 A 或 B 目前的新值。是以稱之為慣性時延,即信号脈沖寬度小于時延時,對輸出沒有影響。
是以仿真時,時延一定要合理設定,防止某些信号不能進行有效的延遲。
對一個有延遲的與門邏輯進行時延仿真。
module time_delay_module(
input ai, bi,
output so_lose, so_get, so_normal);
assign #20 so_lose = ai & bi ;
assign #5 so_get = ai & bi ;
assign so_normal = ai & bi ;
endmodule
testbench 參考如下:
`timescale 1ns/1ns
module test ;
reg ai, bi ;
wire so_lose, so_get, so_normal ;
initial begin
ai = 0 ;
#25 ; ai = 1 ;
#35 ; ai = 0 ; //60ns
#40 ; ai = 1 ; //100ns
#10 ; ai = 0 ; //110ns
end
initial begin
bi = 1 ;
#70 ; bi = 0 ;
#20 ; bi = 1 ;
end
time_delay_module u_wire_delay(
.ai (ai),
.bi (bi),
.so_lose (so_lose),
.so_get (so_get),
.so_normal (so_normal));
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
仿真結果如下:
信号 so_normal 為正常的與邏輯。
由于所有的時延均大于 5ns,是以信号 so_get 的結果為與操作後再延遲 5ns 的結果。
信号 so_lose 前一段是與操作後再延遲 20ns 的結果。
由于信号 ai 第二個高電平持續時間小于 20ns,so_lose 信号會因慣性時延而漏掉對這個脈沖的延時檢測,是以後半段 so_lose 信号仍然為 0。
全加器
下面采用資料流描述方式,來設計一個 1bit 全加器。
設 Ai,Bi,Ci 分别為被加數、加數和相鄰低位的進位數,So, Co 分别為本位和與向相鄰高位的進位數。
真值表如下:
In | Out | |||
---|---|---|---|---|
Ci | Ai | Bi | So | Co |
1 | 1 | |||
1 | 1 | |||
1 | 1 | 1 | ||
1 | 1 | |||
1 | 1 | 1 | ||
1 | 1 | 1 | ||
1 | 1 | 1 | 1 | 1 |
全加器的表達式為:
So = Ai ⊕ Bi ⊕ Ci ;
Co = AiBi + Ci(Ai+Bi)
rtl 代碼(full_adder1.v)如下:
module full_adder1(
input Ai, Bi, Ci,
output So, Co);
assign So = Ai ^ Bi ^ Ci ;
assign Co = (Ai & Bi) | (Ci & (Ai | Bi));
endmodule
當然,更為貼近加法器的代碼描述可以為:
module full_adder1(
input Ai, Bi, Ci
output So, Co);
assign {Co, So} = Ai + Bi + Ci ;
endmodule
testbench(test.sv)參考如下:
`timescale 1ns/1ns
module test ;
reg Ai, Bi, Ci ;
wire So, Co ;
initial begin
{Ai, Bi, Ci} = 3'b0;
forever begin
#10 ;
{Ai, Bi, Ci} = {Ai, Bi, Ci} + 1'b1;
end
end
full_adder1 u_adder(
.Ai (Ai),
.Bi (Bi),
.Ci (Ci),
.So (So),
.Co (Co));
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
仿真結果如下:
3.2 Verilog 過程結構、指派和時序控制
Verilog 過程結構
過程結構語句有 2 種,initial 與 always 語句。它們是行為級模組化的 2 種基本語句。
一個子產品中可以包含多個 initial 和 always 語句,但 2 種語句不能嵌套使用。
這些語句在子產品間并行執行,與其在子產品的前後順序沒有關系。
但是 initial 語句或 always 語句内部可以了解為是順序執行的(非阻塞指派除外)。
每個 initial 語句或 always 語句都會産生一個獨立的控制流,執行時間都是從 0 時刻開始。
initial語句
initial 語句從 0 時刻開始執行,隻執行一次,多個 initial 塊之間是互相獨立的。
如果 initial 塊内包含多個語句,需要使用關鍵字 begin 和 end 組成一個塊語句。
如果 initial 塊内隻要一條語句,關鍵字 begin 和 end 可使用也可不使用。
initial 理論上來講是不可綜合的,多用于初始化、信号檢測等。
對上一節代碼稍作修改,進行仿真,代碼如下。
`timescale 1ns/1ns
module test ;
reg ai, bi ;
initial begin
ai = 0 ;
#25 ; ai = 1 ;
#35 ; ai = 0 ; //absolute 60ns
#40 ; ai = 1 ; //absolute 100ns
#10 ; ai = 0 ; //absolute 110ns
end
initial begin
bi = 1 ;
#70 ; bi = 0 ; //absolute 70ns
#20 ; bi = 1 ; //absolute 90ns
end
//at proper time stop the simulation
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
仿真結果如下:
可以看出,2 個 initial 程序語句分别給信号 ai,bi 指派時,互相間并沒有影響。
信号 ai,bi 的值按照指派順序依次改變,是以 initial 内部語句也可以看做是順序執行。
always 語句
與 initial 語句相反,always 語句是重複執行的。always 語句塊從 0 時刻開始執行其中的行為語句;當執行完最後一條語句後,便再次執行語句塊中的第一條語句,如此循環反複。
由于循環執行的特點,always 語句多用于仿真時鐘的産生,信号行為的檢測等。
下面用 always 産生一個 100MHz 時鐘源,并在 110ns 時停止仿真,代碼如下:
`timescale 1ns/1ns
module test ;
parameter CLK_FREQ = 100 ; //100MHz
parameter CLK_CYCLE = 1e9 / (CLK_FREQ * 1e6) ; //switch to ns
reg clk ;
initial clk = 1'b0 ; //clk is initialized to "0"
always # (CLK_CYCLE/2) clk = ~clk ; //generating a real clock by reversing
always begin
#10;
if ($time >= 1000) begin
$finish ;
end
end
endmodule
仿真結果如下:
可見,時鐘周期是我們想要得到的 100MHz。而且仿真在 110ns 時停止。
Verilog 過程指派
過程性指派是在 initial 或 always 語句塊裡的指派,指派對象是寄存器、整數、實數等類型。這些變量在被指派後,其值将保持不變,直到重新被賦予新值。
連續性指派總是處于激活狀态,任何操作數的改變都會影響表達式的結果;過程指派隻有在語句執行的時候,才會起作用。這是連續性指派與過程性指派的差別。
Verilog 過程指派包括 2 種語句:阻塞指派與非阻塞指派。
阻塞指派
阻塞指派屬于順序執行,即下一條語句執行前,目前語句一定會執行完畢。
阻塞指派語句使用等号 = 作為指派符。
前面的仿真中,initial 裡面的指派語句都是用的阻塞指派。
非阻塞指派
非阻塞指派屬于并行執行語句,即下一條語句的執行和目前語句的執行是同時進行的,它不會阻塞位于同一個語句塊中後面語句的執行。
非阻塞指派語句使用小于等于号 <= 作為指派符。
利用下面代碼,對阻塞、非阻塞指派進行仿真,來說明 2 種過程指派的差別。
`timescale 1ns/1ns
module test ;
reg [3:0] ai, bi ;
reg [3:0] ai2, bi2 ;
reg [3:0] value_blk ;
reg [3:0] value_non ;
reg [3:0] value_non2 ;
initial begin
ai = 4'd1 ; //(1)
bi = 4'd2 ; //(2)
ai2 = 4'd7 ; //(3)
bi2 = 4'd8 ; //(4)
#20 ; //(5)
//non-block-assigment with block-assignment
ai = 4'd3 ; //(6)
bi = 4'd4 ; //(7)
value_blk = ai + bi ; //(8)
value_non <= ai + bi ; //(9)
//non-block-assigment itself
ai2 <= 4'd5 ; //(10)
bi2 <= 4'd6 ; //(11)
value_non2 <= ai2 + bi2 ; //(12)
end
//stop the simulation
always begin
#10 ;
if ($time >= 1000) $finish ;
end
endmodule
仿真結果如下:
語句(1)-(8)都是阻塞指派,按照順序執行。
20ns 之前,信号 ai,bi 值改變。由于過程指派的特點,value_blk = ai + bi 并沒有執行到,是以 20ns 之前,value_blk 值為 X(不确定狀态)。
20ns 之後,信号 ai,bi 值再次改變。執行到 value_blk = ai + bi,信号 value_blk 利用信号 ai,bi 的新值得到計算結果 7。
語句(9)-(12)都是非阻塞指派,并行執行。
首先,(9)-(12)雖然都是并發執行,但是執行順序也是在(8)之後,是以信号 value_non = ai + bi 計算是也會使用信号 ai,bi 的新值,結果為 7。
其次,(10)-(12)是并發執行,是以 value_non2 = ai2 + bi2 計算時,并不關心信号 ai2,bi2 的最新非阻塞指派結果。即 value_non2 計算時使用的是信号 ai2,bi2 的舊值,結果為 4’hF。
使用非阻塞指派避免競争冒險
上述仿真代碼隻是為了讓讀者更好的了解阻塞指派與非阻塞指派的差別。實際 Verilog 代碼設計時,切記不要在一個過程結構中混合使用阻塞指派與非阻塞指派。兩種指派方式混用時,時序不容易控制,很容易得到意外的結果。
更多時候,在設計電路時,always 時序邏輯塊中多用非阻塞指派,always 組合邏輯塊中多用阻塞指派;在仿真電路時,initial 塊中一般多用阻塞指派。
如下所示,為實作在時鐘上升沿交換 2 個寄存器值的功能,在 2 個 always 塊中使用阻塞指派。
因為 2 個 always 塊中的語句是同時進行的,但是 a=b 與 b=a 是無法判定執行順序的,這就造成了競争的局面。
但不管哪個先執行(和編譯器等有關系),不考慮 timing 問題時,他們執行順序總有先後,最後 a 與 b 的值總是相等的。沒有達到交換 2 個寄存器值的效果。
always @(posedge clk) begin
a = b ;
end
always @(posedge clk) begin
b = a;
end
但是,如果在 always 塊中使用非阻塞指派,則可以避免上述競争冒險的情況。
如下所示,2 個 always 塊中語句并行執行,指派操作右端操作數使用的是上一個時鐘周期的舊值,此時 a<=b 與 b<=a 就可以互相不幹擾的執行,達到交換寄存器值的目的。
always @(posedge clk) begin
a <= b ;
end
always @(posedge clk) begin
b <= a;
end
當然,利用下面代碼也可以實作交換寄存器值的功能,但是顯然不如在 always 塊中直接用非阻塞指派簡單直覺。
always @(posedge clk) begin
temp = a ;
a = b ;
b = temp ;
end
Verilog 過程時序控制
Verilog 提供了 2 大類時序控制方法:時延控制和事件控制。
時延控制可以分為正常時延與内嵌時延。事件控制主要分為邊沿觸發事件控制與電平敏感事件控制。
時延控制
基于時延的時序控制出現在表達式中,它指定了語句從開始執行到執行完畢之間的時間間隔。
時延可以是數字、辨別符或者表達式。
根據在表達式中的位置差異,時延控制又可以分為正常時延與内嵌時延。
正常時延
遇到正常延時時,該語句需要等待一定時間,然後将計算結果指派給目标信号。
格式為:#delay procedural_statement,例如:
reg value_test ;
reg value_general ;
#10 value_general = value_test ;
該時延方式的另一種寫法是直接将井号 # 獨立成一個時延執行語句,例如:
#10 ;
value_ single = value_test ;
内嵌時延
遇到内嵌延時時,該語句先将計算結果儲存,然後等待一定的時間後指派給目标信号。
内嵌時延控制加在指派号之後。例如:
reg value_test ;
reg value_embed ;
value_embed = #10 value_test ;
需要說明的是,這 2 種時延控制方式的效果是有所不同的。
當延時語句的指派符号右端是常量時,2 種時延控制都能達到相同的延時指派效果。
當延時語句的指派符号右端是變量時,2 種時延控制可能會産生不同的延時指派效果。
例如下面仿真代碼:
`timescale 1ns/1ns
module test ;
reg value_test ;
reg value_general, value_embed, value_single ;
//signal source
initial begin
value_test = 0 ;
#25 ; value_test = 1 ;
#35 ; value_test = 0 ; //absolute 60ns
#40 ; value_test = 1 ; //absolute 100ns
#10 ; value_test = 0 ; //absolute 110ns
end
//(1)general delay control
initial begin
value_general = 1;
#10 value_general = value_test ; //10ns, value_test=0
#45 value_general = value_test ; //55ns, value_test=1
#30 value_general = value_test ; //85ns, value_test=0
#20 value_general = value_test ; //105ns, value_test=1
end
//(2)embedded delay control
initial begin
value_embed = 1;
value_embed = #10 value_test ; //0ns, value_test=0
value_embed = #45 value_test ; //10ns, value_test=0
value_embed = #30 value_test ; //55ns, value_test=1
value_embed = #20 value_test ; //85ns, value_test=0
end
//(3)single delay control
initial begin
value_single = 1;
#10 ;
value_single = value_test ; //10ns, value_test=0
#45 ;
value_single = value_test ; //55ns, value_test=1
#30 ;
value_single = value_test ; //85ns, value_test=0
#20 ;
value_single = value_test ; //105ns, value_test=1
end
always begin
#10;
if ($time >= 150) begin
$finish ;
end
end
endmodule
仿真結果如下,由圖可知:
- 一般延時的兩種表達方式執行的結果都是一緻的。
- 一般時延指派方式:遇到延遲語句後先延遲一定的時間,然後将目前操作數指派給目标信号,并沒有"慣性延遲"的特點,不會漏掉相對較窄的脈沖。
- 内嵌時延指派方式:遇到延遲語句後,先計算出表達式右端的結果,然後再延遲一定的時間,指派給目标信号。
事件控制
邊沿觸發事件控制
在 Verilog 中,事件是指某一個 reg 或 wire 型變量發生了值的變化。
事件控制用符号 @ 表示。
語句執行的條件是信号的值發生特定的變化。
關鍵字 posedge 指信号發生邊沿正向跳變,negedge 指信号發生負向邊沿跳變,未指明跳變方向時,則 2 種情況的邊沿變化都會觸發相關事件。例如:
//信号clk隻要發生變化,就執行q<=d,雙邊沿D觸發器模型
always @(clk) q <= d ;
//在信号clk上升沿時刻,執行q<=d,正邊沿D觸發器模型
always @(posedge clk) q <= d ;
//在信号clk下降沿時刻,執行q<=d,負邊沿D觸發器模型
always @(negedge clk) q <= d ;
//立刻計算d的值,并在clk上升沿時刻指派給q,不推薦這種寫法
q = @(posedge clk) d ;
當多個信号或事件中任意一個發生變化都能夠觸發語句的執行時,Verilog 中使用"或"表達式來描述這種情況,用關鍵字 or 連接配接多個事件或信号。這些事件或信号組成的清單稱為"敏感清單"。當然,or 也可以用逗号 , 來代替。例如:
//帶有低有效複位端的D觸發器模型
always @(posedge clk or negedge rstn) begin
//always @(posedge clk , negedge rstn) begin
//也可以使用逗号陳列多個事件觸發
if(! rstn)begin
q <= 1'b ;
end
else begin
q <= d ;
end
end
當組合邏輯輸入變量很多時,那麼編寫敏感清單會很繁瑣。此時,更為簡潔的寫法是 @* 或 @(*),表示對語句塊中的所有輸入變量的變化都是敏感的。例如:
always @(*) begin
//always @(a, b, c, d, e, f, g, h, i, j, k, l, m) begin
//兩種寫法等價
assign s = a? b+c : d ? e+f : g ? h+i : j ? k+l : m ;
end
電平敏感事件控制
前面所讨論的事件控制都是需要等待信号值的變化或事件的觸發,使用 @+敏感清單 的方式來表示的。
Verilog 中還支援使用電平作為敏感信号來控制時序,即後面語句的執行需要等待某個條件為真。Verilog 中使用關鍵字 wait 來表示這種電平敏感情況。例如:
initial begin
wait (start_enable) ; //等待 start 信号
forever begin
//start信号使能後,在clk_samp上升沿,對資料進行整合
@(posedge clk_samp) ;
data_buf = {data_if[0], data_if[1]} ;
end
end