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>;
語言會:
- 檢查
ModuleC是否for ProtoA——是的,因為module ModuleC for ProtoA。 - 在
Wrapper內部,把inst u : P;展開成inst u : ModuleC;的具體實例。 - 產生
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 個,且每個都要手動維護 |
這就是「名稱爆炸」:一個概念(寬度)被編碼進了名稱裡,於是每個寬度值都產生一份獨立的定義。
名稱爆炸的後果:
- 文件爆炸:24 個模組要寫 24 份文件。
- 維護爆炸:修改共用邏輯要在 24 個地方同步。
- 工具負載:24 個模組都要各自編譯與合成。
- 資訊隱藏:工程師在寫 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 契約。這與軟體的依賴注入是同一個思想,但在硬體裡它是編譯期的。
本章小結#
- SV 的
parameter只能改常數值,不能改型別,因此「支援任意寬度的函式」在 SV 裡只能用巨集,造成作用域缺失、型別檢查缺失與名稱爆炸。 - Veryl 的泛型語法是
::<>:宣告在項目識別字之後,支援 function、module、interface、package、struct、union。 - 泛型邊界有五種:
type(任意型別)、inst: X(某型別的實例)、固定資料型別、使用者定義型別、proto邊界。 - 預設泛型參數(
function F::<T: u32 = 10>)讓泛型有合理的預設行為。 gen是關鍵機制:它定義在泛型參數之間的衍生參數,讓泛型參數可以互相推導,避免重複指定所帶來的錯誤可能。proto與for綁定提供「將抽象宣告為具體實例」的機制,inst u : P;在泛型模組內部展開成實際的 proto 實作。- 泛型 vs 巨集 vs 參數的取捨:作用域、型別檢查、表達力。巨集唯一的優勢是能產生 SV 無法表示的東西,代價是失控的維護成本。
- 泛型把「N 份程式碼」變成「一份程式碼 + N 次自動檢查」,消除了一整類「只出現在某個參數組合」的 bug。
- 誠實的邊界:泛型提升的是結構正確性的保證,不是功能正確性的保證。生成的 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」