06.3 通用元件實作:FIFO、Mux、Arbiter#
Cummings 論文文化#
在進入程式碼之前,先講一段真實的歷史。2000 年代的 SNUG(Synopsys Users Group)年會上,Clifford Cummings(Sunburst Design 創辦人)發表的一系列論文成為業界事實標準:Simulation and Synthesis Techniques for Asynchronous FIFO Design(2002 年前後,含 independent-clock FIFO 的格雷碼指標設計)、Asynchronous & Synchronous Reset Design Techniques(重置技術,見 05.3 章)等。這些論文之所以重要,是因為它們指出:FIFO 這種「人人都寫過」的元件,寫對的人非常少——跨時脈 FIFO 的滿/空旗標若用二進位指標跨域比較,會因多位元同時變化的 CDC 問題產生錯誤。Cummings 的解法(格雷碼指標、兩級同步器、方向判斷位元)此後寫進了幾乎所有公司的 design guideline。
「通用元件的正確寫法值得反覆論證與論文發表」這個文化,是本章的背景:通用元件是工程知識密度最高的地方,也因此最值得用參數化語言寫一次、驗證一次、處處使用。
二元 Mux#
最簡單的通用元件:N 選一 mux,選擇訊號為二進位編碼。
module Mux::<W: u32, SEL: u32> (
sel : input logic<$clog2(SEL)>,
din : input logic<SEL, W>,
dout: output logic<W>,
) {
always_comb {
dout = din[sel];
}
}
SEL 是輸入數目,W 是資料寬度,din 是 logic<SEL, W> 的二維陣列。用 generics 讓「輸入數目」與「資料寬度」獨立參數化,$clog2(SEL) 自動算出選擇訊號寬度。如果需要 one-hot 選擇的版本,可以再寫一個 MuxOneHot,介面型別不同(sel: logic<SEL>),錯誤的混用會在編譯期被攔截——這就是泛型版的好處。
同步 FIFO(含幾乎滿/空)#
module SyncFifo::<W: u32, DEPTH: u32> (
clk : input clock,
rst : input reset,
wdata : input logic<W>,
wvalid : input logic,
wready : output logic,
rdata : output logic<W>,
rvalid : output logic,
rready : input logic,
almost_full : output logic,
almost_empty: output logic,
) {
param AW: u32 = $clog2(DEPTH);
param TH : u32 = DEPTH * 3 / 4;
var mem : logic<DEPTH, W>;
var wp : logic<AW>;
var rp : logic<AW>;
var cnt : logic<AW + 1>;
let full : logic = cnt == logic<AW + 1>'(DEPTH);
let empty : logic = cnt == 0;
always_ff (clk, rst) {
if_reset {
wp = 0;
rp = 0;
cnt = 0;
} else {
if wvalid && wready {
mem[wp] = wdata;
wp = wp + 1;
cnt = cnt + 1;
}
if rvalid && rready {
rp = rp + 1;
cnt = cnt - 1;
}
if wvalid && wready && rvalid && rready {
cnt = cnt; // 同時讀寫時數量不變
}
}
}
assign wready = !full;
assign rvalid = !empty;
assign rdata = mem[rp];
assign almost_full = cnt >= logic<AW + 1>'(TH);
assign almost_empty = cnt <= logic<AW + 1>'(1);
always_comb {
}
}
設計要點:
- 深度
DEPTH為 2 的冪次時,指標自然回繞;cnt多一位元以區分滿與空(或用滿/空判斷式直接比較計數)。 - 同時讀寫的那一拍,寫與讀各自的指標推進與計數變化要一致——上述寫法以「先各加減、再修正同拍」呈現,實務上更常將寫讀條件組合成一個 case;重點是這個 corner case 必須被明確處理與驗證(Cummings 論文的核心主張之一:FIFO 的 bug 都藏在同時讀寫與邊界條件)。
almost_full/almost_empty以可程式閾值TH(此處為 3/4 深度)輸出,這是流量控制中比滿旗標更早介入的軟體水位線。- 本元件為單時脈同步 FIFO;跨時脈(asynchronous FIFO)需要格雷碼指標與兩級同步器,屬進階主題,此處不展開。
注意 var 與 let 的分工:會被多個條件更新的 wp/rp/cnt/mem 用 var;單一表達式定義的 full/empty 用 let,永遠不可能被 latch 推論污染。
Round-robin Arbiter#
輪詢(round-robin)仲裁器保證公平:每個 requester 依序獲得授權,上次獲授權者排到隊尾。N 個 request 的版本:
module RRArbiter::<N: u32> (
clk : input clock,
rst : input reset,
req : input logic<N>,
gnt : output logic<N>,
) {
var r_ptr: logic<$clog2(N)>;
always_ff (clk, rst) {
if_reset {
r_ptr = 0;
} else {
if req[r_ptr] {
r_ptr = (r_ptr + 1) % logic<$clog2(N)>'(N);
} else {
// 指標停在原地,等待下一個 request
}
}
}
always_comb {
gnt = 0;
for i: u32 in 0..N {
if i == r_ptr && req[r_ptr] {
gnt[i] = 1;
}
}
}
}
這是一個指標式的簡化輪詢:指標 r_ptr 指向「下一個輪到的 agent」,若該 agent 有 request 則授權並推進指標,否則保持。缺點是若指標位置沒有 request,會跳過後面的 agent(不是嚴格 RR)。嚴格的 round-robin 用「double mask」技巧:把 request 對指標旋轉後做 priority encode,再旋轉回來。概念上:
// 嚴格 RR 的核心(概念示意):
// masked_req = req << (ptr + 1) 依指標旋轉
// winner = priority_encode(masked_req 或 req)
// next_ptr = winner
完整的 double-mask 實作留給讀者作為練習;此處想強調的是,仲裁器的公平性證明(無飢餓、服務順序)是靠指標狀態的 invariant 支撐,這也是為什麼它值得做成一個被充分驗證的通用元件,而不是在每個模組裡重寫一次。
泛型的價值#
三個元件共用同一個模式:以 ::<...> 接收規格、以 param/let 推導內部常數($clog2)、以 var 存狀態、以 always_comb 組合輸出。寫一次、驗證一次,任何規格的實例化都經過相同的型別檢查。這正是 Cummings 論文文化在語言層面的延伸:元件知識被固化為參數化的、可重複使用的程式碼,而不是散落在各專案的隱性經驗。下一章補上最後一塊拼圖:function。