轉載自https://www.cnblogs.com/yuphone/archive/2010/11/10/1874465.html
內容
阻塞賦值VS非阻塞賦值
有兩種賦值語句被用在always塊內:阻塞賦值與非阻塞賦值。關於阻塞與非阻塞復制有3條簡單的准則:
- 將電路分為兩部分:寄存器電路和組合電路
- 在寄存器電路中使用非阻塞賦值
- 在組合電路中使用阻塞賦值
1 概覽
阻塞賦值 基本語法如下:
1
|
[var] = [expression];
|
當該條語句被執行時,右手邊的表達式將被賦給左手邊的變量,期間不允許其他語句的干擾。因此,就阻塞了其他語句,直到該條語句執行完畢為止。阻塞賦值的行為與C語言中的變量賦值類似。
非阻塞賦值 基本語法如下:
1
|
[var] <= [expression];
|
非阻塞賦值的行為非常令人難以琢磨。當always塊被激活(在time step的開始),右手邊的表達式被賦初值。當運行到always塊的結尾(即time step的結尾),運算所得的值被賦給左手邊的變量。
以x變量執行非阻塞賦值為例。因為Verilog模型的實際流程比較復雜,我們將非阻塞賦值的行為翻譯成一下幾個步驟:
- 在alway塊的開始,x值傳遞給x_entry;
- 右手邊的變量x的值被x_entry取代;
- 左手邊變量x的值被x_exit取代;
- 在always塊的結束,x_exit的值傳遞給x。
在下面的代碼片段內,上述四個步驟被呈現在代碼的注釋中。
1
2
3
4
5
|
always
@*
begin
// x_entry = x
y <= x & ...
// y = x_entry & ...
x <= ...
// x_exit = ...
end
// x = x_exit
|
范例 為了了解阻塞賦值和非阻塞賦值的區別,我們用三輸入的的電路來做討論。
代碼1 使用阻塞賦值的電路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
module
and_blocking
(
input
a,
input
b,
input
c,
output
reg
y
);
always
@*
begin
y = a;
y = y & b;
y = y & c;
end
endmodule
|
阻塞賦值的欣慰類似於C語言中的順序賦值。y最終得到的值為a & b & c。注意,此代碼僅用於示范,使用順序語義學來描述電路是比較差勁的行為。
下面給出的代碼,其中的阻塞賦值被替換為非阻塞賦值。代碼注釋詳細說明了y的賦值動作。
代碼2 使用非阻塞賦值的電路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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 & c
end
// y = y_exit
endmodule
|
注意always塊內的前2條語句將不會產生任何效果。上述always塊等價與:
1
2
|
always
@*
y <= y & c;
|
2 組合電路
上一個小節的范例屬於極端的情況。除了缺省值,大部分的組合電路並不會多次賦值同一變量。阻塞賦值和非阻塞賦值都可以用於描述同一電路。然而,它們有一些微妙的區別。下面的范例用於解釋這些不同。讓我們以一位同或(異或非)電路為例。我們將詳細列出敏感列表中的變量。
代碼3 使用阻塞賦值的一位同或電路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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;
end
endmodule
|
注意到敏感列表僅包括i0和i1,。當其中之一變化時,always塊被激活,p0、p1和ep被順序運算,ep在第一個time step的結尾被更新。語句的順序非常重要。假設性我們移動最后面的語句到最前面。
1
2
3
4
5
6
|
always
@(i1, i2)
begin
eq = p0 | p1;
p0 = ~i0 & i1;
p1 = i0 & i1;
end
|
在第一條語句中,由於p0和p1還沒有被指定新的值,因此先前被激活的值將會被用到。而先前的值將意味着鎖存器的存在,故此代碼是不正確的。
下面將使用非阻塞賦值替換阻塞賦值。
代碼4 使用非阻塞賦值的一位同或電路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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_entry
end
// 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觸發器:
1
2
|
always
@(posede clk)
q <= d;
|
當然也可以用阻塞賦值來引用D觸發器,如下:
1
2
|
always
@(posede clk)
q = d;
|
雖然在單個D-FF情況下面,上面的代碼工作正常,但是當多個寄存器互相動作的時候,這里就出現許多微妙的問題。
考慮兩個寄存器在每個時鍾周期交換數據。使用阻塞賦值,代碼為:
1
2
3
4
5
|
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的角度看,兩種結果都是有效的。
下面我們修改代碼中的阻塞賦值為非阻塞賦值。
1
2
3
4
5
6
7
8
9
|
always
@(posede clk)
begin
// b_entry = b
a <= b;
// a_exit = b_entry
end
// a = a_exit
always
@(posede clk)
begin
// a_entry = a
b <= a;
// b_exit = a_entry
end
// b = b_exit
|
通過注釋我們看到,因為輸入(entry)值都被用於賦值,所以無論執行的順序,a和b都得到正確的值。
因此為了避免競爭冒險,我們使用非阻塞賦值來引用D-FF和觸發器。
4 混用阻塞和非阻塞賦值的時序電路
在同一個always塊內,有可能混用阻塞賦值和非阻塞賦值。下面我們使用簡單的例程來解釋不同組合的行為,以加強對賦值的理解。
圖4.1 通過混合賦值來引用電路
考慮圖4.1(b)所示的原理圖。當時鍾上升沿來臨之時,a、b與運算所得的值被存入D-FF。基於前面的講解,我們可以將存儲和組合電路分配到兩段代碼中。如代碼4.1所示。
代碼4.1 兩段實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
module
ab_dff_2seg
(
input
clk,
input
a,
input
b,
output
reg
q
);
reg
q_next;
// D-FF
always
@(
posedge
clk)
q <= q_next;
// 組合電路
always
@*
q_next = a & b;
endmodule
|
我們可以變換一下,將兩段組合在一起,使用單個always塊來描述電路。下面通過六次嘗試,來描述阻塞和非阻塞賦值的不同組合的區別。如代碼4.2所示。
代碼4.2 混合賦值例程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
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;
// 嘗試0
always
@(
posedge
clk)
begin
ab0 = a & b;
q0 <= ab0;
end
// 嘗試1
always
@(
posedge
clk)
begin
// ab1_entry = ab1; q1_entry = q1
ab1 <= a & b;
// ab1_exit = a & b
q1 <= ab1;
// q1_exit = ab1_entry
end
// ab1 = ab1_exit; q1 = q1_exit
// 嘗試2
always
@(
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&b
end
// ab4 = ab4_exit; q4 = q4_exite
// 嘗試5(調換嘗試3的順序)
always
@(
posedge
clk)
begin
q5 = ab5;
ab5 = a & b;
end
endmodule
|
在嘗試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移除,合並代碼如下:
1
2
3
4
5
|
// 嘗試0
always
@(
posedge
clk)
begin
q0 <= a & b;
end
|