跳轉到

5.2 泛型與 gen:超越參數覆寫的程式碼生成#

一個 SV 做不到的事#

先做一個小實驗。請你寫一個「取兩個值中較大者」的函式,要求支援任意位寬。

在 SystemVerilog 裡,這是做不到的。

function automatic logic [15:0] max16(input logic [15:0] a, b);
    return a > b ? a : b;
endfunction

function automatic logic [31:0] max32(input logic [31:0] a, b);
    return a > b ? a : b;
endfunction

function automatic logic [63:0] max64(input logic [63:0] a, b);
    return a > b ? a : b;
endfunction

你會發現這三個函式除了位寬以外完全一樣。要支援 8、12、16、24、32、48、64 bits,你需要寫七份幾乎相同的程式碼。

為什麼 SV 不能只寫一份?因為 SV 的函式沒有型別參數。它有 parameter,但 parameter 只能改變常數值,不能改變型別。而「位寬」在 SV 裡恰好是型別的一部分(logic [W-1:0])。

唯一的出路是文字巨集:

`define MAX_FN(name, width) \
    function automatic logic [``width-1:0] ``name(input logic [``width-1:0] a, b); \
        return a > b ? a : b; \
    endfunction

`MAX_FN(max8, 8)
`MAX_FN(max16, 16)
`MAX_FN(max32, 32)

這就是「巨集地獄」的起點。巨集是編譯前的文字替換,它有五個結構性缺陷:

缺陷 說明
沒有作用域 define 是全域的,會污染整個編譯單元
沒有型別檢查 巨集展開後的錯誤訊息指向展開後的行號,而不是巨集呼叫處
參數是文字 \`width` 必須是整數字面值,不能是運算式
巢狀巨集難以閱讀 兩層以上巢狀時,錯誤訊息幾乎無法閱讀
工具支援不一致 各家工具對巨集的行為細節有差異(特別是有關引號與空白的處理)

SystemVerilog 加入了 parameter type,方向是對的:

function automatic T max #(parameter type T = logic) (input T a, input T b);
    return a > b ? a : b;
endfunction

但 SV 的泛型有它自己的問題:它綁在 class 上(class 的參數化),對 module / function / package 這些「硬體建構」不適用。SV 的 module 只能用 parameter(值)與 parameter type(型別),而且 parameter type 的語法冗長、與 module 的關係彆扭。

Veryl 的泛型把這件事放回語言的核心。


Veryl 的泛型語法#

::<> 語法#

泛型參數寫在項目識別字之後,用 ::<...> 包住:

module Name::<T: u32> { ... }

完整的宣告形式:

module Register::<W: u32> (
    i_clk : clock,
    i_d   : input  logic<W>,
    o_q   : output logic<W>,
) {
    var r_q : logic<W>

    always_ff (i_clk) {
        r_q = i_d;
    }

    assign o_q = r_q;
}

實例化時指定泛型參數:

inst u_r8  : Register::<8>;
inst u_r32 : Register::<32>;

泛型參數的宣告形式#

泛型參數 ::= 識別字 : 邊界 [ = 預設值 ]
邊界形式 意義
type 任意型別
inst: X 某個型別 X 的實例
u32 / p32 / i32 等 固定的資料型別
使用者定義的型別 某個 struct / enum / interface
proto 模組 某個協定模組

支援泛型的項目#

項目 語法 例子
函式 function F::<T: u32> { } function Max::<T: u32>(a: T, b: T) -> T
模組 module M::<T: u32> { } module Register::<W: u32>
介面 interface I::<W: u32> { } interface Axi::<W: u32>
套件 package P::<T: u32> { } package Config::<W: u32>
結構體 struct S::<T: type> { } struct Wrapper::<T: type>
聯合體 union U::<T: type> { }

泛型邊界#

(一)type 邊界:接受任意型別。

struct Wrapper::<T: type> {
    value : T,
}

這是最寬鬆的邊界。T 可以是任何型別:u32、logic<8>、某個 struct、某個 enum。

(二)inst: X 邊界:接受某個型別的實例。

module Driver::<I: inst: AxiInterface> { ... }

這表示「泛型參數必須是某個介面型別的實例」。這是泛型模組表達「我需要一個符合某個介面的東西」的機制。

