跳轉到

3.2 介面、modport 與 connect 操作符#

為什麼需要介面#

在第 3.1 節結束時,我們的模組之間只能透過「一堆散落的訊號」連接。假設我們要設計一個 AXI4-Lite 從介面,每個模組的埠清單會是這樣:

module AxiSlave (
    i_clk     : clock,
    i_rst_n   : reset,
    i_awaddr  : input  logic<32>,
    i_awvalid : input  logic,
    o_awready : output logic,
    i_wdata   : input  logic<32>,
    i_wstrb   : input  logic<4>,
    i_wvalid  : input  logic,
    o_wready  : output logic,
    i_araddr  : input  logic<32>,
    i_arvalid : input  logic,
    o_arready : output logic,
    o_rdata   : output logic<32>,
    o_rvalid  : output logic,
    i_rready  : input  logic,
    o_bvalid  : output logic,
    i_bready  : input  logic,
) {
}

這裡的問題不只是「埠很多」:

  1. 協定被稀釋成獨立訊號。 「awvalid 與 awready 必須同時握手」這個協定的本質,在埠清單裡完全看不出來。
  2. 容易接錯。 把 wstrb 接到 wvalid 上是型別允許的、編譯允許的,只有在整合時才發現。
  3. 介面定義沒有單一來源。 如果有五個模組都需要 AXI-Lite,五份埠清單要各自維護,改一個欄位要改五處。
  4. 文件要另外寫。 協定的語意只能存在於工程師的腦中或 Word 裡。

介面(interface)的存在意義就是把「一組相關訊號 + 它們之間的協定」封裝成一個可以命名、可以參數化、可以傳遞的第一類物件。


SystemVerilog 介面的回顧#

在進入 Veryl 之前,先理解 SV 介面帶來了什麼、又沒解決什麼。這很重要,因為 Veryl 的介面系統是針對 SV 介面的具體不足設計的,而不是憑空發明。

SV 介面的基本形態#

interface axi_if #(parameter ADDR_W = 32) (input logic clk);
    logic [ADDR_W-1:0] awaddr;
    logic              awvalid;
    logic              awready;
    ...
    modport master (
        output awaddr,
        output awvalid,
        input  awready
    );
    modport slave (
        input  awaddr,
        input  awvalid,
        output awready
    );
endinterface

模組透過埠引用介面:

module AxiSlave (axi_if.slave bus);
    always_ff @(posedge bus.clk) begin
        bus.awready <= 1'b1;
    end
endmodule

SV 介面解決了什麼#

  • 協定封裝:一組訊號有單一命名空間。
  • modport:宣告「從這個角色的視角看,哪些訊號是輸入、哪些是輸出」,工具可以檢查方向。
  • 參數化:axi_if #(.ADDR_W(40))。
  • 跨模組傳遞:一個模組產生的介面可以直接接到另一個模組。

SV 介面沒有解決什麼#

(一)方向檢查是「文件級」而非「強制級」的。

SV 的 modport 檢查是基於角色視圖的靜態檢查,它的規則是:當你在一個模組的埠上寫 bus.slave 時,該模組只能讀取 awaddr、awvalid(在 slave modport 中它們是 input),只能寫入 awready。

但這個檢查有幾個漏洞:

  • modport 可以省略。 module AxiSlave (axi_if bus); 不帶 modport 時,介面變成「隱含的雙向」——任何方向都能存取。檢查失效。
  • 可以取得介面物件後再取 modport。 在程式碼裡寫 bus.awvalid 時,如果變數宣告型別是 axi_if(而非 axi_if.slave),檢查同樣失效。
  • 多個驅動不會被 modport 抓到。 兩個 assign 驅動同一個介面成員,SV 會視為合法的多驅動 net(需要 tri),modport 完全不會抱怨。

(二)多驅動問題。

SV 的 interface 裡的成員預設是 logic,也就是 variable。variable 在 SV 中不允許多驅動——兩個 assign 驅動同一個 logic 會是錯誤(要宣告成 tri 才行)。

但實務上,匯流排匯接點(例如兩個 master 驅動同一條 bus 的 valid)天然需要多驅動語意。SV 的解法是把成員宣告成 tri,並用 wand / wor 指定解析方式。這引入了一整套新的語意(強度解析、四態),也就是第 1.3 節批評的「隱含規則」。

(三)modport 的方向定義與實際驅動者必須手動保持一致。

