owned this note
owned this note
Published
Linked with GitHub
# 6. Verilog 函數
[參考網址:Verilog 中文詳細教程](https://www.runoob.com/w3cnote/verilog-tutorial.html)
###### tags: `verilog` `IC 設計`
---
[toc]
---
關鍵詞:函數,大小端轉換,數碼管譯碼
在 Verilog 中,可以利用任務(關鍵字為 task)或函數(關鍵字為 function),將重複性的行為級設計進行提取,並在多個地方調用,來避免重複代碼的多次編寫,使代碼更加的簡潔、易懂。
### :pushpin: 函數
函數只能在模塊中定義,位置任意,並在模塊的任何地方引用,作用範圍也局限於此模塊。函數主要有以下幾個特點:
1)不含有任何延遲、時序或時序控制邏輯
2)至少有一個輸入變量
3)只有一個返回值,且沒有輸出
4)不含有非阻塞賦值語句
5)函數可以調用其他函數,但是不能調用任務 Verilog 函數聲明格式如下:
```verilog=
function [range-1:0] function_id ;
input_declaration ;
other_declaration ;
procedural_statement ;
endfunction
```
函數在聲明時,會隱式的聲明一個寬度為 range、 名字為 function_id 的寄存器變量,函數的返回值通過這個變量進行傳遞。當該寄存器變量沒有指定位寬時,默認位寬為 1。 函數通過指明函數名與輸入變量進行調用。函數結束時,返回值被傳遞到調用處。 函數調用格式如下:
```verilog=
function_id(input1, input2, …);
```
下面用函數實現一個數據大小端轉換的功能。 當輸入為 4'b0011 時,輸出可為 4'b1100。例如:
```verilog=
module endian_rvs
#(parameter N = 4)
(
input en, //enable control
input [N-1:0] a ,
output [N-1:0] b
);
reg [N-1:0] b_temp ;
always @(*) begin
if (en) begin
b_temp = data_rvs(a);
end
else begin
b_temp = 0 ;
end
end
assign b = b_temp ;
//function entity
function [N-1:0] data_rvs ;
input [N-1:0] data_in ;
parameter MASK = 32'h3 ;
integer k ;
begin
for(k=0; k<N; k=k+1) begin
data_rvs[N-k-1] = data_in[k] ;
end
end
endfunction
endmodule
```
函數里的參數也可以改寫,例如:
```verilog=
defparam data_rvs.MASK = 32'd7 ;
```
但是仿真時發現,此種寫法編譯可以通過,仿真結果中,函數里的參數 MASK 實際並沒有改寫成功,仍然為 32'h3。這可能和編譯器有關,有興趣的學者可以用其他 Verilog 編譯器進行下實驗。 函數在聲明時,也可以在函數名後面加一個括號,將 input 聲明包起來。 例如上述大小端聲明函數可以表示為:
```verilog=
function [N-1:0] data_rvs(
input [N-1:0] data_in
......
) ;
```
### :pushpin: 常數函數
常數函數是指在仿真開始之前,在編譯期間就計算出結果為常數的函數。常數函數不允許訪問全局變量或者調用系統函數,但是可以調用另一個常數函數。 這種函數能夠用來引用複雜的值,因此可用來代替常量。 例如下面一個常量函數,可以來計算模塊中地址總線的寬度:
```verilog=
parameter MEM_DEPTH = 256 ;
reg [logb2(MEM_DEPTH)-1: 0] addr ; //可得addr的宽度为8bit
function integer logb2;
input integer depth ;
//256为9bit,我们最终数据应该是8,所以需depth=2时提前停止循环
for(logb2=0; depth>1; logb2=logb2+1) begin
depth = depth >> 1 ;
end
endfunction
```
automatic 函數
在 Verilog 中,一般函數的局部變量是靜態的,即函數的每次調用,函數的局部變量都會使用同一個存儲空間。若某個函數在兩個不同的地方同時並發的調用,那麼兩個函數調用行為同時對同一塊地址進行操作,會導致不確定的函數結果。
Verilog 用關鍵字 automatic 來對函數進行說明,此類函數在調用時是可以自動分配新的內存空間的,也可以理解為是可遞歸的。因此,automatic 函數中聲明的局部變量不能通過層次命名進行訪問,但是 automatic 函數本身可以通過層次名進行調用。
下面用 automatic 函數,實現階乘計算:
```verilog=
wire [31:0] results3 = factorial(4);
function automatic integer factorial ;
input integer data ;
integer i ;
begin
factorial = (data>=2)? data * factorial(data-1) : 1 ;
end
endfunction // factorial
```
下面是加關鍵字 automatic 和不加關鍵字 automatic 的仿真結果。 由圖可知,信號 results3 得到了我們想要的結果,即 4 的階乘。 而信號 results_noauto 值為 1,不是可預知的正常結果,這裡不再做無用分析。

### :pushpin: 數碼管譯碼
上述中涉及的相關函數知識似乎並沒有體現出函數的優越性。下面設計一個 4 位 10 進制的數碼管譯碼器,來說明函數可以簡化代碼的優點。 下圖是一個數碼管的實物圖,可以用來顯示 4 位十進制的數字。在比賽計分、時間計時等方面有著相當廣泛的應用。

每位數碼顯示端有 8 個光亮控制端(如圖中 a-g 所示),可以用來控制顯示數字 0-9 。 而數碼管有 4 個片選(如圖中 1-4),用來控制此時哪一位數碼顯示端應該選通,即應該發光。倘若在很短的時間內,依次對 4 個數碼顯示端進行片選發光,同時在不同片選下給予不同的光亮控制(各對應 4 位十進制數字),那麼在肉眼不能分辨的情況下,就達到了同時顯示 4 位十進制數字的效果。

