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,
) {
}
這裡的問題不只是「埠很多」:
- 協定被稀釋成獨立訊號。 「
awvalid與awready必須同時握手」這個協定的本質,在埠清單裡完全看不出來。 - 容易接錯。 把
wstrb接到wvalid上是型別允許的、編譯允許的,只有在整合時才發現。 - 介面定義沒有單一來源。 如果有五個模組都需要 AXI-Lite,五份埠清單要各自維護,改一個欄位要改五處。
- 文件要另外寫。 協定的語意只能存在於工程師的腦中或 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 中,「誰驅動這個成員」不是由語言決定的,而是由連接關係決定的。工程師必須:
- 正確宣告 modport 的方向;
- 正確連接 master 與 slave;
- 保證連接方向與 modport 宣告一致。
三者不一致時,工具不一定會報錯。例如,把 master 的 awvalid(output)接到 slave 的 awvalid(input),這是正確的;但如果你不小心寫了兩份 master 接到同一個 slave,SV 不會阻止——結果是 awvalid 被兩個來源驅動,而 modport 檢查完全通過。
(四)介面成員的「這個成員現在是誰的」不在型別裡。
一個 axi_if.slave 與一個 axi_if.master 是不同的型別,這點 SV 做對了。但SV 沒有辦法表達「這兩個介面應該連在一起」這件事——連接的正確性完全靠工程師的紀律。
Veryl 介面語法#
Veryl 的介面系統在 SV 的基礎上做了兩項根本性強化:
- modport 承擔「驅動權威」的角色——連接的方向由 modport 決定,衝突會被報錯。
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;
這行做了什麼?
- 取
mst_if的預設 modport(master)與slv_if的預設 modport(slave); - 對每個成員,比對兩邊的方向:
- master 的
output成員對上 slave 的input成員 → 合法; - master 的
input成員對上 slave 的output成員 → 合法; - master 的
output對上 slave 的output→ 錯誤(兩個驅動者); - 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; 的完整語意:
- 取出兩邊的 modport。 預設取第一個宣告的 modport(
master與slave),也可以顯式指定。 - 比對成員。 對每個成員,檢查兩邊的方向是否互補(一個 output 對一個 input)。
- 生成連線。 在產生的 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」。
好處:
- 通道可以獨立重用。
CmdChannel可以同時被MemBus與另一個ReadOnlyBus使用。 - 組合介面的結構由名稱表達。 看
MemBus的定義就知道它由哪些通道組成,不需要展開成 20 個欄位。 - 方向宣告只寫一次。 通道介面裡定義 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:握手應該放進介面。
理由:
- 協定是介面的一部分。 一個 valid/ready 匯流排,valid 與 ready 必須成對出現,且它們的語意(何時可以傳輸)與資料本身是綁定的。把資料放進介面而握手留在模組外部,會讓「這個匯流排是什麼」變得不完整。
- 重複消除。 五十個模組都用同一個匯流排,每個模組都要自己寫一遍 valid/ready 的處理邏輯。放進介面就只寫一次。
- 一致性保證。 如果介面定義了握手,則所有使用它的模組都遵守同一個規則。
立場 B:握手不該放進介面(或至少不該是介面的必要部分)。
理由:
- 協定的彈性。 有些匯流排是 valid/ready,有些是 request/ack + grant,有些是信用式(credit-based)。強行統一到一種握手形式,反而限制了設計。
- 耦合過度。 一旦 valid/ready 在介面裡,資料路徑與控制路徑就被綁死了。純資料路徑的模組(例如暫存器陣列)不該被迫理解握手規則。
- 重置與時序的複雜度。 握手語意牽涉時序(例如
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 自動產生的——不需要手寫。
本章小結#
- 介面把協定封裝成一等物件,解決散落訊號的五個問題。
- SV 的 modport 只是檢查視圖,Veryl 的 modport 是驅動權威——方向由 modport 宣告,衝突由
connect檢查。 - 多驅動問題在匯流排場景中被根本消除,因為連接方向是明確且唯一的。
..converse/..same/..input/..output四個便利語法讓方向宣告只寫一次,且在介面演進時自動保持一致。connect a <> b;取代 SV 中一整段手動成員賦值,且保留了方向檢查。- mixin 讓通道可重用、可組合,且展開成標準 SV 介面。
- 泛型介面
InterfaceA::<W>讓不同位寬的介面成為不同型別,防止錯誤連接。 import方向讓模組可以呼叫介面的方法,但實務上應謹慎,避免介面變成「帶實作的物件」。- 判準:線放進介面,規則靠文件與斷言;穩定且重複的握手規則才值得放進介面。
下一節(3.3)進入專案組織:package、別名、可視性控制,以及專案前綴機制。這些是從「單一模組」走向「大型專案」的必要工具。
下一節:第 3.3 節「package、別名與可視性控制」