跳轉到

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 的部分限定在相對簡單的對接層。


實作練習#

  1. 找一個現成的簡單 SV IP(例如開源的 UART),放進專案的 sv/ 目錄;在 Veryl 頂層以 $sv:: 實例化,處理 r#clock 等關鍵字衝突,veryl build 確認生成的 filelist 包含兩側檔案;
  2. 為該 IP 寫一個 Veryl 的 bus monitor 模組,用 bind 注射到 IP 的模組層,並在外部模擬器(或 8.2 節的內建模擬器)中確認監控輸出;
  3. 把一個需要 Veryl 泛型參數的 embed 片段改寫成 \{ ... \} 插值形式,檢查生成 SV 中名稱的展開是否包含專案前綴;
  4. 建立邊界模組:把練習 1 的 $sv:: 實例化收進一個 wrapper,之後在頂層只實例化 wrapper——觀察頂層程式碼中 SV 相關痕跡的消失;
  5. 嘗試在嵌入碼中寫一個故意有 lint 問題的 SV 片段(例如未驅動訊號),執行 veryl check——確認 Veryl lint 對嵌入碼的視而不見,體會「逃生門的代價」。

本節小結#

  1. 互操作是採用的前提;$sv:: 命名空間讓 Veryl 存取 SV 的 package 常數、模組與介面,Veryl 可以扮演整合層。
  2. r# 原始識別字把 SV/Veryl 的關鍵字衝突(clock、reset)局部化到單一使用點,兩側的設計完整性都不犧牲。
  3. embed (inline) sv{{{ ... }}} 原樣嵌入 SV;\{ ... \} 識別字插值讓嵌入碼參與 Veryl 的泛型與命名空間系統——逃生門但不是斷線。
  4. include(inline, "file") 引用不想取得所有權的外部 SV 檔;bind 把監控模組注射進目標模組層級而不修改其原始碼(限制:scope 到模組層,不支援實例路徑)。
  5. 混合專案的組織:$sv:: 觸點集中在邊界模組;filelist 同時含兩種來源;專案前綴保證命名不衝突。
  6. 逃生門的代價是脫離 Veryl 檢查體系——embed 必須集中在明確標記的邊界層,核心邏輯保持純 Veryl。

下一節(9.2)處理另一個方向:把既有的 SystemVerilog 自動轉換成 Veryl。


下一節:第 9.2 節「translate:從 SystemVerilog 到 Veryl 的自動轉換(2024-2026)」