1.1 從邏輯閘到 Verilog 硬體描述思維#
學習目標#
讀完本節後,你應該能夠:
- 說明「硬體描述語言(HDL)」與一般程式語言在執行模型上的根本差異。
- 撰寫基本的 Verilog module,正確使用
wire、reg、assign、always。 - 區分 blocking(
=)與 non-blocking(<=)指定,並理解其適用場合。 - 處理位寬(bit width)與向量的宣告、擴展與截斷。
- 說明為什麼本書採用 iverilog / Verilator 等開源工具鏈。
1.1.1 為什麼需要硬體描述語言?#
在數位邏輯課程中,我們用卡諾圖化簡布林函數,再畫出由 AND、OR、NOT 閘組成的電路圖。當電路只有十幾個閘時,畫圖可行;但一顆現代 CPU 有數十億個電晶體,靠手畫電路圖根本不可能。硬體描述語言(Hardware Description Language, HDL)讓我們用文字描述電路的結構與行為,再由工具自動轉換為閘級電路,這個流程稱為:
flowchart LR
A[Verilog RTL 描述] --> B[行為模擬<br/>iverilog / Verilator]
B --> C[綜合 Synthesis<br/>Yosys]
C --> D[布局佈線<br/>nextpnr / 廠商工具]
D --> E[FPGA 實體電路]初學者最常犯的錯誤,是把 Verilog 當成 C 來寫。關鍵差異在於:
| 觀念 | C 語言 | Verilog |
|---|---|---|
| 執行模型 | 逐行循序執行 | 所有電路同時運作(並行) |
| 「變數」 | 記憶體中的一格 | 代表一條導線或一組正反器 |
| 程序 | main 函數依序呼叫 | 模組間以導線連接、事件觸發 |
| 時間 | 由 OS 排程決定 | 有明確的模擬時間與時脈 |
Verilog 描述的不是「演算法的步驟」,而是「一塊電路的長相與行為」。always 區塊看起來像程式碼,但綜合器會把它轉成多工器、加法器或正反器——你寫的是意圖,工具生成的是硬體。
1.1.2 module:硬體世界的基本積木#
Verilog 的設計由一個個 module(模組)組成,就像積木。module 包含埠(port)宣告與內部實作兩部分:
module and_gate (
input wire a, // 1 位元輸入
input wire b,
output wire y
);
assign y = a & b; // 持續指定:y 永遠等於 a AND b
endmodule
assign 稱為連續指定(continuous assignment):右邊任何訊號一改變,左邊立刻(零延遲地)更新。這正是「導線」的物理直覺——導線沒有記憶,只反映來源。
模組可以被層層實例化(instantiation)組成階層式設計:
module half_adder (
input wire a, b,
output wire sum, carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule
1.1.3 wire 與 reg:導線與暫存器#
初學者常問:「reg 是不是暫存器?」答案:不一定。
wire:代表實體導線,必須持續被驅動,只能用assign或模組輸出驅動。reg:是「可以在always區塊中被指定的變數」。它可能綜合成正反器(有時脈),也可能只是組合邏輯(無時脈)。
// reg 作為組合邏輯(沒有時脈)→ 綜合出多工器
always @(*) begin
if (sel) y = a;
else y = b;
end
// reg 作為時序邏輯(有時脈)→ 綜合出 D 型正反器
always @(posedge clk) begin
if (rst) q <= 1'b0;
else q <= d;
end
判斷口訣:always @(posedge clk) 產生正反器;always @(*) 產生組合邏輯。
1.1.4 blocking vs non-blocking:最重要的觀念#
這是 Verilog 初學者的頭號陷阱。兩種指定在 always 區塊內的排程行為不同:
// Blocking(=):依序執行,像 C
always @(*) begin
b = a;
c = b; // c 拿到「新的」b → c = a
end
// Non-blocking(<=):全部同時取「舊值」
always @(posedge clk) begin
b <= a;
c <= b; // c 拿到「舊的」b → 每拍延遲一級 → 兩級正反器
end
non-blocking 版本綜合出的電路是兩個串接的正反器;如果誤用 blocking 寫時序邏輯,會變成一個正反器加上錯誤的資料路徑,模擬結果可能「看起來對」,但硬體行為完全不同。
業界編碼守則(本書全程遵守):
- 時序邏輯(
always @(posedge clk))一律用<=。 - 組合邏輯(
always @(*))一律用=。 - 同一個
always區塊內不要混用兩種指定。 - 不要在多個
always區塊中指定同一個變數。
1.1.5 位寬與向量#
數位硬體天生就是多位元並行的,Verilog 用向量(vector)表示匯流排:
wire [7:0] byte_bus; // 8 位元,bit 7 是 MSB
reg [15:0] counter; // 16 位元暫存器
wire [31:0] a, b, sum; // 32 位元,兩條匯流排
assign sum = a + b; // 32 位元加法器,加法可能有進位溢出
位寬議題是 RTL bug 的最大來源之一:
reg [7:0] a;
reg [3:0] b;
wire [8:0] s;
assign s = a + b; // Verilog 會把運算擴展到最大位寬 9 位元
要注意幾個規則:
- 運算結果的位寬取決於運算元與左值的最大位寬(上下文相關)。
- 想保留進位,就先把運算元擴展:
assign {cout, s} = a + b;對 8 位元加法而言,{cout, s}是 9 位元,加法會在 9 位寬下進行。 - 未指定位寬的數值預設是 32 位元有號數,
1'b0與0意義不同。 - 截斷是靜默發生的:把 9 位元指定給 8 位元訊號,高位直接被丟棄,工具只會給警告。養成把警告當錯誤看的習慣。
另外注意四值邏輯:Verilog 有 0、1、x(未知)與 z(高阻抗)。x 常來自未初始化的 reg 或未驅動的輸入,模擬中出現 x 傳播往往就是 bug 的徵兆。
1.1.6 為什麼選開源工具鏈?#
傳統 IC 設計依賴商業 EDA 工具(Synopsys、Cadence),一套授權動輒數百萬元,學生難以取得。本書選擇全開源路徑:
| 工具 | 角色 | 對應商業工具 |
|---|---|---|
| Icarus Verilog (iverilog) | 事件驅動模擬器,輕量好學 | ModelSim / VCS |
| GTKWave | 波形檢視器 | Verdi |
| Verilator | 把 Verilog 編譯成 C++,模擬速度快百倍 | 商業高速模擬器 |
| Yosys | 邏輯綜合 | Design Compiler |
| nextpnr | FPGA 布局佈線 | Vivado / Quartus |
這些工具足以完成「寫 RTL → 模擬 → 綜合 → 燒錄 FPGA」的完整流程,而且 iverilog 一行指令即可跑第一個模擬:
iverilog -o sim testbench.v alu.v # 編譯
vvp sim # 執行模擬
gtkwave dump.vcd # 看波形
學會開源工具鏈,你同時理解了整個設計流程的本質,未來轉換到任何商業工具都只是換指令而已。
本章小結#
- HDL 描述的是並行的硬體,不是循序的演算法;
assign是導線,always @(posedge clk)是正反器。 wire是導線,reg是 always 區塊內可指定的變數,未必對應暫存器。- 時序用
<=、組合用=,混用是硬體 bug 的最大來源。 - 位寬決定運算行為,截斷靜默發生;把工具警告當錯誤。
- 開源工具鏈(iverilog + GTKWave + Verilator + Yosys)讓任何人都能完成從 RTL 到 FPGA 的完整流程。
實作題#
- 用
assign寫出全加器(full adder),三個輸入a, b, cin、兩個輸出sum, cout,並用兩個半加器模組實例化的方式再做一次。 - 觀察 blocking 陷阱:寫一個模組包含兩個
always @(posedge clk),分別用=與<=做b <= a; c <= b;式的三級移位,跑模擬比較波形差異。 - 寫一個 8 位元加法器,輸出 9 位元結果(含進位),故意把結果指定給 8 位元訊號,觀察編譯警告與錯誤的模擬結果。
- 安裝 iverilog 與 GTKWave,完成
and_gate的模擬,並用 GTKWave 截圖輸入輸出波形。 - 把 1.1.4 的兩級正反器電路擴充為 4 級,畫出你預期的硬體圖,再用模擬驗證。