9.1 sv 命名空間、embed 與 include:混合語言設計#
互操作是採用的前提#
一個新 HDL 若不能接上既有的 SystemVerilog 資產——成熟的 IP、龐大的驗證環境、被工具流程假設的檔案格式——它就只有教學價值。2.2 節已論證:互操作是三支柱的鏈條中承上啟下的一環,且依賴「產出物可讀」這個前提。本節把互操作的第一個方向(Veryl 呼叫 SystemVerilog)的機制拆開:$sv:: 命名空間、embed、include、bind,以及它們在混合語言專案中的組織方式。
第 9 章的整體分工如下:
| 節 | 問題 | 工具 |
|---|---|---|
| 9.1(本節) | Veryl 專案中使用既有 SV | $sv::、embed、include、bind |
| 9.2 | 既有 SV 轉成 Veryl | veryl translate |
| 9.3 | 全公司的遷移策略與共享介面 | 層次模型、std 函式庫 |
$sv:: 命名空間#
存取 package 常數#
$sv:: 前綴指向專案中既有的 SystemVerilog 編譯單元——它們以 .sv 檔的形式與 Veryl 產出物一起交給下游工具:
// PackageA.sv:既有的 SV package
package PackageA;
parameter ParamA = 4;
endpackage
// 使用它
let _a: logic = $sv::PackageA::ParamA;
$sv::PackageA::ParamA 的語意是「SV package PackageA 中的 ParamA」。型別在這個邊界上被約定:SV 的 parameter 常數在 Veryl 端以相容的位向量/整數型別表示。數值常數的跨界是無痛的;複雜型別(struct、typedef)的跨界則有限制(見下文「型別系統的邊界」)。
實例化 SV 模組與介面#
module Top {
inst b: $sv::ModuleB;
inst c: $sv::InterfaceC;
}
inst b: $sv::ModuleB;——實例化 SV 模組ModuleB,生成碼中就是一個對ModuleB的模組實例;inst c: $sv::InterfaceC;——實例化 SV 介面;介面內的訊號與 modport 以 Veryl 介面相似的方式存取。
這個機制的實際效果是:Veryl 可以扮演整合層。新的頂層、新的膠合邏輯(glue logic)用 Veryl 寫,底下掛的是多年累積的 SV IP。9.3 節的遷移層次模型中,Level 1 的混合專案正是這個形態。
r# 原始識別字#
混合語言的第一個現實摩擦是關鍵字衝突。SV 的埠經常命名為 clock、reset——這兩個名字在 Veryl 中是型別關鍵字(4.2 節)。連接這樣的埠時,用 r# 前綴把名字作為原始識別字引用:
// 既有 SV 模組:
// module LegacyCore (input clock, input reset_n, ...);
inst u_core: $sv::LegacyCore (
clock : i_clk, // 錯誤:clock 是 Veryl 的型別關鍵字
);
inst u_core: $sv::LegacyCore (
r#clock: i_clk, // 正確:r#clock 引用名為 "clock" 的埠
);
工具對錯誤寫法的回應是明確的語法錯誤(expected identifier, found keyword "clock" 之類的訊息),此時補上 r# 即可。r# 的設計借用自 Rust 的 raw identifier 慣例,語意是:「r# 之後的名字按字面處理,不做關鍵字解析」。
值得體會的是這個設計的定位:Veryl 沒有為了相容而把 clock、reset 從關鍵字降級成一般識別字(那會犧牲 4.2 節型別抽象的語法清晰度),也沒有要求使用者回去改 SV IP 的埠名(那違反互操作的前提)。r# 把衝突局部化到單一的使用點,兩邊的設計完整性都保住了。
型別系統的邊界#
$sv:: 跨界時,型別的對應規則:
| SystemVerilog | Veryl 端表示 | 跨界狀態 |
|---|---|---|
logic [7:0] | logic<8> | 無痛 |
wire [15:0] | logic<16> | 無痛 |
parameter / localparam 常數 | 整數 / 位向量常數 | 無痛 |
struct | 部分支援(欄位逐一對應) | 有限制 |
typedef 的複合型別 | 需就地展開或重新宣告 | 有限制 |
| 介面中的 modport | 經介面實例存取 | 可用 |
複合型別的限制根源是:SV 的 typedef 是文字層級的別名,其定義散落在各處;Veryl 的型別系統要求顯式與封閉。實務的應對是在 Veryl 端重新宣告對應的 struct(欄位相同),並在邊界模組(見下文)中做欄位級的拆裝——多寫幾行,換來兩側型別系統各自完整。
embed 宣告#
基本形式:原樣展開#
embed (inline) sv{{{ ... }}} 把 SystemVerilog 程式碼原樣放進生成碼:
module Top (
i_clk: clock,
) {
embed (inline) sv{{{
// 原生的 SV 片段 原樣出現在生成的 .sv 中
localparam SOME_SV_CONST = 8'h5A;
}}};
}
三層大括號 {{{ / }}} 是刻意的設計(2.2 節):嵌入碼本身經常含有大括號(begin/end 對應的 {}、struct 常值),三層括號使內部的 {} 不需要轉義。同時,這個笨重的語法在視覺上把嵌入碼與正常 Veryl 區隔開——逃生門必須不太舒服,否則會被濫用。
識別字插值:與 Veryl 命名空間的接點#
嵌入碼可以透過 \{ ... \} 插入 Veryl 的識別字與泛型實例:
embed (inline) sv{{{
bind u_a \{ Module47B::<32> \} u_b32 ();
}}};
\{ Module47B::<32> \} 在生成時被替換為 Veryl 泛型模組 Module47B 以參數 32 實例化後的生成名稱(含專案前綴,3.3 節)。這解決了一個嵌入碼的根本難題:Veryl 的泛型模組在生成 SV 之前沒有最終名字,手寫 SV 無法引用它們;插值機制讓嵌入碼參與 Veryl 的泛型與命名空間系統,而不是漂浮在命名空間之外。
這是「逃生門但不是斷線」的關鍵設計:embed 允許你借 SV 的語法(例如 SV 特有的 construct),但透過插值,嵌入碼與 Veryl 的型別與泛型世界仍然相連。沒有插值的 embed 是一堵牆;有插值的 embed 是一扇帶鉸鏈的門。
嵌入碼與屬性#
含 embed 的項目可以標注 Veryl 屬性。典型場景:clocking block 的監控介面(SV 的 clocking block 是 Veryl 沒有對應語法的 construct),其中會有 Veryl lint 會誤報的未使用訊號:
#[allow(unused_variable)]
interface MonitorIf (
i_clk: clock,
) {
embed (inline) sv{{{
clocking cb_mon @(posedge i_clk);
input #1step bus_valid, bus_data;
endclocking
}}};
}
屬性沿著宣告階層生效,使嵌入碼可以接受 Veryl lint 體系的管轄——又一個「門上有鉸鏈」的例子。
embed (cocotb)#
embed (cocotb) py{{{ ... }}} 在 8.3 節已完整介紹:同一個 embed 機制,語言標記換成 py,作用對象是測試流程。這顯示 embed 是一個通用的原樣嵌入框架,sv 與 py 是它的兩個已實作目標。
include 宣告#
include(inline, "module.sv");
include 與 embed 的差別:embed 內嵌片段,include 內嵌檔案——從相對路徑讀入外部 SV 檔,原樣放到生成碼的對應位置。inline 關鍵字強調「原樣貼上,不做轉換」。
典型的使用場景:既有的大型 SV 檔不想搬進 Veryl 原始碼樹。例如一個由其他團隊維護的 .svh 巨集定義檔,或一份需要保持獨立版本控制的第三方 SV 模組。include 讓 Veryl 專案引用它們,而不取得它們的所有權——這對「兩個團隊各自維護各自的部分」的組織邊界非常重要。
bind 宣告#
SV 的 bind 與 Veryl 的對應#
SystemVerilog 的 bind 把一個模組「注射」進另一個模組的層級中,不修改目標原始碼。它是驗證的關鍵技術:bus monitor、coverage collector、assertion module 都靠 bind 附加到 DUT 上。
Veryl 提供對應的語法:
bind Module <- u0: ModuleB (
i_clk : clk,
i_data : data,
);
語意:在模組 Module 的內部層級中,建立一個名為 u0 的 ModuleB 實例,括號內是連接(連接到 Module 的內部訊號)。目標模組的原始碼完全不改動。
完整範例:bus monitor 的 bind#
先定義監控器:
pub module BusMonitor (
i_clk : clock,
i_rst : reset,
i_valid: input logic,
i_data : input logic<32>,
) {
var r_trans: logic<32>;
always_ff {
if_reset {
r_trans = 0;
} else if i_valid {
r_trans = i_data;
// 統計、檢查、覆蓋收集……
}
}
}
把它 bind 到 DUT 上:
bind AxieTop <- u_mon: BusMonitor (
clk : i_clk,
rst : i_rst,
i_valid: r_bus_valid,
i_data : r_bus_data,
);
生成的 SV 中,AxieTop 模組內會出現 BusMonitor 的實例,接線到 AxieTop 的內部訊號 r_bus_valid / r_bus_data。監控器的程式碼與 DUT 的原始碼完全分離——這對驗證與設計的職責分離至關重要:監控邏輯的修改不需要碰 RTL 的版本。
限制:以模組為 scope,不能針對實例#
SV 的 bind 可以指定特定實例的路徑(bind dut.u_mem.u_bank ...);Veryl 的 bind 目標只能是模組或介面這一層的 scope——bind 到 Module 意味著所有 Module 的實例都會被注入。
這個簡化是有意識的取捨:實例路徑的 bind 引入了「層級路徑」的語意複雜度——路徑中的每一級都可能是泛型實例化、生成迴圈的產物,解析路徑需要模擬整個生成過程。Veryl 以「scope 只到模組層」把這個複雜度整個收掉。實務的影響有限:需要「只監控某個實例」時,常見的替代是包一個 wrapper 模組(wrapper 內 bind,外部用 wrapper 取代原實例),代價是多一層模組。
混合語言專案的組織#
目錄結構建議#
my_soc/
├── Veryl.toml
├── src/ # Veryl 原始碼:新模組與頂層整合
│ ├── top.veryl
│ ├── bus_bridge.veryl
│ └── legacy_bindings.veryl # 所有 $sv:: 實例化集中在這裡
├── sv/ # 既有 SV:IP 與驗證環境
│ ├── uart_core.sv
│ ├── memory_ctrl.sv
│ └── tb/
│ └── uvm_env.sv
└── external/ # include 引用的外部檔
└── defines.svh
分工原則:
| 留在 SV | 用 Veryl |
|---|---|
| 已驗證、已流片的 IP | 新設計的模組 |
| UVM 驗證環境(8.3 節的嵌入承接) | 頂層整合與膠合邏輯 |
依賴 SV 特有語法的構造(clocking block、config) | 匯流排介面層(9.3 節 std 函式庫) |
legacy_bindings.veryl 這個檔案值得強調:把所有 $sv:: 實例化集中在一處。它就是整個專案與 SV 資產的介面清單——code review 時檢查跨語言邊界只需要看一個檔案,9.3 節的 CODEOWNERS 機制也可以針對它單獨指定審查者。
產出的 filelist#
veryl build 產生的 filelist(7.2 節)列出所有生成 SV 檔;混合專案中,下游工具的 filelist 要同時包含兩種來源:
# 生成 SV(來自 Veryl)+ 既有 SV
-f Veryl.f
sv/uart_core.sv
sv/memory_ctrl.sv
sv/tb/uvm_env.sv
由於 Veryl 的生成模組帶專案前綴(3.3 節),my_soc_uart_core(Veryl 端)與 uart_core(SV 端)不會衝突;反之,若 SV 端有一個也叫 my_soc_Top 的模組,前綴機制會在整合初期就暴露衝突,而不是在 link 階段以神秘的行為呈現。
設計討論:逃生門的代價#
embed、include、$sv:: 都有一個共同的代價:跨過界線的部分脫離了 Veryl 的檢查體系。lint(7.2 節)、CDC 檢查(6.2 節)、型別檢查、fmt——這些保證只作用於 Veryl 語言範圍內的程式碼。一段 200 行的嵌入 SV 在 Veryl 的世界裡是黑箱,它的品質責任回到 SV 的工具鏈與人的紀律。
因此本節的組織建議可以收斂成一條原則:
把 embed 集中在明確標記的邊界層,核心邏輯保持純 Veryl。
「邊界模組」模式(boundary module pattern):
/// 與 SV IP 的邊界 wrapper:所有跨語言的髒活集中在這裡
pub module UartBridge (
i_clk : clock,
i_rst : reset,
i_wdata : input logic<8>,
i_wr : input logic,
o_rdata : output logic<8>,
) {
// 對內:純 Veryl 的世界
var fifo_wr : logic;
var fifo_d : logic<8>;
// 對外:唯一的 $sv:: 觸點
inst u_uart: $sv::uart_core (
r#clock: clk,
r#reset: rst,
r#wdata : fifo_d,
r#wr : fifo_wr,
r#rdata : o_rdata,
);
}
邊界模組的特徵:數量少(每個 SV IP 一個)、職責單一(型別轉換、埠名對接、r# 摩擦的吸收)、可被 CODEOWNERS 單獨管理。核心資料路徑與控制邏輯完全不碰 $sv::——於是「Veryl 的檢查保證」覆蓋了專案中邏輯最複雜、bug 最貴的那部分程式碼,而 SV 的部分限定在相對簡單的對接層。
實作練習#
- 找一個現成的簡單 SV IP(例如開源的 UART),放進專案的
sv/目錄;在 Veryl 頂層以$sv::實例化,處理r#clock等關鍵字衝突,veryl build確認生成的 filelist 包含兩側檔案; - 為該 IP 寫一個 Veryl 的 bus monitor 模組,用
bind注射到 IP 的模組層,並在外部模擬器(或 8.2 節的內建模擬器)中確認監控輸出; - 把一個需要 Veryl 泛型參數的 embed 片段改寫成
\{ ... \}插值形式,檢查生成 SV 中名稱的展開是否包含專案前綴; - 建立邊界模組:把練習 1 的
$sv::實例化收進一個 wrapper,之後在頂層只實例化 wrapper——觀察頂層程式碼中 SV 相關痕跡的消失; - 嘗試在嵌入碼中寫一個故意有 lint 問題的 SV 片段(例如未驅動訊號),執行
veryl check——確認 Veryl lint 對嵌入碼的視而不見,體會「逃生門的代價」。
本節小結#
- 互操作是採用的前提;
$sv::命名空間讓 Veryl 存取 SV 的 package 常數、模組與介面,Veryl 可以扮演整合層。 r#原始識別字把 SV/Veryl 的關鍵字衝突(clock、reset)局部化到單一使用點,兩側的設計完整性都不犧牲。embed (inline) sv{{{ ... }}}原樣嵌入 SV;\{ ... \}識別字插值讓嵌入碼參與 Veryl 的泛型與命名空間系統——逃生門但不是斷線。include(inline, "file")引用不想取得所有權的外部 SV 檔;bind把監控模組注射進目標模組層級而不修改其原始碼(限制:scope 到模組層,不支援實例路徑)。- 混合專案的組織:
$sv::觸點集中在邊界模組;filelist 同時含兩種來源;專案前綴保證命名不衝突。 - 逃生門的代價是脫離 Veryl 檢查體系——embed 必須集中在明確標記的邊界層,核心邏輯保持純 Veryl。
下一節(9.2)處理另一個方向:把既有的 SystemVerilog 自動轉換成 Veryl。
下一節:第 9.2 節「translate:從 SystemVerilog 到 Veryl 的自動轉換(2024-2026)」