(三)固定資料型別邊界。

module Register::<W: u32> { ... }

W 必須是 u32 型的值。這是最常用的形式——用它做位寬、深度等參數化。

(四)使用者定義型別邊界。

struct Wrapper::<T: Point> { ... }

(五)proto 邊界:接受符合某個協定的模組。

module Master::<P: ProtoA> { ... }

這是最有意思的一種,我們在「原型」一節詳細討論。

預設泛型參數#

function Clamp::<T: u32 = 10>(x: T) -> T {
    return x > T'(10) ? T'(10) : x;
}

呼叫時可以用 ::<> 使用預設值:

let r = Clamp::<>()(20);

這個機制的價值:它讓一個泛型函式有「合理的預設行為」,呼叫端在不需要特定化時不必強迫指定。


gen 宣告:衍生參數#

這是 Veryl 泛型機制中最關鍵也最被低估的一項。

語法#

gen W      : u32  = 32;
gen T      : type = logic<8>;
gen PTR_W  : u32  = $clog2(DEPTH);

gen 宣告的是在泛型參數之間定義的衍生參數——它的值可以由其他泛型參數計算出來。

解決什麼問題#

考慮一個模組,它的位寬由 W 決定,但它的指標寬度是由 W(或深度)推導的。如果沒有 gen,你只有兩個選擇:

(一)重複指定。

module Fifo::<W: u32, DEPTH: u32, PTR_W: u32> { ... }

呼叫端必須寫:

inst u : Fifo::<32, 256, 8>;

問題:PTR_W 完全可以從 DEPTH 推導。要求呼叫端手動指定 8,等於把一個可以計算的東西變成一個可能出错的輸入。工程師寫錯了(寫成 9),型別系統也無能為力。

(二)把它寫成 const 放模組內。

module Fifo::<W: u32, DEPTH: u32> {
    const PTR_W : u32 = $clog2(DEPTH);
    ...
}

這可行,但有個限制:const 不能成為泛型參數的預設值,也不能參與其他泛型參數的定義。 而且在 gen 存在之前,這是唯一的做法。

gen 的做法#

module Fifo::<W: u32, DEPTH: u32> (
    i_clk : clock,
    i_rst : reset,
    o_data: output logic<W>,
) {
    gen PTR_W : u32 = $clog2(DEPTH);

    var r_mem : logic<W> [DEPTH]
    var r_wptr: logic<PTR_W>

    always_ff (i_clk, i_rst) {
        if_reset {
            r_wptr = 0;
        } else {
            r_wptr = r_wptr + 1;
        }
    }
}

呼叫端只需要:

inst u : Fifo::<32, 256>;

PTR_W 自動被推導為 8。沒有重複指定,因此沒有寫錯的可能。

gen 的型別也能是 type#

module Wrapper::<W: u32> {
    gen DataT : type = logic<W>;
    gen AddrT : type = logic<$clog2(DEPTH)>;   // 若 DEPTH 也是泛型參數

    var r_data : DataT
}

這讓「衍生型別」也成為可能——不只是衍生整數常數。

gen 與 const 的分工#

gen const
位置 泛型參數之間 項目主體內
可否作為泛型參數的預設值 可以 不可以
可否引用其他泛型參數 可以 可以(若 const 在有泛型參數的項目內)
可否被泛型參數引用 可以(這是主要用途) 不可以
對外可見 作為泛型參數的預設值對呼叫端可見 僅項目內

gen 的核心用途是「讓泛型參數可以互相推導,避免重複指定」。


原型(proto)與 for 綁定#

問題#

有時候你想寫一個模組,它需要「某個具有特定介面的子模組」,但你不想綁死具體是哪個實作。這在物件導向語言裡是「介面」,在硬體語言裡也有對應物——SV 的 interface 是一種,但 SV 的介面不能作為 module 的參數傳入。

Veryl 的解法是 proto(原型)。

宣告一個 proto 模組#

proto module ProtoA {
    // 宣告這個模組「必須提供」的介面
}

proto module 只宣告契約,不實作任何東西。

用 for 綁定實作#

module ModuleC for ProtoA {
    // ModuleC 的實作
}

