時脈從不睡覺——而這要你付出代價
想像一棟巨大的辦公大樓,依公司規定,每一盞燈、每一台螢幕、每一台冷氣,二十四小時全功率運轉——連空房間也開著,連假日凌晨三點也開著。電表瘋狂地轉,幾乎沒人受惠。那棟大樓就是你的晶片,而那個永不關閉的電源就是時脈。時脈是心跳,告訴每個正反器何時擷取新資料,而它必須整齊劃一地抵達所有正反器。為了做到這點,時脈樹合成建出一棵龐大的緩衝器與導線之樹,把時脈扇出到數十萬——有時數百萬——個端點。
痛點在這裡。依定義,那棵時脈樹每一個週期都翻轉。每個緩衝器每週期把負載電容充放電兩次,每個正反器的時脈輸入也是如此。這會燒掉動態功耗——*即使那些正反器裡儲存的資料從不改變*。一顆等記憶體而停頓的處理器、兩幀畫面之間的影像編解碼器、一個閒置的周邊——它們全都還在繳時脈樹的稅。在許多設計中,光是時脈網路就占了總動態功耗的 30–50%。它遠遠是最值得下手攻擊的單一目標。
天真的修法——以及它為何反咬一口
點子自己就寫好了:如果某個暫存器這個週期沒有新資料要載入,就別把時脈邊緣送給它。把沒人的房間的心跳停掉。看起來最便宜的做法,是把時脈與一個致能訊號做 AND:當 `EN` 為低時,閘控後的時脈保持平坦,下游的正反器就凍結。沒有時脈邊緣、沒有翻轉、沒有功耗。很簡單,對吧?
┌──────┐
EN ────┤ │
│ AND ├──── gated_clk (the tempting, BROKEN version)
CLK ────┤ │
└──────┘
CLK __|‾‾|__|‾‾|__|‾‾|__|‾‾|__
EN _____|‾‾‾‾‾‾‾‾‾‾|__________
^ EN rises mid-high
gated __|‾‾|__|‾‾|XX|‾‾|__|‾‾|__
^^ GLITCH: a runt clock pulse!麻煩出在時序。致能訊號 `EN` 從某些邏輯產生,它何時到就何時到。如果它剛好*在時脈為高時*改變,AND 閘的輸出就會出現一段短而醜的偏移——一個毛刺,一個遠比真實時脈週期窄的小脈衝。正反器無法分辨小脈衝與真實邊緣;它可能擷取到垃圾,或進入亞穩態。你把功耗問題換成了功能性錯誤,這是個糟糕透頂的交易。
整合式時脈閘控單元:用門栓守住開門時機
解法很優雅:別讓致能想過就過——把它擋在門口,直到安全的那一刻才放行。在 AND 閘前面放一個位準敏感閂鎖(latch),由時脈的*低*相位來控制。致能只允許在時脈為低時更新;一旦時脈轉高,閂鎖關閉,餵給 AND 閘的值就被凍結。如此一來,`EN` 上任何抖動都發生在低相位,而此時 AND 輸出本來就是低,於是它絕不可能從高脈衝中刻出一個毛刺。這個「閂鎖+AND」的封裝就是[[clock-gating|整合式時脈閘控]](ICG)單元——一個已特性化、無毛刺的單一標準單元,每個現代單元庫都會提供,每個工具也都知道怎麼用它。
┌──────────┐
EN ───►│ LATCH │ EN_latched ┌──────┐
│ (open on ├──────────────►│ │
┌───►│ CLK=0) │ │ AND ├──► gated_clk
│ └──────────┘ ┌─►│ │
│ │ └──────┘
CLK├───────────────────────────┘
CLK __|‾‾|__|‾‾|__|‾‾|__|‾‾|__
EN ____|‾‾‾‾‾‾‾‾‾‾|__________ (changes any time)
EN_latched _______|‾‾‾‾‾‾‾‾‾‾‾‾|______ (only updates when CLK=0)
gated_clk __|‾‾|__|‾‾|__|‾‾|__|‾‾|__ (clean — full pulses only)致能由誰寫?工具推斷 vs. 手寫
`EN` 訊號從哪來?有兩條路,而好的設計兩條都用。第一條是自動推斷:你寫平常的 RTL,帶一個條件式的暫存器更新,合成工具看出這個模式,就免費替你插入一個 ICG 單元。任何帶有回授 MUX 的正反器——「保留我的舊值,除非某條件成立」——都是候選。工具把那個循環 MUX 變成時脈閘上的致能。這是主力;在典型的區塊裡,工具會自動把絕大多數暫存器閘控起來。
// RTL the tool will AUTO-GATE (note the conditional update):
always @(posedge clk) begin
if (load_en) // <- becomes the clock-gate enable
data_q <= data_d; // register holds when load_en==0
end
// Synthesis infers an ICG: clk is gated by load_en. No clock
// edge reaches data_q's flops when load_en is low → no toggling.
// HAND-CODED coarse gate over a whole idle block:
assign blk_clk_en = ~unit_idle; // designer-supplied intent
icg u_icg (.CLK(clk), .EN(blk_clk_en), .GCLK(blk_gated_clk));
// blk_gated_clk now feeds an ENTIRE sub-block's clock tree.第二條路是手寫致能,這正是人類展現價值之處。工具只看到一個週期的邏輯;它無法知道某個算術單元因為沒人對它下指令,接下來一千個週期都是死的。但你知道。你可以把這份知識寫成明確的致能——`unit_idle`——並實例化一個 ICG 單元,關掉一大片時脈樹。工具自己永遠找不到那筆節省。教訓是:讓工具自動收割那些容易的、區域性的致能,把你的人力花在它推斷不出的、粗粒度的、架構層級的致能上。
粗 vs. 細:一次該閘控多少
時脈閘控有不同的粒度,而選擇粒度才是真正的工程。細粒度閘控在少數幾個正反器前——有時只有一個暫存器——放一個小 ICG 單元。它能在各處抓到節省,但每個小閘都有開銷:單元本身耗電、增加延遲,而插入成千上萬個會花掉面積與心力。如果某個暫存器本來就常翻轉,在它前面放個閘可能花掉的比省下的還多。
粗粒度閘控在整個子區塊的根部——一個核心、一個加速器、一個周邊——放一個 ICG 單元,一次關掉那個區塊的*整棵*時脈子樹。收益巨大,因為你連時脈樹的緩衝器都一起停掉了,不只是末端的正反器,而且一個閘就涵蓋數千個端點、每個正反器的攤分開銷微乎其微。代價是:它只在*整個*區塊真正閒置時才觸發,所以你需要一個乾淨可靠的閒置訊號,並且必須確定區塊內部沒有東西還需要跳動。實務上工具會建出多層閘控樹——靠根部是粗閘、深處是細閘——於是一個暫存器可以同時被「它所屬區塊閒置」*和*「它自己的區域致能」所閘控。
讓它上工——並看見回報
在真實的流程上,時脈閘控大多是自然發生的——但要發生得好,得有幾個刻意的動作。以下是從「功耗問題」到「省下功耗」的路徑。
- 寫出對閘控友善的 RTL:使用條件式暫存器更新(`if (en) q <= d;`),讓合成工具能推斷致能,而不是你自己手算的永遠寫入回授。
- 手動加入粗粒度、架構層級的閘:以工具自己發現不了的區塊層級閒置訊號,實例化 ICG 單元。
- 讓合成插入 ICG 單元:在工具中開啟時脈閘控、設定最小位元寬度門檻,讓它自動閘控大部分暫存器。
- 平衡閘控後的時脈:時脈樹合成把每個 ICG 視為時脈節點,平衡閘控與未閘控分支之間的時脈歪斜,使閘的延遲不致破壞時序。
- 用真實活動量測:在具代表性的工作負載上跑功耗分析,確認那些致能在實務中真的會閒置——一個從不關閉的閘什麼也省不了。
Worked saving — a 32-bit register block, gated coarsely: Without gating: α_clk = 1.0 (toggles every cycle) With gating: block is idle ~70% of cycles → effective α ≈ 0.30 P_dyn ∝ α · C · V² · f (from rung 2) Power ratio = 0.30 / 1.00 = 0.30 → ~70% less dynamic power in this block's clock + flops. ...and because a COARSE gate also stops the clock-tree buffers above the flops, the real-world saving is even larger than the leaf-flop number alone suggests.