在 SV 中,「誰驅動這個成員」不是由語言決定的,而是由連接關係決定的。工程師必須:

  1. 正確宣告 modport 的方向;
  2. 正確連接 master 與 slave;
  3. 保證連接方向與 modport 宣告一致。

三者不一致時,工具不一定會報錯。例如,把 master 的 awvalid(output)接到 slave 的 awvalid(input),這是正確的;但如果你不小心寫了兩份 master 接到同一個 slave,SV 不會阻止——結果是 awvalid 被兩個來源驅動,而 modport 檢查完全通過。

(四)介面成員的「這個成員現在是誰的」不在型別裡。

一個 axi_if.slave 與一個 axi_if.master 是不同的型別,這點 SV 做對了。但SV 沒有辦法表達「這兩個介面應該連在一起」這件事——連接的正確性完全靠工程師的紀律。


Veryl 介面語法#

Veryl 的介面系統在 SV 的基礎上做了兩項根本性強化:

  1. modport 承擔「驅動權威」的角色——連接的方向由 modport 決定,衝突會被報錯。
  2. connect 操作符——連接兩個介面時,方向自動比對,衝突自動偵測。

基本形態#

interface InterfaceA {
    var a : logic
    var b : logic

    modport master {
        a : output
        b : input
    }

    modport slave {
        a : input
        b : output
    }
}

對照 SV:

interface InterfaceA;
    logic a;
    logic b;

    modport master (
        output a,
        input  b
    );
    modport slave (
        input  a,
        output b
    );
endinterface

術語對照:

概念 SV Veryl
介面成員 logic a; var a : logic
modport modport master (output a, input b); modport master { a : output b : input }
介面參數 parameter parameter
連接 .bus (bus) 隱含 connect a <> b;

「介面成員用 var 宣告」這件事值得注意。 在 SV 中,介面成員用 logic 宣告,而 logic 隱含地是 variable(可以被 always_ff 賦值)。Veryl 顯式寫出 var,與模組內部變數的宣告一致,讓「這是一個有值的儲存體」這件事明確。

帶參數的介面#

interface DataBus #(
    parameter WIDTH : u32 = 32,
) {
    var data  : logic<WIDTH>
    var valid : logic
    var ready : logic

    modport master {
        data  : output
        valid : output
        ready : input
    }

    modport slave {
        data  : input
        valid : input
        ready : output
    }
}

介面的實例化#

在 SV 中,介面必須在模組外部或內部被實例化,然後透過埠傳遞:

DataBus #(.WIDTH(64)) bus (.*);
module Sub (DataBus bus); ... endmodule

在 Veryl 中,介面與模組一樣是頂層項目,透過 inst 實例化,並且可以在模組的埠清單中以型別出現:

module Producer (
    i_clk : clock,
    o_bus : output DataBus,
) {
}

更常見的做法是直接在模組內實例化介面,並透過 connect 連接:

module Top (
    i_clk : clock,
) {
    inst mst_if : DataBus #(
        .WIDTH (64),
    );
    inst slv_if : DataBus #(
        .WIDTH (64),
    );

    inst u_producer : Producer (
        i_clk : i_clk,
        o_bus : mst_if,
    );

    inst u_consumer : Consumer (
        i_clk : i_clk,
        i_bus : slv_if,
    );

    connect mst_if <> slv_if;
}

modport 的方向語意:從「檢查視圖」到「驅動權威」#

這是 Veryl 與 SV 最根本的差異。

SV:modport 是檢查用的視圖#

在 SV 中,modport 的作用是限制程式碼裡可以讀寫哪些成員。它是一個「存取控制清單」:

  • 在 axi_if.slave 的視圖下,awready 是 output,所以你可以寫它;awaddr 是 input,所以你可以讀它但不能寫。

但 modport 不決定「誰驅動這個成員」。SV 中,驅動關係由連接的拓撲決定,而這個拓撲不被語言檢查。

Veryl:modport 決定驅動權威#

在 Veryl 中,當你用 connect 連接兩個介面時,方向是由 modport 決定的,並且:

兩個被連接的介面,如果對同一個成員宣告了衝突的方向,就會產生編譯錯誤。

具體來說:

connect mst_if <> slv_if;

這行做了什麼?

  1. 取 mst_if 的預設 modport(master)與 slv_if 的預設 modport(slave);
  2. 對每個成員,比對兩邊的方向:
  3. master 的 output 成員對上 slave 的 input 成員 → 合法;
  4. master 的 input 成員對上 slave 的 output 成員 → 合法;
  5. master 的 output 對上 slave 的 output → 錯誤(兩個驅動者);
  6. master 的 input 對上 slave 的 input → 錯誤(兩個讀取者,無驅動者)。