這表示「ModuleC 實作 ProtoA 這個協定」。

在泛型中使用 proto 邊界#

module Wrapper::<P: ProtoA> {
    inst u : P;
}

這裡的 inst u : P; 是一個非常特殊的語法:它不是「實例化某個具體型別」,而是「實例化那個在泛型參數 P 上被綁定的 proto 實作」。

當呼叫端寫:

inst u : Wrapper::<ModuleC>;

語言會:

  1. 檢查 ModuleC 是否 for ProtoA——是的,因為 module ModuleC for ProtoA。
  2. 在 Wrapper 內部,把 inst u : P; 展開成 inst u : ModuleC; 的具體實例。
  3. 產生 Wrapper 的特化版本,其中 P 綁定到 ModuleC。

這就是「將抽象宣告為具體實例」的機制。

完整的例子#

// 1. 契約
proto module ProtoA {
    var r_val : logic<8>
}

// 2. 實作 A
module ModuleA for ProtoA {
    var r_val : logic<8>
    assign r_val = 8'hAA;
}

// 3. 實作 B
module ModuleB for ProtoA {
    var r_val : logic<8>
    assign r_val = 8'hBB;
}

// 4. 使用 proto 的泛型模組
module Top {
    inst u_a : Wrapper::<ModuleA>;
    inst u_b : Wrapper::<ModuleB>;
}

module Wrapper::<P: ProtoA> {
    inst u : P;
    assign o_val = u.r_val;
}

產生的 SV 會是 Wrapper 的兩個特化版本(或等價的展開)。這正是「名稱爆炸」問題的另一個面向——我們在後面會看到 Veryl 如何處理它。


泛型模組與泛型介面#

泛型模組#

module Register::<W: u32> (
    i_clk : clock,
    i_d   : input  logic<W>,
    o_q   : output logic<W>,
) {
    var r_q : logic<W>

    always_ff (i_clk) {
        r_q = i_d;
    }

    assign o_q = r_q;
}

實例化:

inst u_r8  : Register::<8>;
inst u_r16 : Register::<16>;

泛型介面#

interface Axi::<W: u32> {
    // 定義 W 寬的 AXI 訊號
    var wdata : logic<W>
    var rdata : logic<W>
}

實例化:

inst u_axi : Axi::<64>;

泛型介面與泛型模組的差異:介面定義的是「訊號群組」,模組定義的是「硬體結構」。兩者都可以泛型化,這在建立可重用匯流排介面時很有用。

泛型與參數的並用#

module Fifo::<W: u32, DEPTH: u32> #(
    parameter INIT : logic<W> = 0,
) (
    ...
) {
}

泛型參數(::<>)決定結構;參數(#(...))決定結構的某個常數實例。

判斷原則:

需求 用什麼
「這個模組支援任意寬度」 泛型 ::<W: u32>
「這個模組的深度固定為 16,但可以被上層改」 參數 #(parameter DEPTH: p32 = 16)
「介面形狀隨寬度改變」 泛型

與 SystemVerilog 的對照#

問題的根源:SV 的 parameter 只能改常數#

讓我們把三種語言並排看。

SystemVerilog(值參數,無法改變型別):

module Register #(
    parameter WIDTH = 32
) (
    input  logic             clk,
    input  logic [WIDTH-1:0] d,
    output logic [WIDTH-1:0] q
);
    logic [WIDTH-1:0] r_q;

    always_ff @(posedge clk) begin
        r_q <= d;
    end

    assign q = r_q;
endmodule

SystemVerilog(巨集,唯一能改變型別的途徑):

`define REGISTER(name, width) \
    module name ( \
        input  logic clk, \
        input  logic [``width-1:0] d, \
        output logic [``width-1:0] q \
    ); \
        logic [``width-1:0] r_q; \
        always_ff @(posedge clk) r_q <= d; \
        assign q = r_q; \
    endmodule

`REGISTER(Register8,  8)
`REGISTER(Register16, 16)
`REGISTER(Register32, 32)

Veryl(泛型):

module Register::<W: u32> (
    i_clk : clock,
    i_d   : input  logic<W>,
    o_q   : output logic<W>,
) {
    var r_q : logic<W>

    always_ff (i_clk) {
        r_q = i_d;
    }

    assign o_q = r_q;
}

