--- ###### tags : `Books` --- # Verilog HDL ## 使用 Verilog 的基本概念 ### 接線 (Nets) 接線是連接硬體元素之點。接線之最主要的關鍵字為 wire ,接線預設為一個位元,並且預設值是 z (除了 trireg 接線,其預設值為 x) ,接線的值由它的驅動信號而來。若接線沒有驅動信號,則其值為 x 。 :::info 在提及接線的時候,我們可以使用 Net 與 wire ,但是真正在 Verilog 只有 wire 的關鍵字。 在 Verilog 中,高阻抗 z 可以使用 ? 替代。 ::: ### 暫存器 (Registers) 暫存器用來表示資料儲存的元素,除非給定新的數值,否則暫存器內的數值會一直維持。 Verilog 中的暫存器與硬體的暫存器不同,就是一個可以持留 (Hold) 數值的變數。暫存器不用向接線一樣需要驅動,其在模擬過程中可以再任意時間被賦值。 ### 時間 Verilog 的時間是以關鍵字 time 作宣告,其功用是儲存模擬時間 (Simulation Time) ,最少要為 64 bits 的資料。 `$time` 是系統函數,其功能是取得目前的模擬時間,以下為範例 : ```verilog= time save_sim_time; // 定義時間變數 initial save_sim_time = $time; // 儲存目前模擬時間 ``` :::info 在宣告 `reg` 與 `wire` 時, vector 的宣告方式為 : ```verilog type [upper:lower] vector_name; ``` 其中 upper 比 lower 大, lower 也有可能比 upper 大。其最主要以 Endianess 有關。舉例來說,當較低的位元是 LSB 時 (`[3:0]`) 為 little-endian,當較高的位元是 MSB 時 (`[0:3]`) 為 big-endian 。 隨意將 little-endian 賦值給 big-endian 或是反之都是不符合規定的。 ::: :::info 在使用 assign 與 always 中,兩者合成出的電路是相同的,唯獨不一樣的是 assign 只能輸出至 wire ,而 always 則只能輸出至 reg 。 ::: ### 系統任務 (System Tasks) * 資訊顯示 (Displaying information) `$display` 是最有用的系統任務,主要用來顯示變數值或是字串內容。很類似 C 語言中的 `printf` 的功能,不過每個 `$display` 命令後會自動換行,如果 `$display` 後面沒有接任何參數,就會輸出一個空行。以下為一些範例 : ```verilog= $display("Hello Verilog World"); $display($time); reg [0:40] virtual; $display("At time %d virtual address is %h", $time, virtual); ``` * 資訊監視 (Monitoring information) Verilog 有提供一個監控訊號變化的系統任務,只要監控訊號有變化,便會輸出新的值,這一個系統任務便是 `$monitor` 。 `$monitor` 與 `$display` 參數與格式相同,但是 `$monitor` 不僅僅是顯示一次而已,而是每一次信號變化都會顯示一次。 每一次僅允許有一個 `$monitor` 可以執行,如果有多個 `monitor` 的使用,只有最後一個有效,前面的 `monitor` 將不會作用。 :::info 不同的 `$monitor` 工作可以利用 `$monitoron` 與 `$monitoroff` 來做切換。 ::: * 中止 (Stopping) 和完成 (Finishing) 模擬 `$stop` 可以暫時中止模擬的進行,可以用在設計者任何一個想要停的時刻來做觀察除錯的工作。 `finish` 則是結束模擬運算。 :::info Verilog 中也有與 C 語言類似的 define 與 include ,以下為使用的例子 : ```verilog= `include "header.v" // 引入 header.v `define WORD_REG reg [31:0] `WORD_REG reg32; // 透過巨集定義一個 32-bit 暫存器變數 ``` ::: ## 模組與輸出入埠 ### 模組 (Modules) 在 Verilog 中一個檔案可以用有許多的模組,且每個模組在檔案中,並沒有向 C 語言一樣要先有副程式,才有主程式的順序規定,模組宣告的順序可以是任意的,不受限於引用層次的高低。 ### 埠的宣告 (Port Declaration) 在 Verilog 中內定的輸出入埠的宣告種類為 wire ,因此假若在埠的宣告中只有宣告 `output` 、 `input` 或是 `inout` ,則皆將其資料型態視為接線型態 (wire) ,假如需要將訊號的值儲存起來,就要將輸出入埠的種類宣告為 reg 。 注意 `input` 與 `inout` 型態的埠不能被宣告為 reg ,因為 reg 的作用主要在儲存訊號值,然而輸入訊號只是代表外來訊號的情況,所以不應該使用 reg 的型態。 ### 輸出入埠的連接規定 (Port Connection Rules) 下圖為輸出入埠的連接規定 : ![](https://i.imgur.com/Z2xUJos.png) * 輸入 在模組的內部,輸入埠永遠只是一個接線 (net) 。由外部來看,輸入埠的訊號可以接到暫存器 (reg) ,或是一個接線的訊號。 * 輸出 在模組的內部輸出訊號,可以宣告為暫存器或是接線的型態。由外部來看,必須接到一個接線,不可以連接到暫存器型態的訊號。 * 雙向 書向埠不管是在模組內,或外都必須連接到接線的型態。 * 輸出入埠的寬度 在 Verilog 中允許輸出入埠的內外連接寬度不同的訊號,但模擬器應該發出警告的訊號。 * 浮接的輸出入埠 Verilog 允許輸出入埠可以浮接,例如應用來除錯的時候,不需要接到任何的訊號,就可以將它浮接。 ## 邏輯閘層次模型 ### 閘的種類 (Gate Type) Verilog 提供了兩種事先定義好的邏輯模型類別 : And/Or 閘與 Buf/Not 閘。 * And/Or 閘 : 只有一個純量輸出與多個純量輸入,而且閘的第一項為輸出,其餘為輸入。 ```verilog= wire OUT, IN1, IN2; // 基本的取名 and a1(OUT, IN1, IN2); // 輸入多於兩個的及閘 and a2(OUT, IN1, IN2, IN3); // 基本邏輯閘不一定要取別名 and (OUT, IN1, IN2); ``` * Buf/Not 閘 : 只有一個純量輸入與多個純量輸出,且閘的最末項為輸入,其他項皆為輸出。 ```verilog= // 基本的取別名 buf b1(OUT1, IN); // 輸出多於二 buf b1_2out(OUT1, OUT2, IN); // 閘的別名不一定要取 not (OUT1, IN); ``` * Bufif/Notif 閘 : 使用 Buf/Not 閘加上一個控制信號。 ```verilog= // 取別名 bufif 閘, 0 與 1 的差別在於 ctrl 信號在進入前是否會先經過 not 閘 bufif1 b1 (out, in, ctrl); bufif0 b0 (out, in, ctrl); // 取別名 notif 閘 notif1 n1 (out, in, ctrl); notif0 n0 (out, in, ctrl); ``` ### 別名陣列 (Array of Instances) 在新的 Verilog 2001 的標準裡,可以利用別名陣列來做宣告。 ```verilog= wire [7:0] OUT, IN1, IN2; // basic gate instantiations. nand n_gate[7:0](OUT, IN1, IN2); ``` ### 閘的延遲 (Gate Delays) * 上升延遲 : 輸出由其他值上升至 1 所經過的延遲。 * 下降延遲 : 輸出由其他值下降至 0 所經過的延遲。 * 關閉延遲 : 輸出由其他值轉換至 z 的延遲。 ```verilog= // 所有轉換延遲 delay_time 時間 and #(delay_time) a1(out, i1, i2); // 上升和下降延遲的設定 and #(rise_val, fall_val) a2(out, i1, i2); // 上升,下降和關閉延遲的設定 bufif0 #(rise_val, fall_val, turnoff_val) b1(out, in, control); and #(5) a1(out, i1, i2); // 所有轉換延遲 5 單位時間 and #(4, 6) a1(out, i1, i2); // 上升 = 4 、 下降 = 6 bufif0 #(3, 4, 5) b1(out, in, control); // 上升 = 3 、下降 = 4 、關閉 = 5 ``` ## 資料處理模型 ### 持續指定的描述 (Continuous Assignments) 持續指定是在資料處理的層次中,對一條接線指定其邏輯值的最基本用法。持續指定的方式在 Verilog 中使用關鍵字 assign 來描述。其中驅動的強度在 Verilog 的預設值為 strong1 與 strong0 。持續指定的特性有以下幾點 : 1. 在指定敘述的左邊,必須是純量接線 (scalar net) 的單一接線,或是一個向量接線 (vector net) ,也可以是兩者的集合,但不能是一個純量暫存器或是向量暫存器。 2. 持續指定永遠處於活動的狀態,一旦敘述符號右邊的運算元的值發生變化時,其左邊指定那條線的值也會相對隨即改變。 3. 在敘述右邊的運算元,可以是暫存器、接線或是一個函數呼叫。 4. 延遲 (Delay) 是用來控制當指定敘述,需要更動左邊的接線值時,所需經過的延遲時間。 持續指定最主要三種方法,以下一一列出 : * 正規的持續指定 ```verilog= wire out; assign out = in1 & in2; ``` * 隱含式的持續指定 (Implicit Continuous Assignment) 除了前面用關鍵字 assign 的方式外,我們也可以在宣告接線的時候,一併加上持續指定的敘述,效果是一樣的。 ```verilog= // 正規的持續指定 wire out; assign out = in1 & in2; // 隱含式的持續指定,有相同效果 wire out = in1 & in2; ``` * 隱含式的接線宣告 (Implicit Net Declaration) 在對一條未經宣告的訊號使用隱藏式的持續指定時,該訊號會自動被宣告為接線。 ```verilog= // 持續指定 wire i1, i2; assign out = i1 & i2; // 訊號 out 並未被宣告為接線。不過因為隱含持續指定的緣故,模擬器會自動完成這項宣告。 ``` :::info 在撰寫 Verilog 時,是非常不推薦使用 implicit nets 的。以以下為例 : ```verilog wire [2:0] a, c; // Two vectors assign a = 3'b101; // a = 101 assign b = a; // b = 1 implicitly-created wire assign c = b; // c = 001 <-- bug my_module i1 (d,e); // d and e are implicitly one-bit wide if not declared. // This could be a bug if the port was intended to be a vector. ``` implicit nets 是不能為 vector 的,並且只能用已宣告過的 net assign 給未宣告的 net 。因為 c 與 b 皆為未宣告的 net ,因此此種寫法是錯誤的。若想要避免 Verilog 自動幫使用者宣告 implicit net 產生不易除錯的錯誤的話,可以在開頭加上 ```verilog `default_nettype none ``` 避免產生 implicit net 。 ::: ### 延遲 (Delay) 這裡延遲的意思,是指一個 assign 右邊的值發生變化到相對應左邊的值,發生變化所需的時間。最主要有三種方法 : * 正規指定延遲 (Regular Assignment Delay) ```verilog= wire out; assign #10 out = in1 & in2; // 在一個持續指定中描述延遲的方法 ``` 上例中,當 `in1` 或是 `in2` 的值發生變化時,相對應到 `out` 值發生變化所需的時間為十個時間單位。 :::info 需要注意的是,當輸入突波 (Pulse) 的時間寬度小於延遲的時候,則輸入所引發的變化將不會傳到輸出。 ::: * 隱含式連續指定延遲 (Implicit Continuous Assignment Delay) 配合前面所提的隱含式的指定敘述。 ```verilog= // 隱含式持續指定描述延遲的方式 wire #10 out = in1 & in2; // 相同於 wire out; assign #10 out = in1 & in2; ``` * 接線宣告延遲 (Net Declaration Delay) 在宣告一條接線時,指定這條接線的延遲,這條接線的敘述則用關鍵字 assign 來指定。 ```verilog= // 宣告一個 net 變數 out 的延遲時間為 10 個時間單位 wire #10 out; assign out = in1 & in2; // 上面的寫法與下面的寫法有相同的效果 wire out; assign #10 out = in1 & in2; ``` ### 運算子的種類 (Operator Types) #### 算數運算子 (Arithmetic Operators) * 二元運算子 如果任一個運算元,有不確定值 (x) 的位元時,則其結果為不確定值 (x) 。另外在取餘數時,其結果的正負符號需要與第一個運算元相同。 ``` 13 % 3 // 結果為 1 16 % 4 // 結果為 0 -7 % 2 // 結果為 -1 ,正負符號要跟第一個運算元相同 7 % -2 // 結果為 +1 ,正負符號要跟第一個運算元相同 ``` * 一元運算子 要注意的是在 Verilog 中負數皆是以二的補數來表示,因此在 Verilog 中負數的型態,最好是整數或是實數,避免使用 \<sss\>'\<base\>\<nnn\> 型態的負數。 #### 相等運算子 (Equality Operators) 相等運算有兩種 : 邏輯上的相等運算與事件上的相等運算。在比較的時候,是將兩個運算元逐一位元的相互比較,假使兩個相比較的運算元長度不一的時候,較短的那個運算元就填 0 。以下為兩種相等運算子的比較 : | 運算式 | 說明 | 可能傳回值 | |:-------:|:---------------------------------------------------------:|:----------:| | `a==b` | a 跟 b 相等,若是 a 、 b 中有 x 或是 z 的值的話則傳回 x | 0, 1, x | | `a!=b` | a 跟 b 不相等,若是 a 、 b 中有 x 或是 z 的值的話則傳回 x | 0, 1, x | | `a===b` | a 跟 b 相等,包括 x 或是 z 的比較 | 0, 1 | | `a!==b` | a 跟 b 不相等,包括 x 或是 z 的比較 | 0, 1 | #### 位元運算子 (Bitwise Operators) 其運算是將兩個運算元做相對應逐一位元的邏輯運算,假若兩個運算元的長度不一,則較短的那個運算元所欠缺的相對位元部分就補 0 。 #### 化簡運算子 (Reduction Operators) 化簡運算子與位元運算子相同,但是只有一個運算元,其運算是針對單一運算元的所有位元做邏輯上的運算,並輸出一個位元的結果。不同的是他是針對單一運算元從右到左逐一位元的做運算,其中反及、反或、反互斥或是由及、或與互斥或的運算結果取邏輯反運算得到的。 #### 條件運算子 (Conditional Operators) 條件運算子在資料處理模型中,經常用來描述條件指定 (Conditional Assignments) ,其中條件運算式就像是一個開關的控制。 條件運算子也可以用在巢狀的架構。 ```verilog= assign out = (A == 3) ? (control ? x : y) : (control ? m : n); ``` ## 行為模型 ### 結構化程序 (Structured Procedures) 在 Verilog 中有兩個結構化程序 : always 和 initial 這兩個敘述 (Statement) ,在行為模型朱是最基本的敘述,其他行為模式敘述皆必須在這兩個敘述之中。 Verilog 是一個並行程式語言 (Concurrent Programming Language) 而不像 C 語言的本質是循序的,在 Verilog 中,執行流 (Activity Flows) 是並行執行的,而每一個 always 和 initial 敘述代表一個執行流,其皆起始於模擬時間零,且不能有巢狀結構。 #### initial 敘述 一個 initial 區塊啟動於模擬時間零,且執行一次。如果有多個 initial 區塊,將同時啟動於模擬時間零,但各自結束執行。 initial 區塊通常是用在初始化,監控邏輯值的變化,顯示波型與其他需要在整個模擬中,只執行一次的動作。 #### always 敘述 always 區塊起始於模擬時間零,然後以迴圈的形式持續重複執行。若 always 敘述是用來描述一個持續重複工作的數位邏輯電路,如時脈產生器。 ### 程序指定 (Procedural Assignment) 程序指定可用來更新暫存器 (reg) 、整數 (integer) 、實數 (real) 或時間 (time) 變數的值,變數上被指定的值,將被保持到被新的程序指定更新為止,這與持續指定 (Continuous Assignment) 是大不相同的。 #### 阻礙指定 (Blocking Assignments) 阻礙指定敘述會依照其在循環區塊中的位置,依序執行。平行區塊中的阻礙指定,並不會互相限制。阻礙指定的運算符號是 `=` 。 在指定敘述裡,如果等號右邊的位元數,較等號左邊的暫存變數的位元數要多時,較低位元的部分會被存取,多出的較高位元則被捨棄。反之若暫存變數位元數較多,則多出的較高位元數補零。 #### 無阻礙指定 (Nonblocking Assignment) 在循序區塊裡一個無阻礙指定,能安排執行順序不受敘述位置前後的影響,其運算符號為 `<=` 。以以下為例 : ```verilog= reg x, y, z; reg [15:0] reg_a, reg_b; integer count; // 所有行為模型敘述,必須在 initial 或 always 區塊。 initial begin x = 0; y = 1; z = 1; // 純量指定 count = 0; // 指定至整數變數 reg_a = 16'b0; reg_b = reg_a; // 設定向量初始 reg_a[2] <= #15 1'b1; // 在延遲後選擇位元指定 reg_b[15:13] <= #10 {x, y, z}; // 指定連結的結果,至向量部分指定。 count <= count + 1; // 指定至一個整數 (遞增) end ``` 上面的範例中從 `x = 0` 到 `reg_b = reg_a` 是在模擬時間零執行。接著 : 1. `reg_a[2] = 1` 是在第 15 個模擬時間單位執行。 2. `reg_b[15:13] = {x, y, z}` 是在第 10 個模擬時間單位執行。 3. `count = count + 1` 是在第 0 個模擬時間單位執行。 由上述範例可知,模擬器對於數個無阻礙指定敘述的執行時間排程,並不受同一區塊的其他阻礙指定敘述的影響。 :::warning 在實際電路設計的時候,同一區塊混用了阻礙指定與無阻礙指定是相當不好的做法。 ::: * 無阻礙指定的運用 無阻礙指定敘述在描述數位電路設計的行為相當重要,無阻礙指定是運用在共同事件驅動下,數個同時資料轉換發生的情形。以以下為例 : ```verilog= always @(posedge clock) begin reg1 <= #1 in1; reg2 <= @(negedge clock) in2 ^ in3; reg3 <= #1 reg1; // reg1 舊的值 end ``` 上述的例子中,在 clock 正緣觸發後便會讀取等號右邊變數 in1 、 in2 、 in3 與 reg1 之值,並將結果存在模擬器內部。接著依照排定的時間延遲寫入等號左邊變數。因此,即使 reg1 被指定新值的敘述先被執行, reg3 仍會被指定為 reg1 的舊值。 因此 reg1 、 reg2 、 reg3 最後的值,與各個敘述的先後次序無關。 :::info 在數位設計中,在一個共同的事件下,有多個資料同時轉換時,大多使用無阻礙指定,因阻礙指定可能執行的先後次序造成競爭情況。 ::: ### 時序控制 (Timing Controls) 在 Verilog 中有數種時序控制的結構可供使用,時序控制主要是設定某一程序敘述在特定時間被執行。共有三種時序控制的方法可使用 : 延遲基礎時序控制、事件基礎時序控制和位準感測時序控制。 #### 延遲基礎時序控制 (Delay-Based Timing Contorl) * 正規延遲控制 (Regular Delay Control) 在延遲時間非零的時候,使用在程序指定敘述的左邊。 ```verilog= // 定義參數 parameter latency = 20; parameter delta = 2; // 定義暫存器變數 reg x, y, z, p, q; initial begin x = 0; // 無延遲 #10 y = 1; // 使用數值控制延遲,延遲執行 y = 1 10 個時間單位 #latency z = 0; // 使用識別字控制延遲,延遲 20 個時間單位 # (latency + delta) p = 1; // 使用一個運算式控制延遲 #y x = x + 1; // 使用識別字控制延遲,從變數 y 取得延遲值 # (4:5:6) q = 0; // 最大、典型、最小延遲值 end ``` * 指定內部延遲控制 (Intra-Assignment Delay Control) 不同於正規延遲控制,指定內部延遲控制是用在指定程序中,指定運算子 (Assignment Operator) 右邊的延遲時間。這種延遲敘述改變了執行流程 (Flow of Activity) 。 ```verilog= // 定義暫存器變數 reg x, y, z; // 指定內部延遲 initial begin x = 0; z = 0; y = #5 x + z; // 在 time = 0 讀取 x 和 z 的值並執行 x + z ,直到時間單位 5 在指定至 y 。 end // 使用暫時變數與正規延遲得到相同的結果 initial begin x = 0; z = 0; temp_xz = x + z; #5 y = temp_xz; // 在目前的時間讀取 x + z 的值,並儲存至暫時變數。即使時間 0 至 5 間 x 和 z 的值改變了,也不影響單位時間 5 被指定至 y 的值。 end ``` :::info 這兩種延遲控制的不同,在於正規延遲控制延緩整個指定執行,而指定內部延遲控制為先算好指定之等號右邊的運算,延緩把這運算結果特定的時間量,再放至左邊變數。 ::: * 零延遲控制 (Zero delay control) 不同的 always-initial 區塊程序敘述,可能會排在同一個時間點執行,這些在不同區塊程序敘述的實際執行順序是不可確定的。零延遲控制可以確定程序敘述在其他敘述執行完之後才執行。這是為了要消除競爭情況,但是如果有多個零延遲控制敘述,這些敘述的順序仍然是不可確定的。 ```verilog= initial begin x = 0; y = 0; end initial begin #0 x = 1; // 零延遲控制 #0 y = 1; end ``` ### 事件基礎時序控制 (Event-Based Timing Control) #### 正規事件控制 事件控制的符號是 @ ,當信號產生正緣、負緣的轉換,或著數值的改變時,敘述會被執行。以下為範例 : ```verilog= @(clock) q = d; // 當 clock 的值改變, q = d 被執行。 @(posedge clock) q = d; // 當 clock 正緣觸發, q = d 被執行。 @(negedge clock) q = d; // 當 clock 負緣觸發, q = d 被執行。 q = @(posedge clock) d; // d 立刻被執行,等到 clock 正緣觸發在指定至 q 。 ``` :::info 在 Verilog 中,所謂的正緣觸發為訊號從 0 到 (x, z, 1) 或是從 (x, z) 到 1。 所謂的負緣觸發為訊號從 1 到 (0, x, z) 或是從 (x, z) 到 0 。 ::: #### 命名事件控制 Verilog 可以宣告一個事件,然後觸發和識別這個事件,事件並不會包含任何資料。一個被命名的事件是用關鍵字 event 來宣告,並使用符號 `->` 來觸發,一個觸發的事件是用 @ 來識別。 ```verilog= event received_data; // 定義一個事件 recieved_data always @(posedge clock) // 在正緣觸發時作檢查 begin if (last_data_packet) -> received_data; // 觸發事件 recieved_data end always @(received_data) // 直到 received_data 事件被觸發 data_buf = {data_pkt[0], data_pkt[01], data_pkt[2], data_pkt[3]}; ``` #### 事件或控制 當有多個訊號或事件中,任一個都能觸發某個敘述或多敘述區塊執行,就好像這些訊號或事件做或 (OR) 的邏輯運算。 ```verilog= always @(reset or clock or d) // 等待 reset, clock 或 d 的數值改變 begin if (reset) q = 1'b0; else if (clock) q = d; end ``` 除了可以使用 or 這個運算子,也可以利用逗號 `,` 運算子來達到這功能。 :::info 由於觸發的訊號很多時,很容易造成遺漏。因此 Verilog 提供了兩組萬用字元的描述方式 : `@*` 或是 `@(*)` ,來代表所有可能的觸發訊號。 ::: ### 位準感測時序控制 (Level-Sensitive Timing Control) Verilog 也允許位準感測時序控制,亦即等到某一個條件變成真 (true) 值,才執行某個敘述或區塊。 ```verilog= always wait (count_enable) #20 count = count + 1; ``` 以上述為例,若 count_enable 為 0 ,這個敘述將不會被執行。如果是 1 ,敘述將在 20 個單位時間後執行。若 count_enable 停在 1 的位準,則每隔 20 個時間, count 都會加 1 。 ### 多路徑分支 (Multiway Branching) #### case 敘述 (case Statements) 關鍵字為 case 、 endcase 、 default ,其中 default 的敘述是非必要的 (Optional) ,並且 case 敘述 是一個位元一個位元去比較 0 , 1 , x 和 z 。 ```verilog= module demultiplexer1_to_4 (out0, out1, out2, out3, in, s1, s0); // 宣告輸出入埠 output out0, out1, out2, out3; reg out0, out1, out2, out3; input in; input s1, s0; always @(s1 or s0 or in) case ({s1, s0}) // 依控制訊號選擇開關位置。 2'b00: begin out0 = in; out1 = 1'bz; out2 = 1'bz; out3 = 1'bz; end 2'b01: begin out0 = 1'bz; out1 = in; out2 = 1'bz; out3 = 1'bz; end 2'b10: begin out0 = 1'bz; out1 = 1'bz; out2 = in; out3 = 1'bz; end 2'b11: begin out0 = 1'bz; out1 = 1'bz; out2 = 1'bz; out3 = in; end // 如果選擇訊號有 x 則輸出 x , // 如果選擇訊號有 z 則輸出 z , // 如果僅有一個 x ,其他皆為 z ,則 x 有較高的優先順序。 2'bx0, 2'bx1, 2'bxz, 2'bxx, 2'b0x, 2'b1x, 2'bzx: begin out0 = 1'bx; out1 = 1'bx; out2 = 1'bx; out3 = 1'bx; end 2'bz0, 2'bz1, 2'bzz, 2'b0z, 2'b1z: begin out0 = 1'bz; out1 = 1'bz; out2 = 1'bz; out3 = 1'bz; end default: $display("Unspecified control signals"); endcase endmodule ``` 在上述的例子中,有多個輸入的組合如 2'bz0 、 2'bz1 、 2'bzz 、 2'b0z 和 2'b1z 都對應到同一個敘述或區塊,則只需要用逗點 `,` 來分隔。 #### 關鍵字 casex, casez 在 casez 中,不管是在條件運算式或選擇中,所有的 z 值就像隨意值一樣,可用 ? 替代之。在 casex 中,對所有 x 與 z 皆視為隨意值。 因此在 casex 和 casez 中,僅比較非 x 或 z 位置的值。 ```verilog= reg [3:0] encoding; integer state; casex (encoding) // x 表示隨意 (don't care) 值 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 ``` 在上述的例子中,若 encoding = 4'b10xz 將使 next_state = 3 被執行。 ### 迴圈 (Loops) 在 Verilog 中有四種迴圈敘述 : while 、 for 、 repeat 和 forever ,並且所有的迴圈皆僅能在 initial 或 always 的區塊中,其能包含延遲的敘述。 #### for 迴圈 for 迴圈可以用來初始化一個陣列或記憶體。並且 for 迴圈一般使用在有固定起始和結束的迴圈,如果迴圈式在某條件下的簡單迴圈,最好使用 while 迴圈。 #### repeat 迴圈 其關鍵字為 repeat ,其功用為指定一個固定次數的迴圈。 repeat 迴圈必須帶一個數值,可以是一個常數、變數或訊號值,而不能是一個邏輯表示式。若其為變數或訊號值時,僅在迴圈剛開始時才被讀取,在迴圈執行期間並不會再去讀取。 #### forever 迴圈 其關鍵字為 forever , forever 迴圈並未包含任何條件運算式 (Expression) , 會一直執行到遇到 $finish 。 ### 循序和平行區塊 (Sequential and Parallel Blocks) #### 區塊型態 有兩種型態的區塊 : 循序區塊和平行區塊。 * 循序區塊 其關鍵字為 begin 和 end ,具有以下特性 : 1. 每一個敘述會依照其順序執行,且要等到前一個敘述執行完才能開始執行 (除了包含指定內部延遲控制的無阻礙指定) 。 2. 每一個相對延遲控制或事件控制的敘述,其需要等到前一個敘述執行完成,再依相對延遲時間或事件執行。 * 平行區塊 其關鍵字為 fork 和 join ,具以下特性 : 1. 在平行區塊中的所有敘述會同時執行。 2. 在平行區塊中,敘述的執行順序是依照延遲控制與事件控制。 3. 所有時序控制與事件驅動的時間,都為相對於進入區塊的模擬時間。 :::info 所有在平行區塊中的敘述皆執行於區塊啟動時,因此敘述在區塊中的順序是不重要的。 ::: 以下為範例 : ```verilog= // 範例 1 : 包含延遲的平行區塊 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 ``` 在平行區塊中,若在同一個時間有兩個敘述影響到同一個變數,會有產生競爭情況的可能。其中以下方的例子,因為 x 與 y 的賦值不一定會優先於 z 與 w 的賦值,因此 z 與 w 的值也有可能皆為 `2'bxx` ,一切皆已模擬器的設計為主。 #### 區塊的特殊特性 (Special Features of Blocks) 區塊敘述的特殊功能主要有三種 : 巢狀區塊 、 命名區塊與禁能命名區塊。 * 巢狀區塊 (Nested blocks) 區塊可以巢狀使用。循序區塊和並行區塊是可以合在一起的。 ```verilog= // 巢狀區塊 initial begin x = 1'b0; fork #5 y = 1'b1; #10 z = {x, y}; join #20 w = {y, x}; end ``` * 命名區塊 (Named blocks) 區塊是可以命名的 : 1. 命名區塊中可以宣告區域變數。 2. 命名區塊是設計階層的一部分,在命名區塊中的變數可用階層化命名參照存取 (Hierarchical Naming Reference) 。 3. 可以禁能一個命名區塊,也就是停止這區塊的執行。 ```verilog= // 命名區塊 module top; initial begin: block1 // 循序區塊命名為 block1 integer i; // 整數 i 是區塊中 block1 的靜態區塊區域變數 ... end initial fork: block2 // 平行區塊命名為 block2 reg i; // 暫存器 i 是 block2 的靜態區域變數 ... join endmodule ``` * 禁能命名區塊 (Disabling Named Blocks) 關鍵字 disable 提供一個方法來結束一個命名區塊的執行。其功能與 C 語言中的 break 敘述類似,不同的是 break 僅能中斷正在執行的迴圈,而 disable 可以停止命名區塊的執行。 ```verilog= // 說明 : 尋找 flag (向量變數) 中第一個 1 的位元 reg [15:0] flag; integer i; // 整數持續計數 initial begin flag = 16'b 0010_0000_0000_0000; i = 0; begin : block1 // 將 while 迴圈內的主要區塊命名為 block1 while (i < 16) begin if (flag[i]) begin $display("Encountered a TRUE bit at element number %d", i); disable block1; // 因發現 "1" 位元所以禁能 block1 end i = i + 1; end end end ``` #### 產生區塊 (Generate Blocks) 這一節中,我們將介紹如何利用產生區塊的方式,在模擬開始之前動態產生所需的區塊。這樣的產生方式,很適合開發參數化的設計模型,針對多位元的訊號所需的重複動作,可以產生出對應的模組。產生描述由關鍵字 generate 開始,到 endgenerate 結束。 * 迴圈化產生 (Generate Loop) 一個迴圈化產生可以在一個迴圈內初始化數個底下列出的資料型態 : 1. 變數定義 2. 模組 3. 使用者自訂原生邏輯閘,或是基本邏輯閘 4. 連續指定 5. initial 與 always 區塊 以下為範例 : ```verilog= // 將兩個 N 位元的匯流排資料做互斥運算 module bitwise_xor(out, i0, i1); // 參數定義,可以隨後修改 parameter N = 32; // 32 位元匯流排 // 宣告輸出入埠 output [N-1:0] out; input [N-1:0] i0, i1; // 宣告一個暫存的變數,用來做產生的指標,並不會在模擬過程中真正被使用到 genvar j; // 產生逐位元的 xor 運算 generate for (j = 0; j < N; j = j + 1) begin: xor_loop xor g1 (out[j], i0[j], i1[j]); end // 結束迴圈化產生 endgenerate endmodule ``` * 條件化產生 (Generate Conditional) ```verilog= // 參數化乘法器 module multiplier (product, a0, a1); // 宣告輸入向量寬度,可以被重新定義 parameter a0_width = 8; // 八位元 parameter a1_width = 8; // 八位元 // 區域參數宣告 // 這部分的參數,不可以被 defparam 來重新定義 localparam product_width = a0_width + a1_width; // 宣告輸出入埠 output [product_width-1:0] product; input [a0_width-1:0] a0; input [a1_width-1:0] a1; // 依照位元寬度來決定使用哪一種乘法器 generate if (a0_width < 8) || (a1_width < 8) cla_multiplier #(a0_width, a1_width) m0(product, a0, a1); else tree_multiplier #(a0_width, a1_width) m0(product, a0, a1); endgenerate endmodule ``` * 組合條件化產生 (Generate Case) ```verilog= // N 位元加法器 module adder(co, sum, a0, a1, ci); // 參數宣告,可以重新定義 parameter N = 4; // 四位元 // 宣告輸出入埠 output [N-1:0] sum; output co; input [N-1:0] a0, a1; input ci; // 依照輸入向量的位元寬度來決定該使用哪一種加法器 generate case (N) // Special cases for 1 and 2 bit adders 1: adder_1bit adder1(c0, sum, a0, a1, ci); // 一位元的加法器 2: adder_2bit adder2(c0, sum, a0, a1, ci); // 二位元的加法器 // 如果都不是,就採用 N 位元進位預算式加法器 default: adder_cla #(N) adder3(c0, sum, a0, a1, ci); endcase endgenerate endmodule ``` ## 任務與函數 ### 任務與函數的不同之處 | 函數 | 任務 | |:-------------------------------------------------------:|:-----------------------------------------------------------:| | 一個函數可以引用其他的函數,但不能引用其他的任務 | 一個任務可以引用其他的任務與函數 | | 函數永遠在時間等於零的時候開始執行 | 任務可以不從時間等於零的時候開始執行 | | 函數不能包含有延遲、事件或是控制時間的任何陳述 | 任務可以包含有延遲、事件與時間控制的陳述 | | 函數至少要有一個 input 的宣告 | 任務可以擁有零個或是更多個 input 、 output 或是 inout | | 函數永遠回傳單一個值,並且不能有 output 與 inout 的宣告 | 任務並沒有傳回的值,但可以藉由 output 、 inout 將值輸出出來 | ### 任務 以關鍵字 task 與 endtask 宣告,適用於以下情況 : 1. 需要用到延遲、時間或是事件控制指令的時候。 2. 需要有零個或是多於一個輸出的時候。 3. 沒有輸入的時候。 * 以下為範例 : ```verilog module operation; ... always @(A or B) begin bitwise(AB_AND, AB_OR, AB_XOR, A, B); end ... task bitwise_oper; output [15:0] ab_and, ab_or, ab_xor; input [15:0] a, b; begin #delay ab_and = a & b; ab_or = a | b; ab_xor = a ^ b; end endtask endmodule ``` * 以下為沒有輸入的範例 : ```verilog= module sequence; ... regclock; ... initial init_sequence; // 呼叫任務 init_sequence ... always begin asymmetric_sequence; // 呼叫任務 asymmetric_sequence end ... ... task init_sequence; begin clock = 1'b0; end endtask task asymmetric_sequence; begin #12 clock = 1'b0; #5 clock = 1'b1; #3 clock = 1'b0; #10 clock = 1'b1; end endtask endmodule ``` ### 函數 (Functions) 使用關鍵字 function 與 endfunction,適用於以下情況 : 1. 程序傳回一個值時。 2. 至少有一個傳入參數的時候。 3. 沒有輸出或是輸出入埠。 4. 沒有阻礙程序 在 Verilog 中,當一個函數宣告的時候, Verilog 同時也內定的宣告了一個以函數的名稱為名的暫存器,當函數執行完畢,函數的輸出則經由這個暫存器傳回到我們呼叫函數的地方。需要注意的是,函數至少要有一個輸入,且沒有輸出,因為回傳值是透過內定宣告的暫存器來傳回,還有在函數中不可以呼叫其他的任務,但是可以呼叫其他的函數來使用。 * 以下為範例 : ```verilog= module parity; ... reg [31:0] addr; reg parity; always @(addr) begin parity = calc_parity(addr); $display("Parity calculated = %b", calc_parity(addr)); end ... function calc_parity; input [31:0] address; begin calc_parity = ^address; end endfunction ... endmodule ``` ## 有用的程式技巧 ### 程序持續指定 (Prodedural Continuous Assignments) 程序持續指定可以使用有限週期的指定方式,指定數值到暫存器或著線路,程序持續指定複寫 (override) 任一存在之指定。 #### assign 和 deassign 使用關鍵字 assign 與 deassign ,一般運用在週期控制。 ```verilog= // 非同步設定負緣觸發 D 型正反器 module edge_dff(q, qbar, d, clk, reset); // 輸出輸入 output q, qbar; input d, clk, reset; reg q, qbar; // 宣告 q 和 qbar 是暫存器變數 always @(negedge clk) // 在 clock 負緣指定 q 和 qbar 的值 begin q = d; qbar = ~d; end always @(reset) // 當 reset 為高電位使用程序持續指定複寫 q 和 qbar if (reset) begin // 如果 reset 為高電位,使用程序持續指定複寫 q 和 qbar assign q = 1'b0; assign qbar = 1'b1; end else begin // 如果 reset 為低電位,移除複寫 // 在移除複寫後,q = d and qbar = ~d 將要等到下一個 clock 負緣才能更改暫存器變數的值 deassign q; deassign qbar; end endmodule ``` 在上述的範例中,當 reset 信號為 high 時,q 和 qbar 的指定被複寫 (override) 一指定新值。且這暫存器變數在 deassign 後,直到再一次程序指定才會變更內值。 #### force 和 release 使用關鍵字 force 和 release ,可以複寫在暫存器和接點。建議 force 和 release 不要使用在設計區塊中,最好僅使用在模擬和除錯中。 * 暫存器的 force 和 release force 一個暫存器,則不管任何一種的程序指定或程序持續指定,這一個暫存器將被複寫,直到 release 為止。以下為範例 : ```verilog= module stimulus ... ... // 取別名 D 型正反器 edge_dff dff(Q, Qbar, D, CLK, RESET); ... ... initial begin // 在模擬時間 50 至 100 強制 dff.q 的值 #50 force dff.q = 1'b1; // 強制 q 的值為 1 在模擬時間 50 #50 release dff.q; // 解強制 q 的值在模擬時間 100 end ... ... endmodule ``` * 接點的 force 和 release force 一個接點,則任何程序持續指定將被複寫,直到 release 為止,其可為表示式或數值。當接點被 release 時,馬上還原為原來的驅動值。以下為範例 : ```verilog= module top; ... .,.. assign out = a & b & c; // 持續指定 out ... initial #50 force out = a | b & c; #50 release out; end ... ... endmodule ``` 在上述的例子中,當這個 force 敘述被啟動時,不管是 a 、 b 或 c 有改變,這一個表示式皆會重算。 ### 複寫參數 (Overriding Parameters) #### 模組別名參數指定 若在一個模組中有多個參數,只要依照參數的順序指定,就可以在此模組中給定這些參數新的數值。若未指明覆蓋的數值,則使用預設的參數值。以下為範例 : ```verilog= // 定義包含延遲的模組 module bus_master; parameter var1 = 2; parameter var2 = 3; parameter var3 = 7; ... // <模組內部> ... endmodule // 最高層次模組,取兩個 bus_master 模組的別名 module top; // 取具有新的延遲數值的模組別名 bus_master #(4, 5, 6) b1(); // b1: var1 = 4, var2 = 5, var3 = 6 bus_master #(9, 4) b2(); // b2: var1 = 9, var2 = 4, var3 = 7 (default) // 依照列表順序指定參數值 bus_master #(.var2(4), var3(7)) b3(); // b3: var2 = 4, var3 = 7, var1 = 2 (default) endmodule ``` 在上述的例子中,建議使用指定名稱的方式設定參數值,如此可以將錯誤的機會減到最少,而且增加或減少參數的時候,可以不需擔心改變他們的順序。 ### 有條件的編譯與執行 (Conditional Compilation and Execution) #### 有條件的編譯 有條件的編譯是由編譯指令 (compiler directives) : `` `ifdef`` 、 `` `ifndef`` 、 `` `else`` 、 `` `elsif`` 、 `` `endif`` 來完成。 在 Verilog 檔案中可以使用 `` `define`` 敘述,來設定條件編譯的旗標。需要注意的是, `` `ifdef`` 敘述中不支援布林運算式,例如 `TEST && ADD_B2` 。 #### 有條件的執行 在系統任務中用來處理條件執行的關鍵字為 `$test$plusargs` ,以下為範例 : ```verilog= // 條件執行 module test; ... ... initial begin if($test$plusargs("DISPLAY_VAR")) $display("Display = %b", {a, b, c}); // 如果 flag 被設定則顯示 else $display("No Display"); // 反之,則不顯示 end endmodule ``` 如果執行的時候設定了 DISPLAY_VAR 旗標,則這些變數將會被顯示出來。旗標設定的方法,是在執行的時候,指定執行選項 `+DISPLAY_VAR` 。條件執行可以由系統任務 `$value$plusargs` ,做更進一步的控制。這個系統任務允許測試旗標的設定,是否帶數值。如果找不到吻合的旗標,`$value$plusargs` 傳回 0 ,若找到吻合的旗標則傳回非零的值。以下說明 `$value$plusargs` 的用法 : ```verilog= // 使用 $value$plusargs 的條件執行 module test; reg [8*128-1:0] test_string; integer clk_period; ... ... initial begin if($value$plusargs("testname=%s", test_string)) $readmemh(test_string, vectors); // 讀取測試向量 else // 否則顯示錯誤訊息 $display("Test name option not specified"); if($value$plusargs("clk_t=%d", clk_period)) forever #(clk_period/2) clk = ~clk; // 設定時脈 else // 否則顯示錯誤訊息 $display("Clock period option name not specified"); end // 在這個例子中,若要利用上述的選項,可以在執行模擬器的時候加上下列選項。 // +testname=test1.vec +clk_t=10 // 如此測試檔名 = "test1.vec" 而且 clk+period = 10 endmodule ``` ### 時間刻度 (Time Scales) 語法 : `` `timescale<reference_time_unit>/<time_precision>`` 其中 <reference_time_unit> 是時間與延遲的單位大小, <time_precision> 則指定其精確度。只有 1 、 10 、 100 是其合法的整數值。 ### 檔案輸出 (File Output) 系統任務 `$fopen` 可開啟一個檔案。 任務 `$fopen` 將傳回 32 位元的值叫做多通道描述符號 (multichannel descriptor) ,且在多通道描述符號中僅有一個位元會被設定。標準輸出的多通道描述符號的最末一個位元被設定 (bit 0) ,所以,標準輸出叫做通道 0 ,$fopen 所新開的通道,依其傳回多通道描述符號,依序位元 1 、 位元 2 ... 至位元 31 被設定,分別為通道 1 、 通道 2 ... 、通道 31 。 ```verilog= // 多通道描述符號 integer handle1, handle2, handle3; // integers 是 32-bit 值 // 標準輸出開啟時, descriptor = 32'h0000_0001 (bit 0 set)。 initial begin handle1 = $fopen("file1.out"); // handle1 = 32'h0000_0002 (bit 1 set) handle2 = $fopen("file2.out"); // handle2 = 32'h0000_0004 (bit 2 set) handle3 = $fopen("file3.out"); // handle3 = 32'h0000_0008 (bit 3 set) end ``` #### 寫至檔案 (Writing to file) 系統任務 `$fdisplay` 、 `$monitor` 是用來寫至檔案的。 語法 : `$fdisplay(<file_descriptor>, p1, p2..., pn);`    `$fmonitor(<file_descriptor>, p1, p2..., pn);` p1 、 p2 、 ... 、 pn 可以是變數、信號名,或引號內的字串。file_descriptor 是一個多通道描述符號, Verilog 將輸出寫至那些相對被設置為 1 的檔案。 ```verilog= // 寫至檔案 integer desc1, desc2, desc3; // 三個多通道描述符號 initial begin desc1 = handle1 | 1; // desc1 = 32'h0000_0003; $fdisplay(desc1, "Display 1"); // 寫至檔案 file1.out 和標準輸出 desc2 = handle2 | handle1; // desc2 = 32'h0000_0006; $fdisplay(desc2, "Display 2"); // 寫至檔案 file1.out 和 file2.out desc3 = handle3; // desc3 = 32'h0000_0008; $fdisplay(desc3, "Display 3"); // 寫至檔案 file3.out ``` #### 關閉檔案 (Closing files) 語法 : `$fclose(handle1);`