這個檢查是語言強制的,不需要工程師記住任何規則。

具體例子:方向衝突被攔截#

interface InterfaceA {
    var a : logic

    modport master {
        a : output
    }

    modport slave {
        a : input
    }
}
inst mst_if : InterfaceA;
inst slv_if : InterfaceA;

connect mst_if <> slv_if;   // 合法:master.output 對 slave.input

但如果:

connect mst_if <> mst_if;   // 錯誤:output 對 output

編譯器會報錯,訊息大致會指出「成員 a 在兩邊都是 output」。

在 SV 中,這種錯誤是完全合法的。 兩個 master 接在一起,SV 會把它視為合法的多驅動 net(對 logic 則是錯誤,但那是 SV 的另一個規則,而且規則本身不一致:logic 報錯、tri 不報錯、wand 不報錯)。

這項機制的意義#

回顧第 1.3 節列出的「同一個字元,不同的語意」清單——SV 的多驅動規則正是其中最令人困惑的一條:

宣告 多驅動
logic x; 錯誤
tri x; 允許(強度解析)
wand x; 允許(線與)
wor x; 允許(線或)
wire x; 允許

而 Veryl 的介面設計從根本上消除了這個問題在匯流排場景中的出現:

如果連接方向是明確且唯一的,就沒有「多驅動」這個狀態可言。

這不是把 SV 的規則改嚴格了,而是把資訊放對了位置。SV 裡「這個成員被幾個人驅動」是隱含的、由拓撲決定的;Veryl 裡它是顯式的、由 modport 宣告與 connect 檢查共同確定的。


..converse 與 ..same:避免重複列出方向#

問題#

考慮一個典型的非對稱匯流排。master 與 slave 的方向在某些成員上是相反的。若要寫兩個完整的 modport,會有大量重複:

interface DataBus {
    var data  : logic<32>
    var valid : logic
    var ready : logic

    modport master {
        data  : output
        valid : output
        ready : input
    }

    modport slave {
        data  : input
        valid : input
        ready : output
    }
}

這裡 master 與 slave 每個成員的方向都剛好相反。這在硬體中是常見的(每一條線都是一對),但把它寫兩遍是冗餘的,而且兩份宣告可能不同步——改了 master 忘了改 slave,錯誤會在很晚才被發現。

..converse(master)#

interface DataBus {
    var data  : logic<32>
    var valid : logic
    var ready : logic

    modport master {
        data  : output
        valid : output
        ready : input
    }

    modport slave {
        ..converse(master)
    }
}

..converse(master) 的意思是:「這個 modport 的方向,是 master 的每個成員方向的反轉」。

  • master 的 data: output → slave 的 data: input;
  • master 的 ready: input → slave 的 ready: output。

好處:方向只宣告一次。之後若要新增一個成員,只需在 master 裡加一行,slave 自動跟著反轉。不可能不同步。

..same(master)#

modport monitor {
    ..same(master)
}

..same(master) 表示「方向與 master 完全相同」。

用途:監看(monitor)角色。在匯流排的系統驗證中,monitor 需要同時讀取 master 驅動與 slave 驅動的所有訊號。此時它需要的正是一份「把兩邊都當成 input」的視圖。

..same 的一個重要應用場景是方向完全對稱的匯流排(例如某些 SPI 模式、或是兩端角色可互換的介面):

interface SpiBus {
    var mosi : logic
    var miso : logic
    var sck  : logic

    modport controller {
        mosi : output
        miso : input
        sck  : output
    }

    modport peripheral {
        mosi : input
        miso : output
        sck  : input
    }

    modport bidirectional {
        ..same(controller)
    }
}

..input / ..output:預設成員語法#

有時候某個 modport 需要「除了列出的幾個之外,其餘全部視為 input」。這在 SV 裡要逐個列出所有成員,非常冗長。

interface DataBus {
    var data  : logic<32>
    var valid : logic
    var ready : logic

    modport master {
        data  : output
        valid : output
        ready : input
    }

    modport monitor {
        ..input
        data  : output   // 例外:data 仍是 output
    }
}

..input 的意思是「預設所有成員為 input」,然後個別成員可以覆寫。

