跳轉到

05.5 參數化模組:param 與 generic#

從 parameter 到 text macro 地獄#

可重用設計的需求比 HDL 更早出現:同一個 adder 要服務 8 位元與 32 位元、同一個 FIFO 要服務不同深度。Verilog-1995 的解法是 parameter:模組宣告參數,實例化時以 #(...) 覆寫。這個機制在 1990 年代推動了 parameterized IP 的商業模式,但很快工程師就撞到它的天花板:

  • parameter 只能是數值,不能是型別(你想說「這個模組的資料是 32 位元 signed」,得另外用一個 signed flag parameter)。
  • 複雜的編譯期計算得靠 `define text 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。