跳轉到

06.4 函式與運算子#

function 的角色#

組合邏輯寫多了之後,一定會遇到重複:CRC 計算在同一個模組的兩個地方出現、位元反轉在 encode 與 decode 各寫一次。Verilog 的 function 與 SystemVerilog 的 function automatic 提供了解法,但帶著歷史包袱:Verilog-1995 的 function 是 static 的(所有呼叫共享同一份區域變數),若被遞迴呼叫或從多個 always 塊併發呼叫,模擬行為會錯。SystemVerilog 於是引入 function automatic,要求工程師記得加上 automatic 這個關鍵字——忘了加,除錯又是「單執行緒模擬正確、併發場景出錯」的經典場景。

Veryl 的 function 沒有這個包袱:語意上永遠是「純組合、可內聯」的,沒有 static/automatic 之分。

語法#

function F(a: u32) -> u32 {
    return a + 1;
}
  • 參數列表 (a: u32, b: logic<W>),每個參數標註型別。
  • 回傳型別寫在 -> 之後。
  • 以 return 回傳結果。
  • 定義在 module 或 package 內;放在 package 中即可跨模組共用(以 import P::*; 引入)。

在 always_comb 中呼叫:

module AddOne (
    x: input  u32,
    y: output u32,
) {
    function F(a: u32) -> u32 {
        return a + 1;
    }

    always_comb {
        y = F(x);
    }
}

也可以在 assign、generate 條件、參數推導中使用——凡是能在編譯期或組合語意下求值的地方都可以呼叫。

$clog2 與系統函式#

系統函式以 $ 開頭,在編譯期求值。最常用的是 $clog2:

param AW: u32 = $clog2(DEPTH);

\(clog2(N)\) 是 \(\lceil \log_2 N \rceil\) ,把「1024 深度的 FIFO 需要幾位元位址」這個問題交給編譯器。它對應 SystemVerilog 的 $clog2,但 Veryl 中它與 generics 協作得更自然:module Fifo::<DEPTH: u32> 內直接寫 logic<$clog2(DEPTH)>,任何深度的實例化都會得到寬度正確的指標。其他常見的還有 $bits(型別的位元數)等,用於寬度推導。

運算子與優先權#

Veryl 的運算子集與 Verilog 大致對應,幾個重點:

  • 算術:+ - * / %。
  • 位運算:& | ^(與既有的邏輯語意一致);取反用 !(邏輯)與對位元的 ~。
  • 縮減運算:對整個向量做 AND/OR/XOR,用於 parity 或旗標判斷。
  • 比較:== != < <= > >=。
  • 移位:<< >>。特別注意:Veryl 中 a << b 表示串接(concatenation),等價於 SystemVerilog 的 {a, b},而數值移位使用 shl / shr。這是與 Verilog 相當不同的一點,寫慣 Verilog 的人初期最容易混淆。
let cat: logic<8> = a << b;          // 串接:SV 的 {a, b}
let sh : logic<8> = a shl b;         // 數值左移:SV 的 a << b

優先權遵循一般 C-like 慣例(一元 > 乘除 > 加減 > 移位/串接 > 比較 > 位運算 > 邏輯),但本書的建議一如既往:優先權容易出錯的運算式一律加括號。硬體程式碼的可讀性優先於省鍵盤。

範例:parity 與 popcount#

module Codec::<W: u32> (
    din : input  logic<W>,
    dout: output logic<W>,
    par : output logic,
) {
    function parity(a: logic<W>) -> logic {
        var p: logic = 0;
        for i: u32 in 0..W {
            p = p ^ a[i];
        }
        return p;
    }

    always_comb {
        par = parity(din);
        dout = din;
    }
}

這個 function 內含迴圈與 var,語意上展開後是一串 XOR 樹。注意它只能被組合語意使用(在 always_comb / assign 中呼叫),不能在 always_ff 中跨時脈保持內部狀態——Veryl 的 function 沒有記憶,這正是它與「任務(task)」的分界。

函式內聯與合成的關係#

歷史上,「函式在合成中如何被處理」一直是 HDL 教材的必修段落。答案是:合成工具把 function 呼叫內聯(inline)展開——每個呼叫點複製一份函式體的邏輯。這帶來兩個推論:

第一,function 是零成本的抽象:呼叫十次就有十份電路,不呼叫就沒有電路,不會像軟體函式那樣產生呼叫開銷。這讓工程師可以放心用 function 提高可讀性,不必擔心面積。

第二,它也意味著 function 不是共用硬體的方式。若想讓多處共享同一份邏輯(例如一個共用的 multiplier),必須把結果宣告成訊號、算一次、多處讀取,而不是各處各自呼叫 function。把「程式碼重用」與「硬體重用」混為一談,是新手常見的面積膨脹來源。Verilog 時代這條知識靠口耳相傳;Veryl 的選擇是把 function 限制在純組合語意,至少讓「它一定會被內聯」成為不需要懷疑的保證。

小結#

function 完成了組合邏輯抽象化的最後一塊:表達式層級的重用。搭配 generics(06.1)、generate(06.2)、package(共用 function 與 type 的容器),Veryl 提供了一套完整的、型別安全的可重用設計工具箱。下一部分我們將離開語言本身,進入 veryl2chip 工具鏈:從模擬到合成到 FPGA/ASIC 實體設計。