這在實務上的重要性是:當介面新增欄位時,所有使用 ..input 的 modport 自動涵蓋新欄位,不需要維護多處。

這個機制的價值不在節省行數,而在於「未來相容性」。 介面會演化成。宣告明確清單的 modport 在介面新增成員時,會留下未定義的縫隙;使用 ..input / ..output / ..converse / ..same 的 modport 則自動保持一致。

四個便利語法的對照#

語法 意義 典型用途
..input 預設全部為 input 監看、被動觀察者
..output 預設全部為 output 單向驅動者
..same(m) 與 modport m 方向相同 角色可互換的介面
..converse(m) 與 modport m 方向相反 對稱匯流排的兩端

connect 與 <> 操作符#

基本用法#

connect mst_if <> slv_if;

<> 這個符號在 Veryl 裡有兩個不同的用途,這一點需要分清楚:

語境 用途
型別位置 logic<8> 打包陣列(型別的一部分)
敘述位置 a <> b 連接兩個介面實例

它們不衝突,因為語境完全不同:型別位置出現在 : 之後或 #(...) 內,敘述位置出現在敘述上下文。

connect 做了什麼#

connect mst_if <> slv_if; 的完整語意:

  1. 取出兩邊的 modport。 預設取第一個宣告的 modport(master 與 slave),也可以顯式指定。
  2. 比對成員。 對每個成員,檢查兩邊的方向是否互補(一個 output 對一個 input)。
  3. 生成連線。 在產生的 SystemVerilog 中,這對應到 SV 的 assign 語句。

顯式指定 modport#

connect mst_if <> slv_if;

也可以指定:

connect mst_if.master <> slv_if.slave;

這在介面有多個 modport 且想明確表達時使用。

<> 在 always_comb 中的使用#

除了 connect 敘述,<> 也可以在組合邏輯區塊中用來做成員間的直接對應賦值:

always_comb {
    slv_if.valid <> mst_if.valid;
    slv_if.data  <> mst_if.data;
}

這種寫法的好處是一次宣告完成多個成員的對應,且方向仍然受到檢查(若方向衝突,會報錯)。

但必須注意:在 always_comb 中用 <> 做賦值,實際上是在連續賦值語意與程序式賦值語意之間劃線。若要明確使用 assign,語意更清楚:

assign slv_if.valid = mst_if.valid;
assign slv_if.data  = mst_if.data;

實務建議: 在模組內部做成員對應時,用 assign 更清楚;connect 敘述留給「介面與介面之間」的完整連接。這個區分的好處是讓讀程式碼的人立刻知道「這是連續賦值還是過程式賦值」。

對照 SV:手動成員賦值#

connect mst_if <> slv_if; 在 SV 中需要寫成:

assign slv_if.valid = mst_if.valid;
assign slv_if.data  = mst_if.data;
assign slv_if.ready = mst_if.ready;

這就是 SV 介面設計中「方向檢查形同虛設」的根源:既然要做手動賦值,那麼「誰驅動誰」就完全由每一行賦值決定,modport 的角色只剩下「限制程式碼裡能寫哪些成員」。

Veryl 的 connect 把這三行合成一行而且保留了方向檢查。這是「語法簡化同時增強檢查」的一個漂亮例子。


mixin:把通道組合成複合介面#

mixin 是 Veryl 介面系統的高階用法,也是相對於 SV 最有特色的一項。

問題#

考慮一個典型的記憶體匯流排,它由多個通道組成:

  • 命令通道(address + write/read 指示)
  • 資料通道(write data、read data)
  • 回應通道(ack、error)

如果每個通道都是獨立介面,模組就要有多個埠,連接也要寫多行。

解法:定義通道介面,再 mixin 進主介面#

interface CmdChannel {
    var addr  : logic<32>
    var write : logic

    modport master {
        addr  : output
        write : output
    }

    modport slave {
        addr  : input
        write : input
    }
}

interface DataChannel {
    var wdata : logic<32>
    var rdata : logic<32>

    modport master {
        wdata : output
        rdata : input
    }

    modport slave {
        wdata : input
        rdata : output
    }
}

interface AckChannel {
    var ack   : logic
    var error : logic

    modport master {
        ack   : input
        error : input
    }

    modport slave {
        ack   : output
        error : output
    }
}
interface MemBus {
    mixin CmdChannel;
    mixin DataChannel;
    mixin AckChannel;

    modport master {
        ..converse(slave)
    }