下面,我們用信號 abcdefg 來控制光亮控制端,用信號 csn 來控製片選,4 位 10 進制的數字個十百千位分別用 4 個 4bit 信號 single_digit, ten_digit, hundred_digit, kilo_digit 來表示,則一個數碼管的顯示設計可以描述如下:
```verilog=
module digital_tube
(
input clk ,
input rstn ,
input en ,
input [3:0] single_digit ,
input [3:0] ten_digit ,
input [3:0] hundred_digit ,
input [3:0] kilo_digit ,
output reg [3:0] csn , //chip select, low-available
output reg [6:0] abcdefg //light control
);
reg [1:0] scan_r ; //scan_ctrl
always @ (posedge clk or negedge rstn) begin
if(!rstn)begin
csn <= 4'b1111;
abcdefg <= 'd0;
scan_r <= 3'd0;
end
else if (en) begin
case(scan_r)
2'd0:begin
scan_r <= 3'd1;
csn <= 4'b0111; //select single digit
abcdefg <= dt_translate(single_digit);
end
2'd1:begin
scan_r <= 3'd2;
csn <= 4'b1011; //select ten digit
abcdefg <= dt_translate(ten_digit);
end
2'd2:begin
scan_r <= 3'd3;
csn <= 4'b1101; //select hundred digit
abcdefg <= dt_translate(hundred_digit);
end
2'd3:begin
scan_r <= 3'd0;
csn <= 4'b1110; //select kilo digit
abcdefg <= dt_translate(kilo_digit);
end
endcase
end
end
/*------------ translate function -------*/
function [6:0] dt_translate;
input [3:0] data;
begin
case(data)
4'd0: dt_translate = 7'b1111110; //number 0 -> 0x7e
4'd1: dt_translate = 7'b0110000; //number 1 -> 0x30
4'd2: dt_translate = 7'b1101101; //number 2 -> 0x6d
4'd3: dt_translate = 7'b1111001; //number 3 -> 0x79
4'd4: dt_translate = 7'b0110011; //number 4 -> 0x33
4'd5: dt_translate = 7'b1011011; //number 5 -> 0x5b
4'd6: dt_translate = 7'b1011111; //number 6 -> 0x5f
4'd7: dt_translate = 7'b1110000; //number 7 -> 0x70
4'd8: dt_translate = 7'b1111111; //number 8 -> 0x7f
4'd9: dt_translate = 7'b1111011; //number 9 -> 0x7b
endcase
end
endfunction
endmodule
```
仿真結果如下。 由圖可知,片選、譯碼等信號,均符合設計。實際中,4 位數字應當在一定的時間內保持不變,而片選信號不停的循環掃描,數碼管才能給肉眼呈現一種靜態顯示的效果。

### :pushpin: 小結
如果譯碼器設計沒有使用函數 dt_translate,則在每個 case 選項裡對信號 abcdefg 進行賦值時,還需要對 single_digit,ten_digit, hundred_digit, kilo_digit 進行判斷。這些判斷語句又會重複 4 次。雖然最後綜合出的實際硬件電路可能是一樣的,但顯然使用函數後的代碼更加的簡潔、易讀。
# 6.2 Verilog 任務
關鍵詞:任務
### :pushpin: 任務與函數的區別
和函數一樣,任務(task)可以用來描述共同的代碼段,並在模塊內任意位置被調用,讓代碼更加的直觀易讀。函數一般用於組合邏輯的各種轉換和計算,而任務更像一個過程,不僅能完成函數的功能,還可以包含時序控制邏輯。下面對任務與函數的區別進行概括:

### :pushpin: 任務 任務聲明
任務在模塊中任意位置定義,並在模塊內任意位置引用,作用範圍也局限於此模塊。 模塊內子程序出現下面任意一個條件時,則必須使用任務而不能使用函數。 1)子程序中包含時序控制邏輯,例如延遲,事件控制等 2)沒有輸入變量 3)沒有輸出或輸出端的數量大於 1 Verilog 任務聲明格式如下:
```verilog=
task task_id ;
port_declaration ;
procedural_statement ;
endtask
```
任務中使用關鍵字 input、output 和 inout 對端口進行聲明。 input 、inout 型端口將變量從任務外部傳遞到內部,output、inout 型端口將任務執行完畢時的結果傳回到外部。 進行任務的邏輯設計時,可以把 input 聲明的端口變量看做 wire 型,把 output 聲明的端口變量看做 reg 型。但是不需要用 reg 對 output 端口再次說明。
對 output 信號賦值時也不要用關鍵字 assign。為避免時序錯亂,建議 output 信號採用阻塞賦值。 例如,一個帶延時的異或功能 task 描述如下:
```verilog=
task xor_oper_iner;
input [N-1:0] numa;
input [N-1:0] numb;
output [N-1:0] numco ;
//output reg [N-1:0] numco ; //无需再注明 reg 类型,虽然注明也可能没错
#3 numco = numa ^ numb ;
//assign #3 numco = numa ^ numb ; //不用assign,因为输出默认是reg
endtask
```
任務在聲明時,也可以在任務名後面加一個括號,將端口聲明包起來。 上述設計可以更改為:
```verilog=
task xor_oper_iner(
input [N-1:0] numa,
input [N-1:0] numb,
output [N-1:0] numco ) ;
#3 numco = numa ^ numb ;
endtask
```
### :pushpin: 任務調用
任務可單獨作為一條語句出現在 initial 或 always 塊中,調用格式如下:
```verilog=
task_id(input1, input2, …,outpu1, output2, …);
```
任務調用時,端口必須按順序對應。 輸入端連接的模塊內信號可以是 wire 型,也可以是 reg 型。輸出端連接的模塊內信號要求一定是 reg 型,這點需要注意。 對上述異或功能的 task 進行一個調用,完成對異或結果的緩存。
```verilog=
module xor_oper
#(parameter N = 4)
(
input clk ,
input rstn ,
input [N-1:0] a ,
input [N-1:0] b ,
output [N-1:0] co );
reg [N-1:0] co_t ;
always @(*) begin //任务调用
xor_oper_iner(a, b, co_t);
end
reg [N-1:0] co_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
co_r <= 'b0 ;
end
else begin
co_r <= co_t ; //数据缓存
end
end
assign co = co_r ;
/*------------ task -------*/
task xor_oper_iner;
input [N-1:0] numa;
input [N-1:0] numb;
output [N-1:0] numco ;
#3 numco = numa ^ numb ; //阻塞赋值,易于控制时序
endtask
endmodule
```
對上述異或功能設計進行簡單的仿真,testbench 描述如下。 激勵部分我們使用簡單的 task 進行描述,激勵看起來就更加的清晰簡潔。 其實,task 最多的應用場景還是應用於 testbench 中進行仿真。 task 在一些編譯器中也不支持綜合。
```verilog=
`timescale 1ns/1ns
module test ;
reg clk, rstn ;
initial begin
rstn = 0 ;
#8 rstn = 1 ;
forever begin
clk = 0 ; # 5;
clk = 1 ; # 5;
end
end
reg [3:0] a, b;
wire [3:0] co ;
initial begin
a = 0 ;
b = 0 ;
sig_input(4'b1111, 4'b1001, a, b);
sig_input(4'b0110, 4'b1001, a, b);
sig_input(4'b1000, 4'b1001, a, b);
end
task sig_input ;
input [3:0] a ;
input [3:0] b ;
output [3:0] ao ;
output [3:0] bo ;
@(posedge clk) ;
ao = a ;
bo = b ;
endtask ; // sig_input
xor_oper u_xor_oper
(
.clk (clk ),
.rstn (rstn ),
.a (a ),
.b (b ),
.co (co ));
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
```
仿真結果如下。 由圖可知,異或輸出邏輯結果正確,相對於輸入有 3ns 的延遲。 且連接信號 a,b,co_t 與任務內部定義的信號 numa,numb,numco 狀態也保持一致。

### :pushpin: 任務操作全局變量
因為任務可以看做是過程性賦值,所以任務的 output 端信號返回時間是在任務中所有語句執行完畢之後。 任務內部變量也只有在任務中可見,如果想具體觀察任務中對變量的操作過程,需要將觀察的變量聲明在模塊之內、任務之外,可謂之"全局變量"。 例如有以下 2 種嘗試利用 task 產生時鐘的描述方式。
```verilog=
//way1 to decirbe clk generating, not work
task clk_rvs_iner ;
output clk_no_rvs ;
# 5 ; clk_no_rvs = 0 ;
# 5 ; clk_no_rvs = 1 ;
endtask
reg clk_test1 ;
always clk_rvs_iner(clk_test1);
//way2: use task to operate global varialbes to generating clk
reg clk_test2 ;
task clk_rvs_global ;
# 5 ; clk_test2 = 0 ;
# 5 ; clk_test2 = 1 ;
endtask // clk_rvs_iner
always clk_rvs_global;
```
仿真結果如下。 第一種描述方式,雖然任務內部變量會有賦值 0 和賦值 1 的過程操作,但中間變化過程並不可見,最後輸出的結果只能是任務內所有語句執行完畢後輸出端信號的最終值。所以信號 clk_test1 值恆為 1,此種方式產生不了時鐘。 第二種描述方式,雖然沒有端口信號,但是直接對"全局變量"進行過程操作,因為該全局變量對模塊是可見的,所以任務內信號翻轉的過程會在信號 clk_test2 中體現出來。

### :pushpin: automatic 任務
和函數一樣,Verilog 中任務調用時的局部變量都是靜態的。可以用關鍵字 automatic 來對任務進行聲明,那麼任務調用時各存儲空間就可以動態分配,每個調用的任務都各自獨立的對自己獨有的地址空間進行操作,而不影響多個相同任務調用時的並發執行。 如果一任務代碼段被 2 處及以上調用,一定要用關鍵字 automatic 聲明。 當沒有使用 automatic 聲明任務時,任務被 2 次調用,可能出現信號間干擾,例如下面代碼描述:
```verilog=
task test_flag ;
input [3:0] cnti ;
input en ;
output [3:0] cnto ;
if (en) cnto = cnti ;
endtask
reg en_cnt ;
reg [3:0] cnt_temp ;
initial begin
en_cnt = 1 ;
cnt_temp = 0 ;
#25 ; en_cnt = 0 ;
end
always #10 cnt_temp = cnt_temp + 1 ;
reg [3:0] cnt1, cnt2 ;
always @(posedge clk) test_flag(2, en_cnt, cnt1); //task(1)
always @(posedge clk) test_flag(cnt_temp, !en_cnt, cnt2);//task(2)
```
仿真結果如下。 en_cnt 為高時,任務 (1) 中信號 en 有效, cnt1 能輸出正確的邏輯值; 此時任務 (2) 中信號 en 是不使能的,所以 cnt2 的值被任務 (1) 驅動的共用變量 cnt_temp 覆蓋。 en_cnt 為低時,任務 (2) 中信號 en 有效,所以任務 (2) 中的信號 cnt2 能輸出正確的邏輯值;而此時信號 cnt1 的值在時鐘的驅動下,一次次被任務 (2) 驅動的共用變量 cnt_temp 覆蓋。
可見,任務在兩次並發調用中,共用存儲空間,導致信號相互間產生了影響。

其他描述不變,只在上述 task 聲明時加入關鍵字 automatic,如下所以。
```verilog=
task automatic test_flag ;
```
此時仿真結果如下。 en_cnt 為高時,任務 (1) 中信號 cnt1 能輸出正確的邏輯值,任務 (2) 中信號 cnt2 的值為 X; en_cnt 為低時,任務 (2) 中信號 cnt2 能輸出正確的邏輯值,任務 (1) 中信號 cnt1 的值為 X; 可見,任務在兩次並發調用中,因為存儲空間相互獨立,信號間並沒有產生影響。

# 6.3 Verilog 狀態機
關鍵詞:狀態機,售賣機
有限狀態機(Finite-State Machine,FSM),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。狀態機不僅是一種電路的描述工具,而且也是一種思想方法,在電路設計的系統級和 RTL 級有著廣泛的應用。
### :pushpin: 狀態機類型
Verilog 中狀態機主要用於同步時序邏輯的設計,能夠在有限個狀態之間按一定要求和規律切換時序電路的狀態。狀態的切換方向不但取決於各個輸入值,還取決於當前所在狀態。狀態機可分為 2 類:Moore 狀態機和 Mealy 狀態機。
### :pushpin: Moore 型狀態機
Moore 型狀態機的輸出只與當前狀態有關,與當前輸入無關。 輸出會在一個完整的時鐘週期內保持穩定,即使此時輸入信號有變化,輸出也不會變化。輸入對輸出的影響要到下一個時鐘週期才能反映出來。這也是 Moore 型狀態機的一個重要特點:輸入與輸出是隔離開來的。

### :pushpin: Mealy 型狀態機
Mealy 型狀態機的輸出,不僅與當前狀態有關,還取決於當前的輸入信號。 Mealy 型狀態機的輸出是在輸入信號變化以後立刻發生變化,且輸入變化可能出現在任何狀態的時鐘週期內。因此,同種邏輯下,Mealy 型狀態機輸出對輸入的響應會比 Moore 型狀態機早一個時鐘週期。

### :pushpin: 狀態機設計流程
根據設計需求畫出狀態轉移圖,確定使用狀態機類型,並標註出各種輸入輸出信號,更有助於編程。一般使用最多的是 Mealy 型 3 段式狀態機,下面用通過設計一個自動售賣機的具體實例來說明狀態機的設計過程。
### :pushpin: 自動售賣機
**自動售賣機的功能描述如下**:
飲料單價 2 元,該售賣機只能接受 0.5 元、1 元的硬幣。考慮找零和出貨。投幣和出貨過程都是一次一次的進行,不會出現一次性投入多幣或一次性出貨多瓶飲料的現象。每一輪售賣機接受投幣、出貨、找零完成後,才能進入到新的自動售賣狀態。
**該售賣機的工作狀態轉移圖如下所示**
包含了輸入、輸出信號狀態。 其中,coin = 1 代表投入了 0.5 元硬幣,coin = 2 代表投入了 1 元硬幣。

### :pushpin: 狀態機設計:3 段式(推薦)
狀態機設計如下:
(0) 首先,根據狀態機的個數確定狀態機編碼。利用編碼給狀態寄存器賦值,代碼可讀性更好。
(1) 狀態機第一段,時序邏輯,非阻塞賦值,傳遞寄存器的狀態。
(2) 狀態機第二段,組合邏輯,阻塞賦值,根據當前狀態和當前輸入,確定下一個狀態機的狀態。
(3) 狀態機第三代,時序邏輯,非阻塞賦值,因為是 Mealy 型狀態機,根據當前狀態和當前輸入,確定輸出信號。
```verilog=
// vending-machine
// 2 yuan for a bottle of drink
// only 2 coins supported: 5 jiao and 1 yuan
// finish the function of selling and changing
module vending_machine_p3 (
input clk ,
input rstn ,
input [1:0] coin , //01 for 0.5 jiao, 10 for 1 yuan
output [1:0] change ,
output sell //output the drink
);
//machine state decode
parameter IDLE = 3'd0 ;
parameter GET05 = 3'd1 ;
parameter GET10 = 3'd2 ;
parameter GET15 = 3'd3 ;
//machine variable
reg [2:0] st_next ;
reg [2:0] st_cur ;
//(1) state transfer
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
st_cur <= 'b0 ;
end
else begin
st_cur <= st_next ;
end
end
//(2) state switch, using block assignment for combination-logic
//all case items need to be displayed completely
always @(*) begin
//st_next = st_cur ;//如果条件选项考虑不全,可以赋初值消除latch
case(st_cur)
IDLE:
case (coin)
2'b01: st_next = GET05 ;
2'b10: st_next = GET10 ;
default: st_next = IDLE ;
endcase
GET05:
case (coin)
2'b01: st_next = GET10 ;
2'b10: st_next = GET15 ;
default: st_next = GET05 ;
endcase
GET10:
case (coin)
2'b01: st_next = GET15 ;
2'b10: st_next = IDLE ;
default: st_next = GET10 ;
endcase
GET15:
case (coin)
2'b01,2'b10:
st_next = IDLE ;
default: st_next = GET15 ;
endcase
default: st_next = IDLE ;
endcase
end
//(3) output logic, using non-block assignment
reg [1:0] change_r ;
reg sell_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
else if ((st_cur == GET15 && coin ==2'h1)
|| (st_cur == GET10 && coin ==2'd2)) begin
change_r <= 2'b0 ;
sell_r <= 1'b1 ;
end
else if (st_cur == GET15 && coin == 2'h2) begin
change_r <= 2'b1 ;
sell_r <= 1'b1 ;
end
else begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
end
assign sell = sell_r ;
assign change = change_r ;
endmodule
```
testbench 設計如下。仿真中模擬了 4 種情景,分別是: case1 對應連續輸入 4 個 5 角硬幣;case2 對應 1 元 - 5 角 - 1 元的投幣順序;case3 對應 5 角 - 1 元 - 5 角的投幣順序;case4 對應連續 3 個 5 角然後一個 1 元的投幣順序。
```verilog=
`timescale 1ns/1ps
module test ;
reg clk;
reg rstn ;
reg [1:0] coin ;
wire [1:0] change ;
wire sell ;
//clock generating
parameter CYCLE_200MHz = 10 ; //
always begin
clk = 0 ; #(CYCLE_200MHz/2) ;
clk = 1 ; #(CYCLE_200MHz/2) ;
end
//motivation generating
reg [9:0] buy_oper ; //store state of the buy operation
initial begin
buy_oper = 'h0 ;
coin = 2'h0 ;
rstn = 1'b0 ;
#8 rstn = 1'b1 ;
@(negedge clk) ;
//case(1) 0.5 -> 0.5 -> 0.5 -> 0.5
#16 ;
buy_oper = 10'b00_0101_0101 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
//case(2) 1 -> 0.5 -> 1, taking change
#16 ;
buy_oper = 10'b00_0010_0110 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
//case(3) 0.5 -> 1 -> 0.5
#16 ;
buy_oper = 10'b00_0001_1001 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
//case(4) 0.5 -> 0.5 -> 0.5 -> 1, taking change
#16 ;
buy_oper = 10'b00_1001_0101 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
end
//(1) mealy state with 3-stage
vending_machine_p3 u_mealy_p3 (
.clk (clk),
.rstn (rstn),
.coin (coin),
.change (change),
.sell (sell)
);
//simulation finish
always begin
#100;
if ($time >= 10000) $finish ;
end
endmodule // test
```
仿真結果如下: 由圖可知,代表出貨動作的信號 sell 都能在投幣完畢後正常的拉高,而代表找零動作的信號 change 也都能根據輸入的硬幣場景輸出正確的是否找零信號。

### :pushpin: 狀態機修改:2 段式
將 3 段式狀態機 2、3 段描述合併,其他部分保持不變,狀態機就變成了 2 段式描述。 修改部分如下:
```verilog=
//(2) state switch, and output logic
//all using block assignment for combination-logic
reg [1:0] change_r ;
reg sell_r ;
always @(*) begin //all case items need to be displayed completely
case(st_cur)
IDLE: begin
change_r = 2'b0 ;
sell_r = 1'b0 ;
case (coin)
2'b01: st_next = GET05 ;
2'b10: st_next = GET10 ;
default: st_next = IDLE ;
endcase // case (coin)
end
GET05: begin
change_r = 2'b0 ;
sell_r = 1'b0 ;
case (coin)
2'b01: st_next = GET10 ;
2'b10: st_next = GET15 ;
default: st_next = GET05 ;
endcase // case (coin)
end
GET10:
case (coin)
2'b01: begin
st_next = GET15 ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
2'b10: begin
st_next = IDLE ;
change_r = 2'b0 ;
sell_r = 1'b1 ;
end
default: begin
st_next = GET10 ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
endcase // case (coin)
GET15:
case (coin)
2'b01: begin
st_next = IDLE ;
change_r = 2'b0 ;
sell_r = 1'b1 ;
end
2'b10: begin
st_next = IDLE ;
change_r = 2'b1 ;
sell_r = 1'b1 ;
end
default: begin
st_next = GET15 ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
endcase
default: begin
st_next = IDLE ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
endcase
end
```
將上述修改的新模塊例化到 3 段式的 testbench 中即可進行仿真,結果如下: 由圖可知,出貨信號 sell 和 找零信號 change 相對於 3 段式狀態機輸出提前了一個時鐘週期,這是因為輸出信號都是阻塞賦值導致的。 如圖中紅色圓圈部分,輸出信號都出現了乾擾脈衝,這是因為輸入信號都是異步的,而且輸出信號是組合邏輯輸出,沒有時鐘驅動。 實際中,如果輸入信號都是與時鐘同步的,這種干擾脈衝是不會出現的。如果是異步輸入信號,首先應當對信號進行同步。

### :pushpin: 狀態機修改:1 段式(慎用)
將 3 段式狀態機 1、 2、3 段描述合併,狀態機就變成了 1 段式描述。 修改部分如下:
```verilog=
//(1) using one state-variable do describe
reg [1:0] change_r ;
reg sell_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
st_cur <= 'b0 ;
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
else begin
case(st_cur)
IDLE: begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
case (coin)
2'b01: st_cur <= GET05 ;
2'b10: st_cur <= GET10 ;
endcase
end
GET05: begin
case (coin)
2'b01: st_cur <= GET10 ;
2'b10: st_cur <= GET15 ;
endcase
end
GET10:
case (coin)
2'b01: st_cur <= GET15 ;
2'b10: begin
st_cur <= IDLE ;
sell_r <= 1'b1 ;
end
endcase
GET15:
case (coin)
2'b01: begin
st_cur <= IDLE ;
sell_r <= 1'b1 ;
end
2'b10: begin
st_cur <= IDLE ;
change_r <= 2'b1 ;
sell_r <= 1'b1 ;
end
endcase
default: begin
st_cur <= IDLE ;
end
endcase // case (st_cur)
end // else: !if(!rstn)
end
```
將上述修改的新模塊例化到 3 段式的 testbench 中即可進行仿真,結果如下: 由圖可知,輸出信號與 3 段式狀態機完全一致。 1 段式狀態機的缺點就是許多種邏輯糅合在一起,不易後期的維護。當狀態機和輸出信號較少時,可以嘗試此種描述方式。

### :pushpin: 狀態機修改:Moore 型
如果使用 Moore 型狀態機描述售賣機的工作流程,那麼還需要再增加 2 個狀態編碼,用以描述 Mealy 狀態機輸出時的輸入信號和狀態機狀態。 3 段式 Moore 型狀態機描述的自動售賣機 Verilog 代碼如下:
```verilog=
module vending_machine_moore (
input clk ,
input rstn ,
input [1:0] coin , //01 for 0.5 jiao, 10 for 1 yuan
output [1:0] change ,
output sell //output the drink
);
//machine state decode
parameter IDLE = 3'd0 ;
parameter GET05 = 3'd1 ;
parameter GET10 = 3'd2 ;
parameter GET15 = 3'd3 ;
// new state for moore state-machine
parameter GET20 = 3'd4 ;
parameter GET25 = 3'd5 ;
//machine variable
reg [2:0] st_next ;
reg [2:0] st_cur ;
//(1) state transfer
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
st_cur <= 'b0 ;
end
else begin
st_cur <= st_next ;
end
end
//(2) state switch, using block assignment for combination-logic
always @(*) begin //all case items need to be displayed completely
case(st_cur)
IDLE:
case (coin)
2'b01: st_next = GET05 ;
2'b10: st_next = GET10 ;
default: st_next = IDLE ;
endcase
GET05:
case (coin)
2'b01: st_next = GET10 ;
2'b10: st_next = GET15 ;
default: st_next = GET05 ;
endcase
GET10:
case (coin)
2'b01: st_next = GET15 ;
2'b10: st_next = GET20 ;
default: st_next = GET10 ;
endcase
GET15:
case (coin)
2'b01: st_next = GET20 ;
2'b10: st_next = GET25 ;
default: st_next = GET15 ;
endcase
GET20: st_next = IDLE ;
GET25: st_next = IDLE ;
default: st_next = IDLE ;
endcase // case (st_cur)
end // always @ (*)
// (3) output logic,
// one cycle delayed when using non-block assignment
reg [1:0] change_r ;
reg sell_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
else if (st_cur == GET20 ) begin
sell_r <= 1'b1 ;
end
else if (st_cur == GET25) begin
change_r <= 2'b1 ;
sell_r <= 1'b1 ;
end
else begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
end
assign sell = sell_r ;
assign change = change_r ;
endmodule
```
將上述修改的 Moore 狀態機例化到 3 段式的 testbench 中即可進行仿真,結果如下: 由圖可知,輸出信號與 Mealy 型 3 段式狀態機相比延遲了一個時鐘週期,這是因為進入到新增加的編碼狀態機時需要一個時鐘週期的時延。此時,輸出再用非阻塞賦值就會導致最終的輸出信號延遲一個時鐘週期。這也屬於 Moore 型狀態機的特點。

輸出信號賦值時,用阻塞賦值,則可以提前一個時鐘週期。 輸出邏輯修改如下。
```verilog=
// (3.2) output logic, using block assignment
reg [1:0] change_r ;
reg sell_r ;
always @(*) begin
change_r = 'b0 ;
sell_r = 'b0 ; //not list all condition, initializing them
if (st_cur == GET20 ) begin
sell_r = 1'b1 ;
end
else if (st_cur == GET25) begin
change_r = 2'b1 ;
sell_r = 1'b1 ;
end
end
```
輸出信號阻塞賦值的仿真結果如下: 由圖可知,輸出信號已經和 3 段式 Mealy 型狀態機一致。

# 6.4 Verilog 競爭與冒險
關鍵字:競爭,冒險,書寫規範
### :pushpin: 產生原因
數字電路中,信號傳輸與狀態變換時都會有一定的延時。 在組合邏輯電路中,不同路徑的輸入信號變化傳輸到同一點門級電路時,在時間上有先有後,這種先後所形成的時間差稱為競爭(Competition)。 由於競爭的存在,輸出信號需要經過一段時間才能達到期望狀態,過渡時間內可能產生瞬間的錯誤輸出,例如尖峰脈衝。這種現像被稱為冒險(Hazard)。 競爭不一定有冒險,但冒險一定會有競爭。
例如,對於給定邏輯 F = A & A',電路如左下圖所示。 由於反相器電路的存在,信號 A' 傳遞到與門輸入端的時間相對於信號 A 會滯後,這就可能導致與門最後的輸出結果 F 會出現干擾脈衝。如右下圖所示。


其實實際硬件電路中,只要門電路各個輸入端延時不同,就有可能產生競爭與冒險。 例如一個簡單的與門,輸入信號源不一定是同一個信號變換所來,由於硬件工藝、其他延遲電路的存在,也可能產生競爭與冒險,如下圖所示。

### :pushpin: 判斷方法
代數法 在邏輯表達式,保持一個變量固定不動,將剩余其他變量用 0 或 1 代替,如果最後邏輯表達式能化簡成
Y = A + A'
或
Y = A · A'
的形式,則可判定此邏輯存在競爭與冒險。 例如邏輯表達式 Y = AB + A'C,在 B=C=1 的情況下,可化簡為 Y = A + A'。顯然,A 狀態的改變,勢必會造成電路存在競爭冒險。
### :pushpin: 卡諾圖法
有兩個相切的卡諾圈,並且相切處沒有其他卡諾圈包圍,可能會出現競爭與冒險現象。 例如左下圖所存在競爭與冒險,右下圖則沒有。

其實,卡諾圖本質上還是對邏輯表達式的一個分析,只是可以進行直觀的判斷。 例如,左上圖邏輯表達式可以簡化為 Y = A'B' + AC,當 B=0 且 C=1 時,此邏輯表達式又可以表示為 Y = A' + A。所以肯定會存在競爭與冒險。
右上圖邏輯表達式可以簡化為 Y = A'B' + AB,顯然 B 無論等於 1 還是 0,此式都不會化簡成 Y = A' + A。所以此邏輯不存在競爭與冒險。
需要注意的是,卡諾圖是首尾相臨的。如下圖所示,雖然看起來兩個卡諾圈並沒有相切,但實際上,m6 與 m4 也是相鄰的,所以下面卡諾圖所代表的數字邏輯也會產生競爭與冒險。

其他較為複雜的情況,可能需要採用 "計算機輔助分析 + 實驗" 的方法。
### :pushpin: 消除方法
對數字電路來說,常見的避免競爭與冒險的方法主要有 4 種。
**1)增加濾波電容,濾除窄脈衝**
此種方法需要在輸出端並聯一個小電容,將尖峰脈衝的幅度削弱至門電路閾值以下。 此方法雖然簡單,但是會增加輸出電壓的翻轉時間,易破壞波形。
**2)修改邏輯,增加冗餘項**
利用卡諾圖,在兩個相切的圓之間,增加一個卡諾圈,並加在邏輯表達式之中。 如下圖所示,對數字邏輯 Y = A'B' + AC 增加冗餘項 B'C,則此電路邏輯可以表示為 Y = A'B' + AC + B'C。此時電路就不會再存在競爭與冒險。

**3)使用時鐘同步電路,利用觸發器進行打拍延遲**
同步電路信號的變化都發生在時鐘邊沿。對於觸發器的 D 輸入端,只要毛刺不出現在時鐘的上升沿並且不滿足數據的建立和保持時間,就不會對系統造成危害,因此可認為 D 觸發器的 D 輸入端對毛刺不敏感。利用此特性,在時鐘邊沿驅動下,對一個組合邏輯信號進行延遲打拍,可消除競爭冒險。
延遲一拍時鐘時,會一定概率的減少競爭冒險的出現。實驗表明,最安全的打拍延遲週期是 3 拍,可有效減少競爭冒險的出現。 當然,最終還是需要根據自己的設計需求,對信號進行合理的打拍延遲。
**為說明對信號進行打拍延遲可以消除競爭冒險,我們建立下面的代碼模型。**
```verilog=
module competition_hazard
(
input clk ,
input rstn ,
input en ,
input din_rvs ,
output reg flag
);
wire condition = din_rvs & en ; //combination logic
always @(posedge clk or negedge !rstn) begin
if (!rstn) begin
flag <= 1'b0 ;
end
else begin
flag <= condition ;
end
end
endmodule
```
testbench 描述如下:
```verilog=
`timescale 1ns/1ns
module test ;
reg clk, rstn ;
reg en ;
reg din_rvs ;
wire flag_safe, flag_dgs ;
//clock and rstn generating
initial begin
rstn = 1'b0 ;
clk = 1'b0 ;
#5 rstn = 1'b1 ;
forever begin
#5 clk = ~clk ;
end
end
initial begin
en = 1'b0 ;
din_rvs = 1'b1 ;
#19 ; en = 1'b1 ;
#1 ; din_rvs = 1'b0 ;
end
competition_hazard u_dgs
(
.clk (clk ),
.rstn (rstn ),
.en (en ),
.din_rvs (din_rvs ),
.flag (flag_dgs ));
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
```
仿真結果如下: 由圖可知,信號 condition 出現了一個尖峰脈衝,這是由於信號 din_rvs 與信號 en 相對於模塊內部時鐘都是異步的,所以到達內部門電路時的延時是不同的,就有可能造成競爭冒險。 雖然最後的仿真結果 flag 一直為 0,似乎是我們想要的結果。但是實際電路中,這個尖峰脈沖在時間上非常靠近時鐘邊沿,就有可能被時鐘採集到而產生錯誤結果。

下面我們對模型進行改進,增加打拍延時的邏輯,如下:
```verilog=
module clap_delay
(
input clk ,
input rstn ,
input en ,
input din_rvs ,
output reg flag
);
reg din_rvs_r ;
reg en_r ;
always @(posedge clk or !rstn) begin
if (!rstn) begin
din_rvs_r <= 1'b0 ;
en_r <= 1'b0 ;
end
else begin
din_rvs_r <= din_rvs ;
en_r <= en ;
end
end
wire condition = din_rvs_r & en_r ;
always @(posedge clk or negedge !rstn) begin
if (!rstn) begin
flag <= 1'b0 ;
end
else begin
flag <= condition ;
end
end // always @ (posedge clk or negedge !rstn)
endmodule
```
將此模塊例化到上述 testbench 中,得到如下仿真結果。 由圖可知,信號 condition 沒有尖峰脈衝的干擾了,仿真結果中 flag 為 0 也如預期。 其實,輸入信號與時鐘邊沿非常接近的情況下,時鐘對輸入信號的採樣也存在不確定性,但是不會出現尖峰脈衝的現象。對輸入信號多打 2 拍,是更好的處理方式,對競爭與冒險有更好的抑製作用。

**4)採用格雷碼計數器**
遞加的多 bit 位計數器,計數值有時候會發生多個 bit 位的跳變。 例如計數器變量 counter 從 5 計數到 6 時, 對應二進制數字為 4'b101 到 4'b110 的轉換。因為各 bit 數據位的延時,counter 的變換過程可能是: 4'b101 -> 4'b111 -> 4'b110。如果有以下邏輯描述,則信號 cout 可能出現短暫的尖峰脈衝,這顯然是與設計相悖的。
```verilog=
cout = counter[3:0] == 4'd7 ;
```
而格雷碼計數器,計數時相鄰的數之間只有一個數據 bit 發生了變化,所以能有效的避免競爭冒險。 好在 Verilog 設計時,計數器大多都是同步設計。即便計數時存在多個 bit 同時翻轉的可能性,但在時鐘驅動的觸發器作用下,只要信號間滿足時序要求,就能消除掉 100% 的競爭與冒險。
### :pushpin: 小結
一般來說,為消除競爭冒險,增加濾波電容和邏輯冗餘,都不是 Verilog 設計所考慮的。 計數採用格雷碼計數器,大多數也是應用在高速時鐘下減少信號翻轉率來降低功耗的場合。 利用觸發器在時鐘同步電路下對異步信號進行打拍延時,是 Verilog 設計中經常用到的方法。 除此之外,為消除競爭冒險,Verilog 編碼時還需要注意一些問題,詳見下一小節。
### :pushpin: Verilog 書寫規範
在編程時多注意以下幾點,也可以避免大多數的競爭與冒險問題。
1)時序電路建模時,用非阻塞賦值。
2)組合邏輯建模時,用阻塞賦值。
3)在同一個 always 塊中建立時序和組合邏輯模型時,用非阻塞賦值。
4)在同一個 always 塊中不要既使用阻塞賦值又使用非阻塞賦值。
5)不要在多個 always 塊中為同一個變量賦值。
6)避免 latch 產生。
下面,對以上註意事項逐條分析。
**1)時序電路建模時,用非阻塞賦值**
前面講述非阻塞賦值時就陳述過,時序電路中非阻塞賦值可以消除競爭冒險。 例如下面代碼描述,由於無法確定 a 與 b 阻塞賦值的操作順序,就有可能帶來競爭冒險。
```verilog=
always @(posedge clk) begin
a = b ;
b = a ;
end
```
而使用非阻塞賦值時,賦值操作是同時進行的,所以就不會帶來競爭冒險,如以下代碼描述。
```verilog=
always @(posedge clk) begin
a <= b ;
b <= a ;
end
```
**2)組合邏輯建模時,用阻塞賦值**
例如,我們想實現 C = A&B, F=C&D 的組合邏輯功能,用非阻塞賦值語句如下。 兩條賦值語句同時賦值,F <= C & D 中使用的是信號 C 的舊值,所以導致此時的邏輯是錯誤的,F 的邏輯值不等於 A&B&D。 而且,此時要求信號 C 具有存儲功能,但不是時鐘驅動,所以 C 可能會被綜合成鎖存器(latch),導致競爭冒險。
```verilog=
always @(*) begin
C <= A & B ;
F <= C & D ;
end
```
對代碼進行如下修改,F = C & D 的操作一定是在 C = A & B 之後,此時 F 的邏輯值等於 A&B&D,符合設計。
```verilog=
always @(*) begin
C = A & B ;
F = C & D ;
end
```
**3)在同一個 always 塊中建立時序和組合邏輯模型時,用非阻塞賦值**
雖然時序電路中可能涉及組合邏輯,但是如果賦值操作使用非阻塞賦值,仍然會導致如規範 1 中所涉及的類似問題。 例如在時鐘驅動下完成一個與門的邏輯功能,代碼參考如下。
```verilog=
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
q <= 1'b0;
end
else begin
q <= a & b; //即便有组合逻辑,也不要写成:q = a & b
end
end
```
**4)在同一個 always 塊中不要既使用阻塞賦值又使用非阻塞賦值**
always 涉及的組合邏輯中,既有阻塞賦值又有非阻塞賦值時,會導致意外的結果,例如下面代碼描述。 此時信號 C 阻塞賦值完畢以後,信號 F 才會被非阻塞賦值,仿真結果可能正確。 但如果 F 信號有其他的負載,F 的最新值並不能馬上傳遞出去,數據有效時間還是在下一個觸發時刻。此時要求 F 具有存儲功能,可能會被綜合成 latch,導致競爭冒險。
```verilog=
always @(*) begin
C = A & B ;
F <= C & D ;
end
```
如下代碼描述,仿真角度看,信號 C 被非阻塞賦值,下一個觸發時刻才會有效。而 F = C & D 雖然是阻塞賦值,但是信號 C 不是阻塞賦值,所以 F 邏輯中使用的還是 C 的舊值。
```verilog=
always @(*) begin
C <= A & B ;
F = C & D ;
end
```
下面分析假如在時序電路里既有阻塞賦值,又有非阻塞賦值會怎樣,代碼如下。 假如復位端與時鐘同步,那麼由於復位導致的信號 q 為 0,是在下一個時鐘週期才有效。 而如果是信號 a 或 b 導致的 q 為 0,則在當期時鐘週期內有效。 如果 q 還有其他負載,就會導致 q 的時序特別混亂,顯然不符合設計需求。
```verilog=
always @(posedge clk or negedge rst_n)
if (!rst_n) begin //假设复位与时钟同步
q <= 1'b0;
end
else begin
q = a & b;
end
end
```
需要說明的是,很多編譯器都支持這麼寫,上述的分析也都是建立在仿真角度上。實際中如果阻塞賦值和非阻塞賦值混合編寫,綜合後的電路時序將是錯亂的,不利於分析調試。
**5)不要在多個 always 塊中為同一個變量賦值**
與 C 語言有所不同,Verilog 中不允許在多個 always 塊中為同一個變量賦值。此時信號擁有多驅動端(Multiple Driver),是禁止的。當然,也不允許 assign 語句為同一個變量進行多次連線賦值。從信號角度來講,多驅動時,同一個信號變量在很短的時間內進行多次不同的賦值結果,就有可能產生競爭冒險。 從語法來講,很多編譯器檢測到多驅動時,也會報 Error。
6)避免 latch 產生
具體分析見下一章:《避免 Latch》。