跳轉到

Module 宣告與 port:把 header 變短#

SV module header 的冗長問題#

一個典型的 SystemVerilog module header 長這樣:

module counter #(
    parameter int WIDTH = 8
) (
    input  logic             clk,
    input  logic             rst,
    input  logic             en,
    output logic [WIDTH-1:0] count
);

#( parameter ... ) 與 ( input/output ... ) 兩層括號、每行重複的 input logic、[WIDTH-1:0] 這種範圍表達式——這些是每個硬體工程師每天要打幾十遍的儀式。冗長不只是美觀問題:[WIDTH-1:0] 與 WIDTH 的關聯是靠人眼維護的,改一個參數名就要改所有 port 行;方向與型別的重複也讓 grep 與重構工具難以可靠處理。

Veryl 的語法:一切都在括號內#

Veryl 把 param 與 port 統一放在同一層括號內,用 param/port 關鍵字區分,並以尾隨逗號收尾,讓 diff 乾淨:

module counter (
    param WIDTH: u32 = 8,
    port clk:   input clock,
    port rst:   input reset,
    port en:    input logic,
    port count: output logic<WIDTH>,
) {
    var cnt: logic<WIDTH>;

    always_ff (clk, rst) {
        if_reset {
            cnt = 0;
        } else if en {
            cnt = cnt + 1;
        }
    }

    assign count = cnt;
}

注意幾個特點:

  • param WIDTH: u32 = 8:參數有明確型別,預設值寫在同一行。
  • port 型別用 logic<WIDTH> 而非 logic[WIDTH-1:0],位寬語意就是型別參數,不需要在每個 port 重寫範圍表達式。
  • clock 與 reset 是獨立型別,編譯器能辨識時脈與重置訊號,always_ff (clk, rst) 與 if_reset 都依賴這個型別資訊。
  • 函式體內 var(可變)、let(單一賦值)、const(常數)三種繫結各有語意,取代 SV 中 logic/wire 的模糊區分:var 對應可被程序賦值的變數,let 對應單一驅動的組合連線,const 是編譯期常數。

完整範例:FIFO 頂層#

module sync_fifo (
    param WIDTH: u32 = 32,
    param DEPTH: u32 = 64,
    port clk:     input  clock,
    port rst:     input  reset,
    port w_vld:   input  logic,
    port w_rdy:   output logic,
    port w_data:  input  logic<WIDTH>,
    port r_vld:   output logic,
    port r_rdy:   input  logic,
    port r_data:  output logic<WIDTH>,
) {
    const AW: u32 = $clog2(DEPTH);

    var w_ptr: logic<AW>;
    var r_ptr: logic<AW>;
    var mem:   logic<WIDTH>;

    always_ff (clk, rst) {
        if_reset {
            w_ptr = 0;
        } else if w_vld && w_rdy {
            w_ptr = w_ptr + 1;
        }
    }

    always_ff (clk, rst) {
        if_reset {
            r_ptr = 0;
        } else if r_vld && r_rdy {
            r_ptr = r_ptr + 1;
        }
    }

    assign w_rdy = w_ptr != r_ptr - 1;
    assign r_vld = w_ptr != r_ptr;
}

(此例省略記憶體讀寫細節,專注展示 header 與內部宣告的風格。)const AW: u32 = $clog2(DEPTH) 展示了模組內部常數可以由參數推導,位寬型別 logic<AW> 直接引用它,整條依賴鏈都在型別系統內。

方向推導(direction inference)#

Veryl 允許 port 省略方向:

module adder (
    port a: logic<8>,
    port b: logic<8>,
    port s: logic<8>,
) {
    assign s = a + b;
}

編譯器根據訊號在模組內的使用方式推導方向:被讀取的是 input,被驅動的是 output,既讀又驅動的情形則需要顯式標註。這聽起來只是省幾個字,實際上改變了工作流程——方向是可以從行為推導的冗餘資訊,讓機器算比讓人寫更不容易出錯。需要與外部工具對接、或方向有特殊語意(如 inout)時,仍然可以顯式書寫。

歷史脈絡#

module header 的冗長有歷史根源。Verilog(1984)誕生時是Gateway Design Automation 內部的模擬器腳本語言,設計目標是「快速描述小型測試電路」,而不是大型專案的軟體工程。port 方向(input/output/inout)是 1995 年 IEEE 1364 標準化時為了支援綜合與層級連線而強化的,parameter 區塊則來自 VHDL 的 generic 影響。兩層括號的結構在當年是合理的,但三十年間設計規模成長了四個數量級,header 的重複資訊成本被急劇放大——這是典型的「語言演進落後於使用規模」現象。

教育層面也值得注意:因為 header 冗長,大學的數位設計課程普遍讓學生用「複製上一個 module 再改」的方式寫程式,方向與型別的正確性從來不是教學重點。新一代 HDL 把這些冗餘交給編譯器(型別推導、方向推導),教學重點得以回到電路行為本身——這是工具進步對教育內容的反饋。

小結#

Veryl 的 module 宣告把 param 與 port 放進同一層括號,port 方向可省略由編譯器推導,var/let/const 明確區分三種繫結語意。相較 SV 的兩層括號與重複的方向型別宣告,header 更短、依賴更顯式、錯誤更早被攔截。這些改動的共同邏輯是:能推導的資訊就不該讓人手寫——把工程師的時間還給電路本身。