    modport slave {
        ..converse(master)
    }
}

mixin CmdChannel; 的意思是「把 CmdChannel 介面的所有成員與 modport 併入 MemBus」。

好處:

  1. 通道可以獨立重用。 CmdChannel 可以同時被 MemBus 與另一個 ReadOnlyBus 使用。
  2. 組合介面的結構由名稱表達。 看 MemBus 的定義就知道它由哪些通道組成,不需要展開成 20 個欄位。
  3. 方向宣告只寫一次。 通道介面裡定義 master/slave,組合介面用 ..converse 一次反轉全部。

產生的 SystemVerilog#

mixin 在 SV 裡沒有直接對應,因此 Veryl 會展開成一個完整的 SV 介面:

interface MemBus;
    logic [31:0] addr;
    logic        write;
    logic [31:0] wdata;
    logic [31:0] rdata;
    logic        ack;
    logic        error;

    modport master (
        output addr,
        output write,
        output wdata,
        input  rdata,
        input  ack,
        input  error
    );

    modport slave (
        input  addr,
        input  write,
        input  wdata,
        output rdata,
        output ack,
        output error
    );
endinterface

這個展開的行為很重要:它表示 mixin 是編譯期(其實是產生期)的機制,而非執行期的動態組合。產生的 SV 是標準的、可被任何 SV 工具理解的介面。

代價:展開後的 SV 裡沒有「這是 mixin 來的」的痕跡。所以若要修改通道,必須回頭改通道介面的定義,而不是改展開後的 SV。這是「原始碼為唯一真實來源」原則的必然結果。


介面參數化與 ::<>#

泛型介面#

interface InterfaceA::<W: u32> {
    var data : logic<W>

    modport master {
        data : output
    }

    modport slave {
        data : input
    }
}

InterfaceA::<W: u32> 的語法拆解:

  • InterfaceA — 介面名稱;
  • ::<...> — 泛型參數列表(泛型語法,第 2.2 節說明過);
  • W: u32 — 參數 W,型別 u32。

注意這裡的 ::<> 與型別裡的 <>(打包陣列)在視覺上容易混淆。這就是第 2.2 節說明為什麼位移運算子要用 <: / >: 的同一類問題。

實例化#

inst a_if : InterfaceA::<8>;
inst b_if : InterfaceA::<16>;

這兩個是不同的型別(InterfaceA<u32> 與 InterfaceA<u16>),不能互相連接。這是刻意的:位寬不同的匯流排不應該被連在一起。

泛型參數的預設值#

interface InterfaceA::<W: u32 = 32> {
    var data : logic<W>
    ...
}

省略參數時使用預設值:

inst a_if : InterfaceA;

產生的 SystemVerilog#

泛型介面在 SV 中對應參數化介面:

interface InterfaceA #(
    parameter WIDTH = 32
) (
);
    logic [WIDTH-1:0] data;

    modport master (output data);
    modport slave  (input  data);
endinterface

注意名稱的對應:Veryl 的泛型參數 W 變成 SV 的 WIDTH。這是為了避免 SV 的參數命名慣例衝突(SV 的 W 太短且常與其他參數撞名)。


介面陣列#

inst a_if : InterfaceA::<8> [4];

這會產生 4 個 InterfaceA 實例。對應 SV:

InterfaceA #(.WIDTH(8)) a_if [4] ();

在模組中引用陣列介面實例的元素:

var r_first_data : logic<8>
assign r_first_data = a_if[0].data;

連接陣列介面:

inst mst_ifs : InterfaceA::<8> [4];
inst slv_ifs : InterfaceA::<8> [4];

connect mst_ifs <> slv_ifs;

connect 作用於陣列時,是逐元素對應(mst_ifs[0] 對 slv_ifs[0],依此類推)。這與 SV 中把 unpacked 陣列埠互連的語意一致。


透過 modport 呼叫介面內的函式#

介面不只能承載資料,也可以承載行為。

定義#

interface SimpleBus {
    var valid : logic
    var ready : logic

    modport master {
        valid : output
        ready : input

        /// 送出一次握手
        fn send() -> bool {
            valid = 1;
            return true;
        }
    }

    modport slave {
        valid : input
        ready : output

        fn recv() -> bool {
            return ready;
        }
    }
}

import 方向#

在模組中,若要呼叫介面的函式,需要以 import 方向引入該介面:

module Master (
    i_bus : import SimpleBus.master,
) {
    always_ff {
        if i_bus.send() {
            // ...
        }
    }
}