名稱爆炸#

我們來看 SV 巨集方案的真實代價。

假設我們要支援 8 種寬度(8/16/24/32/48/64/96/128),而且有三個這樣的模組(register、adder、comparator):

語言 產生的定義數
Veryl 泛型 3 個(Register、Adder、Comparator)
SV 巨集 8 寬度 × 3 模組 = 24 個定義
SV 手寫 24 個,且每個都要手動維護

這就是「名稱爆炸」:一個概念(寬度)被編碼進了名稱裡,於是每個寬度值都產生一份獨立的定義。

名稱爆炸的後果:

  1. 文件爆炸:24 個模組要寫 24 份文件。
  2. 維護爆炸:修改共用邏輯要在 24 個地方同步。
  3. 工具負載:24 個模組都要各自編譯與合成。
  4. 資訊隱藏:工程師在寫 RTL 時看到的是 Register32,而不是「一個寬度參數為 32 的 Register」——參數化的意圖被名稱的約定俗成取代了。

而 Veryl 的泛型只有一份 Register 定義,寬度是實例點的屬性。

生成的 SystemVerilog 如何處理泛型#

Veryl 產生的 SV 有兩種可能的策略:

策略一:在實例點特化(產生特化模組)。

module Register #(
    parameter WIDTH = 32
) (
    input  logic clk,
    input  logic [WIDTH-1:0] d,
    output logic [WIDTH-1:0] q
);
    logic [WIDTH-1:0] r_q;

    always_ff @(posedge clk) begin
        r_q <= d;
    end

    assign q = r_q;
endmodule

產生的 SV 其實是 SV 的 parameter 形式。 這是「語義等價」的自然結果:Veryl 的泛型在 SV 中沒有直接對應物,最接近的等價物就是 parameter。

策略二:產生帶特化參數的模組,為每個不同的泛型實例產生一份。

module Register__8 (...)   // WIDTH 固定為 8
module Register__32 (...)  // WIDTH 固定為 32

這取決於工具的選擇。策略一保留了參數化的優點(SV 的工具鏈完全成熟),策略二則是把泛型「烙印」進去。

我們應該指出這裡的一個重要張力:如果 Veryl 產生的是 SV 的 parameter 形式,那麼Veryl 的泛型在 SV 那一端就退化成了 SV 的參數。這是誠實的現實——SV 沒有泛型,所以「語義等價」的目標要求 Veryl 降級。

但這不影響 Veryl 端的價值:在 Veryl 原始碼裡,泛型是型別安全的。讓我們看一個 SV 無法檢查、Veryl 可以檢查的例子:

function Max::<T: u32>(a: T, b: T) -> T {
    return a > b ? a : b;
}

在 Veryl 中,呼叫端不能傳入不同型別的兩個參數:

let r = Max(8'd3, 16'd5);   // 編譯期錯誤:型別不一致

在 SV 的 parameter type 版本裡:

function automatic T max #(parameter type T = logic) (input T a, input T b);
    return a > b ? a : b;
endfunction

SV 允許 max(8'(3), 16'(5))——兩個操作數型別不同,結果會被隱式轉換到較寬的型別。SV 的泛型沒有「所有參數必須同型」這條約束,所以它其實並沒有提供真正的型別安全。

Veryl 的泛型參數是單一的 T,所有使用 T 的位置(包括參數型別)必須一致——這才是真正的泛型。


討論:泛型 vs 巨集 vs 參數#

