3.1 模組定義與埠宣告語法#
模組語法的三十年演進#
在進入 Veryl 之前,先把「模組宣告」這件事在三代語言裡的樣子並排看一次。這不是歷史懷舊,而是因為 Veryl 的模組語法是三者的交集加上兩項重排,看懂了差異,就看懂了每一條規則背後的歷史包袱。
Verilog-1995:分離式宣告#
module counter (clk, rst, d, q);
input clk;
input rst;
input [7:0] d;
output [7:0] q;
reg [7:0] q;
always @(posedge clk)
q <= d;
endmodule
這是 Verilog 的原始形式:埠宣告與型別宣告分離。方向與名稱寫在模組頭,型別(reg / wire)寫在模組體。
它的問題是顯而易見的:閱讀模組介面時,你只看到 input [7:0] d,看不到它是 reg 還是 wire;要理解一個埠的完整型別,必須在模組頭與模組體之間跳來跳去。
Verilog-2001 / SystemVerilog:ANSI 風格#
module counter (
input logic clk,
input logic rst,
input logic [7:0] d,
output logic [7:0] q
);
always_ff @(posedge clk)
q <= d;
endmodule
ANSI 風格把方向、型別、寬度、名字集中在模組頭,介面自我完整。這是 Verilog-2001 的一個重大改進,也是今天所有 SystemVerilog 程式碼的寫法。
但它留下了兩個問題:
(一)名字在最後。 閱讀時最先看到的是 input logic [7:0] 這種描述性前綴,訊號的身分(d)反而是最後才出現。
(二)方向在最前,型別在中間,名字在最後——順序是歷史的產物,不是邏輯的必然。
Veryl#
module Counter (
i_clk : clock,
i_rst : reset,
i_d : input logic<8>,
o_q : output logic<8>,
) {
always_ff {
o_q = i_d;
}
}
名字在最前,方向與型別在後,型別本身用「名稱在前」的次序展開。 整個語言的宣告風格在這裡第一次統一。
Veryl 模組的完整語法#
module 名稱 # ( 參數列表 ) ( 埠列表 ) { 主體 }
語法骨架#
module ModuleName #(
parameter WIDTH : u32 = 32,
) (
i_clk : clock,
i_rst_n : reset,
i_data : input logic<WIDTH>,
o_result: output logic<WIDTH>,
) {
// 主體
}
三個部分:
- 名稱:遵循 Rust 命名慣例,用大寫開頭的駝峰(
ModuleName)。Veryl 會在產生時加上專案前綴。 - 參數列表:
#(...),可省略。 - 埠列表:
(...),可省略(無埠模組)。 - 主體:
{ ... }。
完整的語法元素#
| 元素 | 語法 | 說明 |
|---|---|---|
| 模組名稱 | ModuleName | 駝峰命名 |
| 參數 | #( parameter NAME : TYPE = VALUE, ) | 可在實例化時覆寫 |
| 埠名稱 | i_name / o_name | 前綴慣例(工具不強制,但 lint 鼓勵) |
| 方向 | input / output / inout / import | 方向在名稱之後 |
| 型別 | logic<W> / logic [N] / u32 / clock / reset | 型別在方向之後 |
| 預設值 | i_en: input logic = 1 | 用於輸入埠 |
| 未連接 | i_x: _ | 刻意不連接 |
| 主體 | { ... } | 宣告與敘述 |
埠清單語法#
名稱在前、型別在後#
module Top (
i_data : input logic<10>,
o_result : output logic<10>,
) {
}
對照 SV:
module Top (
input logic [9:0] i_data,
output logic [9:0] o_result
);
語義完全相同,順序相反。 這個重排在生成的 SystemVerilog 中會被還原,因為 SV 只接受這種順序:
module Top (
input logic [9:0] i_data,
output logic [9:0] o_result
);
為什麼要這樣重排?
三個理由:
(一)名字是第一資訊。 在一個有 30 個埠的模組裡,工程師真正想找的是「那個叫 i_addr 的東西是幾位元」。名字在前讓這一眼完成。
(二)與語言內其他所有宣告一致。 Veryl 的內部變數、函式參數、實例連接,全部是「名稱在前」。這意味著一個統一的心智模型:
var r_state : logic<4> // 內部變數
inst u_fifo : Fifo #(...) // 實例
function f ( a : u32 ) // 參數
每一處都是「名字 : 型別」。學會一次,到處適用。
(三)它讓「方向」變成一個可省略的修飾詞。 在 SV 中 input 是方向關鍵字,必須寫出;在 Veryl 中同樣寫出,但位置讓它不干擾到「這是什麼」的辨識。
方向#
| 方向 | 意義 | 對應 SV |
|---|---|---|
input | 資料流入本模組 | input |
output | 資料流出本模組 | output |
inout | 雙向 | inout |
import | 從介面匯入(僅介面用,見 3.2 節) | modport 的 import 方向 |
預設值#
module Top (
i_en : input logic = 1,
o_busy : output logic,
) {
}
對應 SV:
module Top (
input logic i_en = 1'b1,
output logic o_busy
);
預設值的語意:在 SystemVerilog 中,埠的預設值是連接時的替代值。也就是說,如果上層沒有連接這個埠,它會被接到 1'b1(在 SV 的 logic 情況下,因為 logic 是 variable 型的埠,預設值行為在某些工具有微妙差異)。
在 Veryl 裡,預設值的用法同樣是「上層沒給就用預設」。一個常見用途是讓模組成為不依賴外部設定的預設組態:
module Divider #(
parameter WIDTH : u32 = 32,
) (
i_clk : clock,
i_rst_n : reset,
i_en : input logic = 1,
i_by_zero : input logic = 0,
o_result : output logic<WIDTH>,
) {
}
這裡有一個值得提醒的陷阱:預設值讓「忘記連接」變成靜默通過。因此 Veryl 的 lint 有 missing_port 檢查,而 _ 才是明確表示「刻意不連接」的語法(見下)。預設值是「宣告式的設計意圖」,_ 是「宣告式的遺忘」,兩者語意完全不同。
未連接的埠:_#
module Top (
i_clk : clock,
i_data : input logic<8>,
_ : _,
) {
}
_ 表示這個埠刻意不連接。在 SV 裡,不連接的埠可以寫成 .unused(),或直接留空 .port_name(),也可以接一個空。這裡的語法是 _,一個只能用一次的佔位符。
為什麼需要這個? 因為 SV 的 .port_name() 寫法在埠很多時會變得非常難讀:
counter u_counter (
.clk (clk),
.rst (rst),
.d (d),
.q (q),
.unused0 (),
.unused1 (),
.unused2 (),
.unused3 ()
);
Veryl 的 _ 可以連續寫多個,讓未連接的埠不佔版面:
inst u_counter : Counter (
i_clk : i_clk,
i_rst : i_rst,
i_d : i_d,
o_q : o_q,
_ : _,
_ : _,
);
參數與常數#
兩個關鍵字:parameter 與 const#
| 關鍵字 | 位置 | 可覆寫 | 用途 |
|---|---|---|---|
parameter | 模組標頭 #(...) | 可(在實例化時) | 影響硬體結構的參數 |
const | 模組內部 | 不可 | 模組內的固定值 |
module Counter #(
parameter WIDTH : u32 = 32,
parameter INIT : u32 = 0,
) (
i_clk : clock,
o_cnt : output logic<WIDTH>,
) {
const STEP : u32 = 1;
var r_cnt : logic<WIDTH>
always_ff {
r_cnt = r_cnt + STEP;
}
assign o_cnt = r_cnt;
}
對應 SV:
module Counter #(
parameter WIDTH = 32,
parameter INIT = 0
) (
input logic clk,
output logic [WIDTH-1:0] cnt
);
localparam STEP = 1;
logic [WIDTH-1:0] r_cnt;
...
endmodule
為什麼模組內禁止 parameter#
這是 Veryl 的一條硬性規則,也是本章最值得討論的設計決策之一。
在 SystemVerilog 中,parameter 可以出現在模組內部:
module Counter #(parameter WIDTH = 32) (
...
);
parameter HALF = WIDTH / 2; // 模組內的 parameter
parameter LOCAL = 3; // 模組內的 parameter
在 Veryl 中,模組內部只能用 const。
理由有三層。
(一)可覆寫性造成語意不可預測。
SV 模組內的 parameter 理論上可以被 defparam 從外部覆寫:
defparam u_counter.LOCAL = 5;
defparam 是 Verilog 中臭名昭著的建構,因為它是就地修改:它改變的是「模組實例的內部狀態」,而這個修改在模組內部完全不可見。閱讀模組原始碼的人看到 parameter LOCAL = 3,但實際值可能是 5。
更糟的是,defparam 的作用點在階層路徑上,修改一個深層的參數可能改變整個子樹的結構。
(二)覆寫範圍與硬體結構的關係需要明確。
Veryl 的規則是:
只有在模組標頭
#(...)裡宣告的參數才能在實例化時覆寫;模組內部的一切固定值都是const,永遠不可覆寫。
這讓「這個值會不會被上層改掉」這個問題有一個單一的答案:只有模組標頭的參數會。
(三)區分「結構參數」與「實作常數」有實質意義。
一個參數是否應該可被上層改動,取決於它是否影響硬體結構:
| 類型 | 例子 | 應該可覆寫嗎 |
|---|---|---|
| 結構參數 | 位寬、深度、實例數 | 是——上層通常需要不同的版本 |
| 實作常數 | 閾值、係數、magic number | 否——它是設計的一部分 |
Veryl 的語法強制這個區分:結構參數放模組標頭,實作常數用 const 放模組內。
參數的型別#
module Fifo #(
parameter WIDTH : u32 = 32,
parameter DEPTH : u32 = 16,
) (
...
) {
}
SV 的參數沒有型別——parameter WIDTH = 32 中的 32 是無型別的整數常數,工具在推導時才決定它的位寬與型別(這是 SV 裡著名的「參數型別推導地獄」)。
Veryl 要求參數有明確型別。這帶來兩個好處:
- 參數的位寬明確。
parameter WIDTH: u32的WIDTH就是 32-bit,不會出現「參數的位寬隨上下文改變」。 - 可以宣告非整數參數。 例如
parameter RESET_VALUE: logic<8> = 0,或者布林參數parameter ENABLED: bool = true。
參數覆寫#
inst u_fifo : Fifo #(
.WIDTH (64),
.DEPTH (32),
) (
i_clk : i_clk,
o_data: o_data,
);
這是具名參數覆寫,與 SV 的 #(.WIDTH(64)) 對應。
Veryl 刻意不支援 defparam 式覆寫,也不支援位置式覆寫。 位置式覆寫(Fifo #(64, 32))的問題是:一旦參數順序改變,所有實例點都會靜默地改變語意。具名覆寫則不會有這個問題——即使參數順序改變,既有實例仍然正確。
型別語法#
logic 與打包/非打包#
var r_bit : logic // 1 bit
var r_byte : logic<8> // packed,8 bits
var r_words : logic [4] // unpacked,4 個元素的陣列
var r_matrix : logic<8, 4> // packed,8 bits x 4 個元素(packed 的二維)
完整對照:
| Veryl | SystemVerilog | 硬體意義 |
|---|---|---|
logic | logic | 1 bit |
logic<8> | logic [7:0] | 8-bit 向量 |
logic<8, 4> | logic [7:0] [3:0] | packed 二維陣列(8 x 4 bits) |
logic [4] | logic [3:0] [7:0] | unpacked 陣列(4 個 8-bit 元素) |
logic<8> [4] | logic [7:0] [3:0] | unpacked 陣列,元素是 packed 向量 |
logic [4, 2] | logic [3:0] [1:0] | unpacked 二維陣列 |
判斷規則:<> 內的維度是打包維度,[] 內的維度是非打包維度。
整數型別#
Veryl 提供一組固定寬度的整數型別:
| 型別 | 位寬 | 對應 SV |
|---|---|---|
u8 | 8 | logic [7:0](unsigned) |
u16 | 16 | logic [15:0] |
u32 | 32 | logic [31:0] |
u64 | 64 | logic [63:0] |
i8 / i16 / i32 / i64 | 有號版本 | integer / logic signed [..] |
為什麼需要它們? 因為 SV 裡寫一個 32-bit 的無號值,logic [31:0] 是 4-state 的(可以含 x / z),而很多計算其實是純二態的。u32 明確表達「這是一個二態的 32-bit 無號值」。
clock 與 reset:特殊型別#
module Top (
i_clk : clock,
i_rst_n : reset,
) {
}
這兩個是 Veryl 的特殊型別,不是 logic 的別名:
clock:宣告「這是一個時鐘訊號」。它的存在讓語言(與 lint)知道這根線的用途。reset:宣告「這是一個重置訊號」。Veryl 對重置有特殊語意:重置是同步的(第 4 章的主題),reset型別讓語言能推導重置行為。
為什麼要特別型別化?
在 SV 中,時鐘與重置只是普通的 logic:
input logic clk;
input logic rst_n;
工具無法從型別得知它們的用途,因此所有的時鐘域檢查、跨時鐘域檢查、重置一致性檢查,都要靠命名規範(clk 開頭)與外部工具(lint 規則)來做。
Veryl 把這個資訊放進型別裡,於是:
- 語言層級知道哪根線是時鐘;
- 可以檢查「時鐘是否被當成資料用」;
- 可以推導重置結構;
- 第 6 章的跨時鐘域檢查可以建立在型別資訊上。
這是「把隱含規範變成語言保證」的又一個例子,和 pub、_ 前綴、clock 型別是同一個設計哲學的不同應用。
時鐘域標註 'a#
module Top (
i_clk : clock,
i_rst_n : reset,
i_data : input logic<8> 'a,
o_data : output logic<8> 'a,
) {
}
'a 表示這個埠屬於時鐘域 a。這是第 6 章跨時鐘域檢查的基礎:語言可以自動偵測「一個 a 域的訊號被 b 域的邏輯直接使用」。
實體宣告#
基本形式#
inst u_name : ModuleType #(
.PARAM (VALUE),
) (
port_name : signal_name,
);
完整例子:
module Top (
i_clk : clock,
i_rst_n : reset,
i_d : input logic<8>,
o_q : output logic<8>,
) {
inst u_reg : Register #(
.WIDTH (8),
) (
i_clk : i_clk,
i_rst_n : i_rst_n,
i_d : i_d,
o_q : o_q,
);
}
注意埠連接的寫法:i_d : i_d——左邊是子模組的埠名,右邊是父模組的訊號名。
省略名稱的連接語法#
當父子兩邊的埠名完全相同時,可以省略:
inst u_reg : Register #(
.WIDTH (8),
) (
i_clk,
i_rst_n,
i_d,
o_q,
);
這是「同名省略語法」(name omission),與 SV 的 .port_name(port_name) 簡寫(SystemVerilog 中直接寫 port_name 會隱含 .port_name(port_name))對應。
這個語法的價值在於消除重複。 在一個有 20 個埠、埠名遵循嚴格命名規範的模組裡,省略寫法可以讓實例化從五行縮成兩行。
但要注意它的陷阱:一旦連接的不是同名訊號(例如需要接 i_addr 到子模組的 i_data),就必須用完整寫法,此時同一個實例裡混用兩種寫法會降低可讀性。實務上的建議是:一個實例內統一用一種寫法。
陣列實例#
inst u_bank : Memory #(
.WIDTH (32),
.DEPTH (16),
) [4] (
i_clk : i_clk,
i_addr: i_addr,
o_data: o_data,
);
[4] 放在型別與埠清單之間,表示實例化 4 份。
SV 對照:
Memory #(.WIDTH(32), .DEPTH(16)) u_bank [4] (
.clk (i_clk),
.addr (i_addr),
.data (o_data)
);
連接陣列實例的三種情形:
(一)所有實例接同一個訊號(例如所有 bank 共用時鐘):
inst u_bank : Memory [4] (
i_clk : i_clk,
i_addr: i_addr,
o_data: o_data,
);
(二)每個實例接不同的訊號(例如每個 bank 有自己的資料線):
inst u_bank : Memory [4] (
i_clk : i_clk,
i_addr: i_addr,
o_data: o_data,
);
在 Veryl 中,當連接的是 unpacked 陣列時,連接語法會自動對應到陣列實例的對應元素。這一點和 SV 的語意相同(SV 需要 .data(data_array) 配合 unpacked 埠)。
(三)明確的索引存取:在模組內部引用某個實例的元素:
var r_bank0_data : logic<32>
assign r_bank0_data = u_bank[0].o_data;
生成的 SystemVerilog 對照#
我們把上面每一段 Veryl 與它產生的 SV 並排,讓讀者建立「翻譯對應」的信心。
對照一:基本模組#
Veryl:
module Counter #(
parameter WIDTH : u32 = 32,
) (
i_clk : clock,
i_rst_n : reset,
i_en : input logic = 1,
o_count : output logic<WIDTH>,
) {
var r_count : logic<WIDTH>
always_ff {
if !i_rst_n {
r_count = 0;
} else if i_en {
r_count = r_count + 1;
}
}
assign o_count = r_count;
}
生成的 SystemVerilog:
module Counter #(
parameter WIDTH = 32
) (
input logic i_clk,
input logic i_rst_n,
input logic i_en = 1'b1,
output logic [WIDTH-1:0] o_count
);
logic [WIDTH-1:0] r_count;
always_ff @(posedge i_clk) begin
if (!i_rst_n) begin
r_count <= WIDTH'(0);
end
else if (i_en) begin
r_count <= r_count + WIDTH'(1);
end
end
assign o_count = r_count;
endmodule
值得注意的對應關係:
| Veryl | SystemVerilog | 觀察 |
|---|---|---|
i_clk : clock | input logic i_clk | 特殊型別還原成 logic |
logic<WIDTH> | logic [WIDTH-1:0] | <> 還原成 packed 範圍 |
= 1 | = 1'b1 | 常數被推導出明確型別 |
r_count = 0 | r_count <= WIDTH'(0) | 阻塞賦值還原成 <= 並帶寬度 |
always_ff { ... } | always_ff @(posedge i_clk) begin ... end | 還原出時脈敏感度 |
if !i_rst_n | if (!i_rst_n) | 還原出括號 |
WIDTH'(0) 這個形式值得特別說明:SystemVerilog 的常數賦值需要明確的寬度,否則 r_count = 0 中的 0 是 32-bit 的,會在 WIDTH < 32 時產生隱式擴充警告。Veryl 在產生時直接寫成 WIDTH'(0),把寬度明確化。這是「語義等價」的實踐:語法層面 Veryl 讓你不必手動寫型別轉換,產生碼裡卻是明確無歧義的。
對照二:實例化#
Veryl:
inst u_fifo : Fifo #(
.WIDTH (32),
.DEPTH (16),
) (
i_clk : i_clk,
i_rst_n: i_rst_n,
i_data: i_data,
o_full: o_full,
);
生成的 SystemVerilog:
Fifo #(
.WIDTH (32),
.DEPTH (16)
) u_fifo (
.clk (i_clk),
.rst_n (i_rst_n),
.data (i_data),
.full (o_full)
);
注意埠名的改寫:Veryl 的 i_clk 在 SV 中變成 clk(去掉 i_ 前綴),i_rst_n 變成 rst_n。這是 Veryl 的埠名正規化規則:方向前綴 i_ / o_ 是 Veryl 端的慣例,在 SV 端由方向關鍵字表達,因此前綴被移除。
這個改寫是可預測的規則,不是隨意的:去掉 i_ / o_ 前綴、保留其餘部分。這讓工程師可以預期「我寫 i_data,SV 裡會是 data」。
對照三:陣列實例#
Veryl:
inst u_bank : Memory [4] (
i_clk : i_clk,
o_data: o_data,
);
生成的 SystemVerilog:
Memory u_bank [4] (
.clk (i_clk),
.data (o_data)
);
實作練習#
練習一:最簡模組#
題目: 寫一個模組 And2,兩個 1-bit 輸入,輸出它們的 AND。
解答:
module And2 (
i_a : input logic,
i_b : input logic,
o_y : output logic,
) {
assign o_y = i_a & i_b;
}
生成的 SV:
module And2 (
input logic a,
input logic b,
output logic y
);
assign y = a & b;
endmodule
思考: 為什麼這個模組不需要參數?因為它沒有結構參數。如果要支援 N 輸入,就要加 parameter N: u32 = 2。
練習二:參數化計數器#
題目: 寫一個上數計數器 Counter,位寬由參數決定,具備同步高電位重置,有致能訊號,並在達到最大值時產生 o_wrap 訊號。
解答:
module Counter #(
parameter WIDTH : u32 = 8,
) (
i_clk : clock,
i_rst_n : reset,
i_en : input logic,
o_count : output logic<WIDTH>,
o_wrap : output logic,
) {
var r_count : logic<WIDTH>
always_ff {
if !i_rst_n {
r_count = 0;
} else if i_en {
r_count = r_count + 1;
}
}
assign o_count = r_count;
assign o_wrap = i_en && (r_count == {WIDTH{1'b1}});
}
注意兩個技巧:
{WIDTH{1'b1}}用來產生寬度等於WIDTH的全 1 值。這是 SV 的常見寫法,Veryl 也可以用repeat(見 2.2 節)。o_wrap用組合邏輯產生,而不是另一個暫存器。因為o_wrap表達的是「這一拍的輸入造成了溢位」,是當拍的資訊。若寫成暫存器,會延遲一拍,語意不同。
練習三:多路選擇器#
題目: 寫一個 Mux4,4 個 8-bit 輸入,2-bit 選擇訊號,1 個 8-bit 輸出。
解答:
module Mux4 #(
parameter WIDTH : u32 = 8,
) (
i_sel : input logic<2>,
i_d0 : input logic<WIDTH>,
i_d1 : input logic<WIDTH>,
i_d2 : input logic<WIDTH>,
i_d3 : input logic<WIDTH>,
o_d : output logic<WIDTH>,
) {
always_comb {
case i_sel {
2'd0: o_d = i_d0,
2'd1: o_d = i_d1,
2'd2: o_d = i_d2,
default: o_d = i_d3,
}
}
}
這裡用了 case 表達式(2.2 節的支柱一第 5 項)。用 always_comb + case 而不是 assign + 三運算子,是因為四選一的三運算子巢狀會嚴重降低可讀性。
注意: case 陳述賦值給 o_d(一個 output 埠)。在 SV 裡,這需要 always_comb 內對 output logic 賦值——合法。Veryl 的 always_comb 同樣允許。
練習四:FIFO 介面#
題目: 寫一個同步 FIFO 模組 SyncFifo,具備 push / pop 握手、滿溢旗標、空旗標,以及參數化的位寬與深度。
解答:
module SyncFifo #(
parameter WIDTH : u32 = 32,
parameter DEPTH : u32 = 16,
) (
i_clk : clock,
i_rst_n : reset,
i_push : input logic,
i_data : input logic<WIDTH>,
o_full : output logic,
i_pop : input logic,
o_data : output logic<WIDTH>,
o_empty : output logic,
) {
const PTR_WIDTH : u32 = $clog2(DEPTH);
var r_mem : logic<WIDTH> [DEPTH];
var r_wptr : logic<PTR_WIDTH>;
var r_rptr : logic<PTR_WIDTH>;
var r_count : logic<PTR_WIDTH + 1>;
always_ff {
if !i_rst_n {
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;
}
case {
i_push && !o_full && i_pop && !o_empty: r_count = r_count,
i_push && !o_full: r_count = r_count + 1,
i_pop && !o_empty: r_count = r_count - 1,
default: r_count = r_count,
}
}
}
assign o_full = r_count == DEPTH;
assign o_empty = r_count == 0;
assign o_data = r_mem[r_rptr];
}
這份解答展示了三個重要的觀念:
(一)記憶體用 unpacked 陣列宣告。 var r_mem: logic<WIDTH> [DEPTH]; 的 [] 表示這是記憶體(元素陣列),不是位元向量。這正是打包/非打包切分的實用意義。
(二)計數器需要比指標多一位。 PTR_WIDTH + 1 是因為要表示 DEPTH 這個值(例如深度 16 需要能表示 0 到 16,共 17 個狀態),這是 FIFO 的經典細節。在 SV 裡寫 logic [$clog2(DEPTH)-1:0] 會讓人算錯;Veryl 的 PTR_WIDTH + 1 寫法把這個 +1 變成顯式的。
(三)case 陳述裡的 r_count = r_count 這個「空動作」是刻意的。 在 SV 的 always_ff 中,只有被列出的分支才會被賦值;未列出的情況下 r_count 會保持(隱含 hold,這在 always_ff 中是允許的,因為它是硬體暫存器的自然行為)。但在 always_comb 中,未賦值是錯誤(會產生 latch)。Veryl 的 always_ff 語意與 SV 一致(未賦值 = 保持),所以 case 裡可以只寫變動的分支。
不過在 always_ff 中,把 hold 寫成顯式的 r_count = r_count 會讓意圖更清楚,也讓 lint 工具滿意(unassign_variable 檢查會看到所有路徑都有賦值)。這是風格選擇,不是語言要求。
注意:這個 FIFO 實作是同步的(單一時鐘)。跨時鐘域的 FIFO 需要更複雜的握手,本書第 6 章會討論。
lint 要點#
missing_port:未連接的埠#
inst u_fifo : Fifo (
i_clk : i_clk,
i_rst_n: i_rst_n,
i_data: i_data,
// 忘了連 o_full
);
missing_port 會報錯。解法有兩種:
o_full: o_full,
或明確標示不連接:
_ : _,
為什麼要區分這兩種? 因為「忘了連接」與「刻意不連接」在硬體上可能是完全不同的結果:
- 忘了連接:該埠的值是未定義的(在 SV 中是
z或工具給的預設值)。 - 刻意不連接:該埠被接到空,等於永久無效。
_ 讓「刻意」這件事變成程式碼裡可見的事實,而不是工程師的記憶。這是 Veryl 消歧義原則的又一個應用。
unused_variable:未使用的變數#
var r_unused : logic<8>
被宣告但從未使用。兩種處理:
var _r_unused : logic<8> // 底線前綴,抑制警告
#[allow(unused_variable)]
module Top {
var r_unused : logic<8>
}
底線前綴的語意是「這個名字不重要」,這是 Rust 社群約定的傳統,Veryl 採用了它。底線也可以用在多個位置:r_unused_、_unused。
為什麼要對「未使用」發警告? 因為在硬體設計中,未使用的宣告常常是設計意圖沒被實現的跡象:
- 宣告了一個輸出,但忘記驅動它;
- 宣告了一個暫存器,本來要做 pipeline,但忘了接出去;
- 從舊模組複製過來的殘留宣告。
每一種都是 bug 的候選。
其他值得注意的檢查#
| 檢查 | 觸發條件 | 硬體意義 |
|---|---|---|
unassign_variable | 變數從未賦值 | 暫存器永遠是 x/隨機值 |
multiple_assign | 同一變數被賦值多次 | 非預期的多驅動 |
missing_reset_statement | always_ff 沒有重置分支 | 重置行為不明確 |
initial_assign | 非預期的初始賦值 | ASIC 上不保證的行為 |
missing_reset_statement 值得特別說明。 它檢查的是「這段時序邏輯的 reset 行為是否明確」,而不是「有沒有寫 reset 分支」。某些設計確實不需要 reset(例如資料路徑上的暫存器),對這些可以用屬性抑制。
這個檢查的價值在於:重置不明確是硬體 bug 的高頻來源,而它在 SV 中只是 lint 規則,在 Veryl 中是語言層級的保證。
本章小結#
本章建立了 Veryl 描述硬體的基本骨架:
- 模組語法:
module Name #(...) (...) { },三段結構。 - 宣告順序:名稱在前、方向與型別在後——這是全語言統一的風格。
- 方向:
input/output/inout,加上介面專用的import。 - 預設值與
_:前者是宣告式的意圖,後者是宣告式的遺忘。 - 參數與常數:
parameter在模組標頭(可覆寫),const在模組內(不可覆寫)。這條規則消除了defparam式的語意不可預測。 - 型別:
logic<W>(打包)與logic [N](非打包)的切分、u32等整數型別、clock/reset特殊型別、時鐘域標註'a。 - 實例:
inst 名稱 : 型別 #(...) (...),支援具名與省略連接、陣列實例。 - lint:
missing_port與unused_variable讓「忘記」與「刻意」在程式碼裡可區分。
下一節(3.2)進入介面。這是 Veryl 中相對於 SystemVerilog 最有自己特色的部分:它不只是 SV interface 的翻譯,而是把驅動權威的概念直接編進語法裡,讓 connect 操作符能自動檢查方向衝突。
下一節:第 3.2 節「介面、modport 與 connect 操作符」