import 這個方向關鍵字在 Veryl 裡是介面專用的,它表達的是:

「這個模組把介面當作行為的來源來使用(呼叫它的方法),而不是把它當作資料通道。」

對應 SV 的做法是把 modport 當作型別的一部分(SimpleBus.master bus),然後呼叫 bus.send()。SV 的 modport 型別其實不允許呼叫方法(SV 的介面方法要透過 task / function,且 modport 限定哪些 task 可以被呼叫)。

Veryl 的 import 方向把這件事顯式化,而且和 connect 的方向檢查形成一個完整的體系:

方向 角色 對應 SV
input 資料來源 input
output 資料目的 output
inout 雙向資料 inout
import 行為來源(呼叫函式) modport 的 task/function 限定

呼叫#

module Master (
    i_bus : import SimpleBus.master,
) {
    var r_done : logic

    always_comb {
        r_done = i_bus.send();
    }
}

i_bus.send() 會被展開成對介面成員的存取,產生的 SV 等價於手寫的成員賦值。

這裡有一個重要的實務提醒:把「行為」放進介面,雖然語法上很方便,但會讓介面從「協定描述」變成「帶有實作的物件」。在硬體設計中,介面的方法若帶有實作邏輯,會讓「這個介面是什麼」的問題變得模糊。

實務上的建議是:把協定的「握手規則」放在介面的註解與斷言裡,把「行為」留給使用它的模組。 介面的方法應該是對協定的封装(例如「這次握手成功了吗?」),而不是功能的實現。


生成的 SystemVerilog 對照#

對照一:介面定義#

Veryl:

interface DataBus #(
    parameter WIDTH : u32 = 32,
) {
    var data  : logic<WIDTH>
    var valid : logic
    var ready : logic

    modport master {
        data  : output
        valid : output
        ready : input
    }

    modport slave {
        data  : input
        valid : input
        ready : output
    }
}

生成的 SystemVerilog:

interface DataBus #(
    parameter WIDTH = 32
);
    logic [WIDTH-1:0] data;
    logic              valid;
    logic              ready;

    modport master (
        output data,
        output valid,
        input  ready
    );

    modport slave (
        input  data,
        input  valid,
        output ready
    );
endinterface

對照二:connect#

Veryl:

connect mst_if <> slv_if;

生成的 SystemVerilog:

assign slv_if.data  = mst_if.data;
assign slv_if.valid = mst_if.valid;
assign slv_if.ready = mst_if.ready;

再次強調這個對照的重要性:產生的 SV 是手動賦值,方向檢查在編譯期就已經完成,因此 SV 裡不需要任何 tri 或多驅動的處理。

對照三:介面實例化與模組埠#

Veryl:

module Producer (
    i_clk : clock,
    o_bus : output DataBus,
) {
}

生成的 SystemVerilog:

module Producer (
    input  logic    clk,
    output DataBus  bus
);
endmodule

設計討論:握手訊號該不該放進介面#

這是一個真實的設計爭論,值得單獨討論,因為它沒有標準答案。

爭議的兩端#

立場 A:握手應該放進介面。

理由:

  1. 協定是介面的一部分。 一個 valid/ready 匯流排,valid 與 ready 必須成對出現,且它們的語意(何時可以傳輸)與資料本身是綁定的。把資料放進介面而握手留在模組外部,會讓「這個匯流排是什麼」變得不完整。
  2. 重複消除。 五十個模組都用同一個匯流排,每個模組都要自己寫一遍 valid/ready 的處理邏輯。放進介面就只寫一次。
  3. 一致性保證。 如果介面定義了握手,則所有使用它的模組都遵守同一個規則。

立場 B:握手不該放進介面(或至少不該是介面的必要部分)。

理由:

  1. 協定的彈性。 有些匯流排是 valid/ready,有些是 request/ack + grant,有些是信用式(credit-based)。強行統一到一種握手形式,反而限制了設計。
  2. 耦合過度。 一旦 valid/ready 在介面裡,資料路徑與控制路徑就被綁死了。純資料路徑的模組(例如暫存器陣列)不該被迫理解握手規則。
  3. 重置與時序的複雜度。 握手語意牽涉時序(例如 valid 必須在 ready 之前或同時拉高),把它放在介面裡會讓介面不再是「線的集合」,而變成「協定機器」。

一個判準#

我們建議一個可操作的判準:

把「線」放進介面,把「規則」放在使用者的斷言與註解裡;但當某個握手形式在多個匯流排中重複出現,且它的規則是固定的(非協商式的),把它放進介面是划算的。

這個判準的依據是變動性:

  • 規則穩定、且重複出現 → 放進介面,收益大。
  • 規則會隨設計演進 → 留在外部,介面保持穩定。

一個實務上的具體做法是兩層介面:

// 底層:純資料與旗標的線
interface DataChannel {
    var data : logic<32>
    modport master { data : output }
    modport slave  { data : input }
}

// 上層:包含固定握手的複合介面
interface ValidReadyBus {
    mixin DataChannel;
    var valid : logic
    var ready : logic

    modport master {
        ..converse(slave)
        valid : output
        ready : input
    }

    modport slave {
        ..converse(master)
        valid : input
        ready : output
    }
}

底層可以在需要時單獨使用(純資料路徑),上層提供便利的握手封裝。

另一個重要提醒:介面裡的握手語意必須在文件裡說清楚。/// 文件註解在這裡特別有價值:

interface ValidReadyBus {
    /// 資料在 valid 與 ready 同時為高時傳輸
    /// 送出方必須在 valid 拉高後保持 valid,直到看到 ready
    mixin DataChannel;
    var valid : logic
    var ready : logic
    ...
}

語法能保證的是結構(誰驅動誰、方向不衝突),不能保證的是語意(握手規則是什麼)。 這一點必須靠文件與斷言,而 /// 讓文件與程式碼待在同一個檔案裡。


實作練習:Request / Response 匯流排#

題目#

用介面 + connect 建立一個簡單的 request/response 匯流排:

  • request 通道:requester 送出 req 與 addr,provider 回覆 ack。
  • response 通道:provider 送出 data 與 rsp_valid,requester 回覆 rsp_ready。
  • 介面需要 mixin 兩個通道。
  • 兩個模組:Requester 與 Provider。

解答:介面定義#

interface ReqBus {
    var req  : logic
    var addr : logic<32>

    modport master {
        req  : output
        addr : output
    }

    modport slave {
        req  : input
        addr : input
    }
}

interface RspBus {
    var data      : logic<32>
    var rsp_valid : logic
    var rsp_ready : logic

    modport master {
        data      : output
        rsp_valid : output
        rsp_ready : input
    }

    modport slave {
        data      : input
        rsp_valid : input
        rsp_ready : output
    }
}

interface RequestResponse {
    mixin ReqBus;
    mixin RspBus;

    modport master {
        ..converse(slave)
    }

    modport slave {
        ..converse(master)
    }
}

解答:Provider 模組#

module Provider (
    i_clk  : clock,
    i_rst_n: reset,
    i_bus  : input  RequestResponse.slave,
) {
    var r_data : logic<32>

    always_ff {
        if !i_rst_n {
            i_bus.data      = 0;
            i_bus.rsp_valid = 0;
        } else {
            if i_bus.req & !i_bus.rsp_valid {
                i_bus.data      = i_bus.addr ^ 32'hA5A5_A5A5;
                i_bus.rsp_valid = 1;
            } else if i_bus.rsp_valid & i_bus.rsp_ready {
                i_bus.rsp_valid = 0;
            }
        }
    }
}

注意:這裡的 i_bus 宣告為 input RequestResponse.slave,表示「本模組從這個介面讀資料,並且它是 slave 角色」。在 SV 裡對應 RequestResponse.slave bus。

解答:Requester 模組#

module Requester (
    i_clk  : clock,
    i_rst_n: reset,
    o_bus  : output RequestResponse.master,
    o_result: output logic<32>,
) {
    var r_addr  : logic<32>
    var r_state : logic<2>

    always_ff {
        if !i_rst_n {
            o_bus.req  = 0;
            o_bus.addr = 0;
            r_addr      = 0;
            r_state     = 0;
        } else {
            case r_state {
                2'd0: {
                    if o_bus.rsp_ready {
                        o_bus.req  = 1;
                        o_bus.addr = r_addr;
                        r_state    = 2'd1;
                    }
                }
                2'd1: {
                    if o_bus.rsp_valid {
                        o_bus.req = 0;
                        r_state   = 2'd2;
                    }
                }
                2'd2: {
                    if o_bus.rsp_ready {
                        r_addr    = r_addr + 1;
                        o_result  = o_bus.data;
                        r_state   = 2'd0;
                    }
                }
                default: {
                    r_state = 2'd0;
                }
            }
        }
    }
}