面向 參數(#(...)) 巨集(`define) 泛型(::<>)
能改變型別 否 是(文字替換) 是(語言機制)
有作用域 是(模組內) 否(全域) 是
有型別檢查 部分 無 有
錯誤訊息可讀 好 差(指向展開後) 好
產生的程式碼量 少 多(每個實例一份) 少
工具支援 完整 完整但行為細節不一 轉成 SV 的 parameter
表達力 常數值 任意文字 型別與值的受限集合

判斷原則:

你的需求 用什麼
一個數值常數(深度、位寬係數、初始值) 參數
需要根據其他參數計算的衍生值 gen
需要改變型別(位寬、資料結構種類) 泛型
需要根據泛型參數改變型別 gen ... : type
需要改變介面的形狀 泛型(模組或介面)

泛型與巨集最關鍵的差異是「作用域」與「型別檢查」。巨集是全域的,這意味著兩個不同的專案各自定義的 MAX_FN 巨集會在整合時衝突——而衝突的解決方式是改名或加前綴,也就是用命名約定代替作用域。泛型有真正的作用域,因此不需要命名約定。


設計討論:編譯期錯誤 vs 執行期錯誤#

硬體沒有執行期#

這是硬體語言相對於軟體語言最根本的差異之一,也是泛型最重要的價值所在。

在軟體裡,一個型別錯誤的程式碼:

def max_of(a, b):
    return a if a > b else b

max_of(3, "hello")    # 執行期才發現錯誤

會在執行時拋出例外。程式碼已經部署了,bug 是在生產環境中發現的。

在硬體裡,沒有執行期。RTL 要嘛被綜合成硬體,要嘛不被綜合。沒有「執行到那裡才發現錯誤」這種事。

所以硬體語言裡的型別錯誤必須在編譯期被發現:

語言 型別錯誤的發現時機 發現後的代價
Python 執行期 部署後,需要熱修
C++ 編譯期(模板是例外,錯誤在實例化時) 重新編譯
SystemVerilog 多數在編譯期,但很多寬度/整數溢位錯誤只給警告 可能是 silicon 上的 bug
Veryl 編譯期 重新編譯

泛型把「N 份程式碼」變成「一份程式碼 + N 次檢查」#

這一點值得量化。

假設你要支援 8 種寬度。

巨集方案的驗證成本:8 份獨立的程式碼,8 份獨立的審查,8 個獨立的潛在 bug 位置。而且——這是最關鍵的——當你修正一個 bug 時,你必須確認 8 份都修正了。如果其中一份忘了(複製貼上的常見後果),bug 會潛伏在特定寬度的實例中。

泛型方案:一份程式碼,8 個實例點。第 8 次的檢查是自動的,因為它就是同一份程式碼。

這就是「用一份程式碼取代 N 份程式碼」在硬體工程裡的真實價值——它不只是省下打字時間,更是消除了一整類「只出現在某個參數組合」的 bug。

泛型帶來的新錯誤類別#

誠實地說,泛型也帶來新的錯誤來源:

錯誤類別 說明
推論歧義 泛型參數無法從呼叫端唯一推斷
邊界不符 傳入的型別不滿足泛型邊界
實例化點不一致 同一個泛型在不同實例點被特化成不同型別
gen 的相依順序 gen 參數引用了尚未定義的 gen

Veryl 對這些都有對應的診斷。但重點是:這些錯誤全部發生在編譯期,而且它們是型別錯誤,不是邏輯錯誤。

泛型無法防止邏輯錯誤——它只能保證「型別是對的」。一個泛型的 FIFO 仍然可能有握手 bug。這一點必須誠實地說清楚:泛型提升的是「結構正確性」的保證,不是「功能正確性」的保證。


生成的 SystemVerilog 對照#

對照一:泛型模組#

Veryl:

module Register::<W: u32> (
    i_clk : clock,
    i_d   : input  logic<W>,
    o_q   : output logic<W>,
) {
    var r_q : logic<W>

    always_ff (i_clk) {
        r_q = i_d;
    }

    assign o_q = r_q;
}

生成的 SystemVerilog:

module Register #(
    parameter WIDTH = 32
) (
    input  logic         i_clk,
    input  logic [WIDTH-1:0] i_d,
    output logic [WIDTH-1:0] o_q
);
    logic [WIDTH-1:0] r_q;

    always_ff @(posedge i_clk) begin
        r_q <= i_d;
    end

    assign o_q = r_q;
endmodule

觀察:泛型參數 W 轉成了 SV 的 parameter WIDTH,而 logic<W> 轉成了 logic [WIDTH-1:0]。語義等價,但表達力降級——這是 SV 語言能力的限制,不是 Veryl 的缺陷。

對照二:gen 衍生參數#

Veryl:

