| C(程序) | Verilog(電路) | |
|---|---|---|
| 執行模型 | 逐行循序執行 | 所有模組/區塊永遠同時運作 |
| 變數 | 記憶體位置 | 導線(wire)或儲存元件(暫存器) |
| if/case | 控制流程 | 多工器(選擇電路) |
| for 迴圈 | 重複執行 | 複製硬體(generate)或僅限模擬 |
| 函式呼叫 | 堆疊跳轉 | 模組實例化(實體電路一份) |
寫 Verilog 時,問自己的問題永遠是:「這段描述會合成出什麼電路?」寫出漂亮但不可合成(或合成出非預期電路)的程式碼,是初學者的第一大坑。
模組、埠與實例化
module mux2 #(
parameter WIDTH = 32 // 參數化位寬
)(
input wire [WIDTH-1:0] a,
input wire [WIDTH-1:0] b,
input wire sel,
output wire [WIDTH-1:0] y
);
assign y = sel ? b : a; // 條件運算子 = 多工器
endmodule
// 實例化(named port connection, 建議寫法)
mux2 #(.WIDTH(32)) u_alusrc_mux (
.a(rs2_data), .b(imm), .sel(alu_src), .y(alu_b)
);模組是硬體的基本封裝單位;實例化一次就是放一份實體電路。第 23 章的 CPU 頂層就是把 ALU、暫存器檔、控制器等模組「接線」起來。
資料型態與常數
wire:導線,必須被連續驅動(assign 或模組輸出);
reg:「在 always 區塊中被指定的訊號」——不一定是正反器!組合 always 中的 reg 仍是純組合邏輯;
四值邏輯:0、1、x(未知)、z(高阻抗);模擬中看到 x 通常代表未初始化或多重驅動;
常數寫法:
32’h00000013(32 位元十六進位)、5’d10、4’b0010;向量切片:
instr[19:15];拼接:{a, b[3:0], 2’b00};複製:{{20{instr[31]}}, instr[31:20]}——這正是第 23 章立即值產生器的符號延伸寫法。
組合邏輯
兩種等價寫法:
// 寫法一: 連續指定 (簡單邏輯)
assign zero = (y == 32'b0);
// 寫法二: always @(*) + case (複雜選擇, 如 ALU)
always @(*) begin
case (alu_op)
4'b0000: y = a + b;
4'b0001: y = a - b;
// ...
default: y = 32'b0; // 必須有 default!
endcase
end(1) 敏感列表用 @(*),讓工具自動推導;(2) 所有分支都要指定所有輸出(或在區塊開頭給預設值)——漏掉任何一種情況,工具會推斷「保持原值」,合成出意外的鎖存器(latch)。第 23 章 control.v 在 always 開頭統一給預設值,就是為了這條。
時序邏輯
reg [31:0] pc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pc <= 32'b0; // 非同步重置
else
pc <= pc_next; // 非阻塞指定 <=
end阻塞(=)與非阻塞(<=)
=(阻塞):立即生效,後面的敘述看得到新值——用於組合 always;<=(非阻塞):右式在時脈邊緣先「取樣」,所有左式同時更新——用於時序 always,正確模擬所有正反器同時打拍的物理事實。
交換兩個暫存器:
always @(posedge clk) begin a <= b; b <= a; end // 正確: 交換
always @(posedge clk) begin a = b; b = a; end // 錯誤: b 得到 b管線暫存器(第 25 章)是一整排這樣的正反器:IF/ID、ID/EX、EX/MEM、MEM/WB 同一邊緣同時更新,全靠非阻塞語意。
記憶體陣列與初始化
reg [31:0] regs [0:31]; // 32 個 32 位元字
reg [31:0] mem [0:1023]; // 4 KiB 指令記憶體
initial $readmemh("program.hex", mem); // 從十六進位檔載入
assign rd1 = (ra1 == 5'd0) ? 32'b0 : regs[ra1]; // 讀埠 (組合)
always @(posedge clk)
if (we && wa != 5'd0) regs[wa] <= wd; // 寫埠 (時序)這就是第 23 章 regfile.v 與 imem.v 的骨架:非同步讀、同步寫、x0 恆零。
測試平台(Testbench)
測試平台是不可合成的模擬程式:產生時脈與重置、施加激勵、檢查結果:
`timescale 1ns/1ps
module tb;
reg clk = 0, rst_n = 0;
always #5 clk = ~clk; // 100 MHz 時脈
top_single_cycle dut (.clk(clk), .rst_n(rst_n), ...);
initial begin
$dumpfile("wave.vcd"); // 波形輸出
$dumpvars(0, tb);
rst_n = 0; repeat (2) @(posedge clk);
rst_n = 1; // 釋放重置
// ... 等待/檢查 ...
if (dut.u_dmem.mem[0] == 32'hC0DE600D) $display("PASS");
$finish;
end
endmodule常用系統任務:$display(印出)、$monitor、$finish、$fatal、$dumpfile/$dumpvars(VCD 波形,GTKWave 檢視)。層級參考 dut.u_dmem.mem[0] 可直接窺探內部訊號——驗證的利器(第 26 章)。
工具流程
iverilog -g2012 -o sim.vvp alu.v regfile.v ... tb.v # 編譯
vvp sim.vvp # 模擬
gtkwave wave.vcd # 看波形本教材使用開源的 Icarus Verilog;業界常用 Verilator(開源、極快)、VCS、Questa。FPGA 合成用 Vivado/Quartus 或開源的 Yosys。
常見陷阱清單
意外鎖存器:組合 always 分支不完整(前述鐵律)。
混用 = 與 <=:時序用 <=、組合用 =,絕不混用。
多重驅動:同一 wire 被兩處 assign,或同一 reg 在兩個 always 中指定——模擬出 x、合成報錯。
組合迴圈:組合訊號繞一圈回到自己(我們在本書開發中就真實踩過:暫存器檔的寫穿旁路在單週期 CPU 形成迴圈——輸出 wb_data 經旁路回到 ALU 輸入再回到 wb_data,模擬器直接卡死。解法:旁路做成參數,只在管線版開啟)。
位寬不符:Verilog 靜默截斷/延伸;比較帶號數要用
$signed()(見 alu.v 的 slt/sra)。用 initial 初始化可合成邏輯:FPGA 可以、ASIC 不行;正式設計一律用重置訊號。
本章重點回顧
Verilog 描述電路:always 區塊全部並行;if/case 是多工器。
組合:always @(*) + =、分支必須完整;時序:posedge clk + <=。
reg 不等於正反器;wire 需連續驅動;x 代表未初始化或衝突。
記憶體=reg 陣列;$readmemh 載入程式;testbench 以層級參考檢查內部狀態。
練習
寫出 4 對 1 多工器的三種寫法:巢狀條件運算子、case、if-else。
下列程式碼會合成出什麼問題?
always @(*) if (en) y = a;如何修正?用 Verilog 寫一個 0–9 循環計數器(同步重置),並寫 testbench 驗證。
解釋為什麼管線暫存器必須用非阻塞指定;若用阻塞會發生什麼?
寫一個 8 位元移位暫存器,並以 $monitor 觀察它十個週期。
9 IEEE Std 1364-2005, IEEE Standard for Verilog Hardware Description Language. S. Harris and D. Harris, Digital Design and Computer Architecture, RISC-V Edition, Morgan Kaufmann, 2021. C. Cummings, “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!”, SNUG 2000.