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 實體設計。