module Fifo::<W: u32, DEPTH: u32> (
    i_clk : clock,
    o_data: output logic<W>,
) {
    gen PTR_W : u32 = $clog2(DEPTH);

    var r_mem : logic<W> [DEPTH]
    var r_ptr : logic<PTR_W>
}

生成的 SystemVerilog:

module Fifo #(
    parameter WIDTH = 32,
    parameter DEPTH = 256,
    parameter PTR_W = $clog2(DEPTH)
) (
    input  logic i_clk,
    output logic [WIDTH-1:0] o_data
);
    logic [WIDTH-1:0] r_mem [0:DEPTH-1];
    logic [PTR_W-1:0] r_ptr;

gen 轉成了 SV 的第三個 parameter,其預設值是運算式。 這在 SV 裡是完全合法的,而且保留了「只指定一個值」的優點——呼叫端只需要覆寫 WIDTH 與 DEPTH。

對照三:proto 綁定#

Veryl:

proto module ProtoA {
    var r_val : logic<8>
}

module ModuleC for ProtoA {
    var r_val : logic<8>
    assign r_val = 8'hCC;
}

module Wrapper::<P: ProtoA> {
    inst u : P;
    assign o_val = u.r_val;
}

生成的 SystemVerilog:

module ModuleC (
    output logic [7:0] r_val
);
    assign r_val = 8'hCC;
endmodule

module Wrapper (
    output logic [7:0] o_val
);
    ModuleC u (
        .r_val (r_val)
    );
    assign o_val = r_val;
endmodule

注意:proto module ProtoA 本身不產生任何 SV(它只是契約),而 Wrapper 被特化成了使用 ModuleC 的具體版本。


實作練習#

練習一:泛型 FIFO#

題目: 寫一個泛型的同步 FIFO,位寬與深度都是泛型參數,指標寬度用 gen 推導。

解答:

module SyncFifo::<W: u32, DEPTH: u32> (
    i_clk   : clock,
    i_rst   : reset,

    i_push  : input  logic,
    i_data  : input  logic<W>,
    o_full  : output logic,

    i_pop   : input  logic,
    o_data  : output logic<W>,
    o_empty : output logic,
) {
    gen PTR_W : u32 = $clog2(DEPTH);
    gen CNT_W : u32 = $clog2(DEPTH + 1);

    var r_mem   : logic<W> [DEPTH];
    var r_wptr  : logic<PTR_W>;
    var r_rptr  : logic<PTR_W>;
    var r_count : logic<CNT_W>;

    always_ff (i_clk, i_rst) {
        if_reset {
            r_wptr  = 0;
            r_rptr  = 0;
            r_count = 0;
        } else {
            if i_push && !o_full {
                r_mem[r_wptr] = i_data;
                r_wptr        = r_wptr + 1;
            }

            if i_pop && !o_empty {
                r_rptr = r_rptr + 1;
            }

            if i_push && !o_full && i_pop && !o_empty {
                r_count = r_count;
            } else if i_push && !o_full {
                r_count = r_count + 1;
            } else if i_pop && !o_empty {
                r_count = r_count - 1;
            }
        }
    }

    assign o_full  = r_count == DEPTH;
    assign o_empty = r_count == 0;
    assign o_data  = r_mem[r_rptr];
}

注意兩個 gen:

  • PTR_W = $clog2(DEPTH):指標寬度。這裡有個潛在問題——若 DEPTH 不是 2 的冪(例如 100),$clog2(100) = 7 能表示 0..127,但記憶體只有 100 格。這正是 5.1 節討論過的問題。 一個改良是把 DEPTH 也取成 2 的冪:
gen DEPTH_LOG2 : u32 = $clog2(DEPTH);
gen DEPTH_POW2  : u32 = 1 << DEPTH_LOG2;

然後用 DEPTH_POW2 當實際深度。

  • CNT_W = $clog2(DEPTH + 1):計數器需要能表示 DEPTH 這個值,所以要多一位。這個「+1」是 FIFO 的經典細節,用 gen 表達讓它一次寫對。

實例化:

inst u_fifo32 : SyncFifo::<32, 256>;
inst u_fifo8  : SyncFifo::<8, 16>;

練習二:泛型的位元向量操作套件#

題目: 寫一個泛型的套件,提供位元向量操作:計數設定位元(popcount)、最高位索引(find first set)、位元反轉。