解答:頂層整合#

module Top (
    i_clk  : clock,
    i_rst_n: reset,
    o_result: output logic<32>,
) {
    inst bus : RequestResponse;

    inst u_requester : Requester (
        i_clk   : i_clk,
        i_rst_n : i_rst_n,
        o_bus   : bus,
        o_result: o_result,
    );

    inst u_provider : Provider (
        i_clk  : i_clk,
        i_rst_n: i_rst_n,
        i_bus  : bus,
    );

    connect bus <> bus;
}

關鍵:connect bus <> bus; 這行。 這是自我連接:requester 端是 master 角色、provider 端是 slave 角色,兩者由 connect 統一檢查方向。

等等——這裡有個細節值得說明。 在實際的 Veryl 程式碼中,Requester 的 o_bus 埠型別是 output RequestResponse.master,而 Provider 的 i_bus 埠型別是 input RequestResponse.slave。它們不是同一個型別(master modport 與 slave modport 是不同型別),所以不能直接連接。

正確的整合方式是分別實例化兩個介面,再用 connect 連起來:

module Top (
    i_clk   : clock,
    i_rst_n : reset,
    o_result: output logic<32>,
) {
    inst mst_bus : RequestResponse;
    inst slv_bus : RequestResponse;

    inst u_requester : Requester (
        i_clk   : i_clk,
        i_rst_n : i_rst_n,
        o_bus   : mst_bus,
        o_result: o_result,
    );

    inst u_provider : Provider (
        i_clk  : i_clk,
        i_rst_n: i_rst_n,
        i_bus  : slv_bus,
    );

    connect mst_bus <> slv_bus;
}

這正是 connect 存在的原因:因為 master 與 slave 是不同的型別(這是正確的型別設計),所以不能用一般埠連接語法把它們接起來——必須用 connect,而 connect 會做方向檢查。

若寫成 connect mst_bus <> slv_bus; 而把 master 接 master,語言會報錯。 這正是本章開頭描述的那個「驅動權威」機制所帶來的實際價值。

生成的 SystemVerilog(摘要)#

interface RequestResponse;
    logic        req;
    logic [31:0] addr;
    logic [31:0] data;
    logic        rsp_valid;
    logic        rsp_ready;

    modport master (
        output req,
        output addr,
        input  data,
        input  rsp_valid,
        output rsp_ready
    );

    modport slave (
        input  req,
        input  addr,
        output data,
        output rsp_valid,
        input  rsp_ready
    );
endinterface
assign slv_bus.req       = mst_bus.req;
assign slv_bus.addr      = mst_bus.addr;
assign mst_bus.data      = slv_bus.data;
assign mst_bus.rsp_valid = slv_bus.rsp_valid;
assign slv_bus.rsp_ready = mst_bus.rsp_ready;

注意 mixin 展開後的方向:master 的 ..converse(slave) 使得 master 端在 ReqBus 上是 output(req、addr),在 RspBus 上是相反的(data、rsp_valid 是 input,rsp_ready 是 output)。這個複合方向正是 request/response 匯流排的正確語意,而它是由 ..converse 自動產生的——不需要手寫。


本章小結#

  1. 介面把協定封裝成一等物件,解決散落訊號的五個問題。
  2. SV 的 modport 只是檢查視圖,Veryl 的 modport 是驅動權威——方向由 modport 宣告,衝突由 connect 檢查。
  3. 多驅動問題在匯流排場景中被根本消除,因為連接方向是明確且唯一的。
  4. ..converse / ..same / ..input / ..output 四個便利語法讓方向宣告只寫一次,且在介面演進時自動保持一致。
  5. connect a <> b; 取代 SV 中一整段手動成員賦值,且保留了方向檢查。
  6. mixin 讓通道可重用、可組合,且展開成標準 SV 介面。
  7. 泛型介面 InterfaceA::<W> 讓不同位寬的介面成為不同型別,防止錯誤連接。
  8. import 方向讓模組可以呼叫介面的方法,但實務上應謹慎,避免介面變成「帶實作的物件」。
  9. 判準:線放進介面,規則靠文件與斷言;穩定且重複的握手規則才值得放進介面。

下一節(3.3)進入專案組織:package、別名、可視性控制,以及專案前綴機制。這些是從「單一模組」走向「大型專案」的必要工具。


下一節:第 3.3 節「package、別名與可視性控制」