05.5 參數化模組:param 與 generic#
從 parameter 到 text macro 地獄#
可重用設計的需求比 HDL 更早出現:同一個 adder 要服務 8 位元與 32 位元、同一個 FIFO 要服務不同深度。Verilog-1995 的解法是 parameter:模組宣告參數,實例化時以 #(...) 覆寫。這個機制在 1990 年代推動了 parameterized IP 的商業模式,但很快工程師就撞到它的天花板:
- parameter 只能是數值,不能是型別(你想說「這個模組的資料是 32 位元 signed」,得另外用一個 signed flag parameter)。
- 複雜的編譯期計算得靠
`definetext macro,而 text macro 沒有作用域、沒有型別、沒有檢查。2000 年代的大型專案中,`include與`define的層層疊套(macro 地獄)是出了名的維護災難:一個 macro 定義在不同檔案的 include 順序下有不同意義,錯誤訊息指向完全不相干的地方。 - SystemVerilog 加強了 parameter 與
type parameter(parameter type T = logic),方向正確,但語法冗長,且 type parameter 的使用體驗依然像「半個巨集」。
Veryl 的設計選擇是:泛型(generics)是語言層級的一等公民,型別安全由編譯器保證,而不是文字替換。本章統一採用以下語法形式。
兩種參數化:param 與 generic#
Veryl 中有兩條參數化路徑。第一種是模組內的 param(與 Verilog parameter 對應,出現在 port list 之前):
module Adder #(
param W: u32 = 8,
) (
a: input logic<W>,
b: input logic<W>,
y: output logic<W>,
) {
assign y = a + b;
}
實例化時以 #(...) 傳入:
inst u: Adder #(16) (bus);
第二種是 generics:把參數寫在模組名稱後的 ::<...> 中,型別也可以參數化:
module Reg::<W: u32> (
clk : input clock,
rst : input reset,
din : input logic<W>,
dout : output logic<W>,
) {
var r_q: logic<W>;
always_ff (clk, rst) {
if_reset {
r_q = 0;
} else {
r_q = din;
}
}
}
實例化語法:
inst r0: Reg::<8>;
inst r1: Reg::<32>;
本書的 generics 一律採用這種 ::<...> 形式;數值型的簡單覆寫則可視情況用 param + #(),兩者語意在專案中保持一致即可。
為什麼「語言層級泛型」重要#
Verilog 的 #() 與 text macro 的本質都是「編譯前的文字操作」。Veryl 的 generics 是真正的型別化參數:W 在模組內是 u32 型的常數,參與寬度運算(logic<W>、logic<W+1>)時的型別錯誤會被檢查。例如把 logic<W> 的訊號接到 logic<W*2> 的埠上,編譯器會報 width mismatch;用 text macro 寫同一件事,你只會得到展開後難以閱讀的錯誤訊息,或更糟——悄悄地截位。
這也是 Rust 社群那套「make invalid states unrepresentable」哲學的延伸:讓錯誤的接法在編譯期不成立,而不是靠 testbench 事後發現。
generics 與 param 的分工#
經驗法則:
param:對外可調的設計參數(深度、寬度、閾值),會出現在合成報告與文件中。::<...>generics:需要參數化「型別或結構」的場合,例如一個 generic register file 的資料寬度、或未來需要以型別為參數的元件。
兩者可以並用:模組同時有 generics 與 param,實例化時分別以 ::<...> 與 #(...) 給值。
完整範例:參數化 register file#
module RegFile::<WORDS: u32, W: u32> (
clk : input clock,
waddr : input logic<$clog2(WORDS)>,
wdata : input logic<W>,
we : input logic,
raddr : input logic<$clog2(WORDS)>,
rdata : output logic<W>,
) {
var rf: logic<WORDS, W>;
always_ff {
if we {
rf[waddr] = wdata;
}
rdata = rf[raddr];
}
}
(此例的 always_ff 不帶重置,表示不需要 reset 的記憶陣列;$clog2 是系統函式,詳見 06.4 章。)實例化:
inst rf: RegFile::<16, 8>;
WORDS 與 W 是完全獨立的維度,任何組合都會產生一個經過型別檢查的具體模組。用 text macro 寫同樣的東西,每個維度都是一層 `define,錯誤訊息會在展開後的三百行裡等著你。
小結#
參數化的演進史,是「文字替換 → 有型別的編譯期參數」的演進史:Verilog parameter 解決了數值重用,SystemVerilog 補了 type parameter,Veryl 則以 Rust 式 generics 把它變成乾淨、可檢查的語言特性。本書統一以 module Foo::<W: u32> 宣告、inst u: Foo::<8> 實例化。下一章看另一個編譯期機制:generate。