解答:

package BitOps::<W: u32> {
    pub fn popcount(a: logic<W>) -> u32 {
        var count : u32 = 0;
        for i: u32 in 0..W {
            if a[i] {
                count = count + 1;
            }
        }
        return count;
    }

    pub fn reverse(a: logic<W>) -> logic<W> {
        var r : logic<W>;
        for i: u32 in 0..W {
            r[i] = a[W - 1 - i];
        }
        return r;
    }

    pub fn msb_index(a: logic<W>) -> p32 {
        // 回傳最高位元的索引;全 0 時回傳 0
        var idx : p32 = 0;
        for i: u32 in 0..W {
            if a[i] {
                idx = i;
            }
        }
        return idx;
    }
}

這個套件展示了泛型套件的價值:一份 BitOps,在不同的寬度下使用。

注意 msb_index 回傳 p32——因為索引必然是非負的,這正是 p32 的用途(見 5.1 節)。如果回傳 i32,就無法表達「索引非負」這個約束。

實例化:

let r_cnt = BitOps::<32>::popcount(i_data);
let r_rev = BitOps::<32>::reverse(i_data);

思考:popcount 在硬體上是組合電路(一組加法器的樹),不是軟體迴圈。for 迴圈在這裡被展開成硬體——這是 5.3 節的主題。

練習三:proto 與泛型組合#

題目: 定義一個 ProtoCounter 協定,要求實作者提供一個「加一」的方法;然後寫一個泛型模組,能與任何實作了這個協定的計數器搭配。

解答:

proto module ProtoCounter {
    // 契約:實作者必須提供 o_count
    var o_count : logic<32>
}

module SimpleCounter for ProtoCounter {
    var o_count : logic<32>

    always_ff (i_clk, i_rst) {
        if_reset {
            o_count = 0;
        } else {
            o_count = o_count + 1;
        }
    }
}

module CounterWrapper::<P: ProtoCounter> {
    inst u : P;
    assign o_doubled = u.o_count * 2;
}

這個練習的意義在於:一個模組(CounterWrapper)不需要知道它操作的計數器是什麼實作,只需要知道它遵守 ProtoCounter 契約。這與軟體的依賴注入是同一個思想,但在硬體裡它是編譯期的。


本章小結#

  1. SV 的 parameter 只能改常數值,不能改型別,因此「支援任意寬度的函式」在 SV 裡只能用巨集,造成作用域缺失、型別檢查缺失與名稱爆炸。
  2. Veryl 的泛型語法是 ::<>:宣告在項目識別字之後,支援 function、module、interface、package、struct、union。
  3. 泛型邊界有五種:type(任意型別)、inst: X(某型別的實例)、固定資料型別、使用者定義型別、proto 邊界。
  4. 預設泛型參數(function F::<T: u32 = 10>)讓泛型有合理的預設行為。
  5. gen 是關鍵機制:它定義在泛型參數之間的衍生參數,讓泛型參數可以互相推導,避免重複指定所帶來的錯誤可能。
  6. proto 與 for 綁定提供「將抽象宣告為具體實例」的機制,inst u : P; 在泛型模組內部展開成實際的 proto 實作。
  7. 泛型 vs 巨集 vs 參數的取捨:作用域、型別檢查、表達力。巨集唯一的優勢是能產生 SV 無法表示的東西,代價是失控的維護成本。
  8. 泛型把「N 份程式碼」變成「一份程式碼 + N 次自動檢查」,消除了一整類「只出現在某個參數組合」的 bug。
  9. 誠實的邊界:泛型提升的是結構正確性的保證,不是功能正確性的保證。生成的 SV 因為 SV 沒有泛型而降級為 parameter 形式——這是「語義等價」目標的必然代價。

下一節(5.3)我們進入語言中最表達力密集的部分:表達式。我們會看到 Veryl 如何用 if / case / switch 表達式取代 SV 的巢狀三元運算子與 casez / casex 的怪異機制,以及 inside / outside、msb / lsb、repeat 這些針對硬體位元操作的專用語法。


下一節:第 5.3 節「表達式:if/case/switch、for/inside/outside 與 msb/repeat」