跳轉到

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>,
) {
    // 主體
}

三個部分:

  1. 名稱:遵循 Rust 命名慣例,用大寫開頭的駝峰(ModuleName)。Veryl 會在產生時加上專案前綴。
  2. 參數列表:#(...),可省略。
  3. 埠列表:(...),可省略(無埠模組)。
  4. 主體:{ ... }。

完整的語法元素#

元素 語法 說明
模組名稱 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 要求參數有明確型別。這帶來兩個好處:

  1. 參數的位寬明確。 parameter WIDTH: u32 的 WIDTH 就是 32-bit,不會出現「參數的位寬隨上下文改變」。
  2. 可以宣告非整數參數。 例如 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}});
}

注意兩個技巧:

  1. {WIDTH{1'b1}} 用來產生寬度等於 WIDTH 的全 1 值。這是 SV 的常見寫法,Veryl 也可以用 repeat(見 2.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 描述硬體的基本骨架:

  1. 模組語法:module Name #(...) (...) { },三段結構。
  2. 宣告順序:名稱在前、方向與型別在後——這是全語言統一的風格。
  3. 方向:input / output / inout,加上介面專用的 import。
  4. 預設值與 _:前者是宣告式的意圖,後者是宣告式的遺忘。
  5. 參數與常數:parameter 在模組標頭(可覆寫),const 在模組內(不可覆寫)。這條規則消除了 defparam 式的語意不可預測。
  6. 型別:logic<W>(打包)與 logic [N](非打包)的切分、u32 等整數型別、clock / reset 特殊型別、時鐘域標註 'a。
  7. 實例:inst 名稱 : 型別 #(...) (...),支援具名與省略連接、陣列實例。
  8. lint:missing_port 與 unused_variable 讓「忘記」與「刻意」在程式碼裡可區分。

下一節(3.2)進入介面。這是 Veryl 中相對於 SystemVerilog 最有自己特色的部分:它不只是 SV interface 的翻譯,而是把驅動權威的概念直接編進語法裡,讓 connect 操作符能自動檢查方向衝突。


下一節:第 3.2 節「介面、modport 與 connect 操作符」