4.8 FPGA 靜態時序分析:從 pip 延遲到 Fmax#
對照原始碼:
veryl-eda/src/pnr/sta.rs(375 行)。繞線收斂之後, 我們手上有一份「每網走哪些 wire、每個 hop 接哪種線」的清單—— 這正是算時序所需的一切。STA 的任務:不用任何模擬輸入, 純粹用圖上的延遲模型算出最壞路徑與 \(F_{max}\) , 並回頭告訴繞線器(4.7)與佈局器(4.6)誰該加權。
4.8.1 延遲從哪裡來#
sta.rs 的延遲常數全部來自 IceStorm 的 timings_up5k.txt (slow corner,1.2 V、最壞製程/溫度),分四類:
const CLK_Q: f64 = 1.391; // FF clk → Q
const LUT: [f64; 4] = [1.285, 1.232, 1.205, 0.874]; // in_k → lcout
const SETUP: [f64; 4] = [1.232, 1.060, 0.980, 0.728]; // in_k 的 setup
const CARRY_IN1: f64 = 0.675; const CARRY_IN2: f64 = 0.609; const CARRY_CIN: f64 = 0.278;
const RAM_CLK_Q: f64 = 1.179; // BRAM RCLK → RDATA
const IO_IN: f64 = 1.105; const IO_OUT: f64 = 0.187;
以及路由 hop 的延遲——這是最 FPGA 特有的一項(sta.rs:45-62):
pub fn hop_delay(src: Kind, dst: Kind) -> f64 {
let from_lc = src == Kind::LutOut;
match dst {
Kind::LutIn => 0.662,
Kind::Local => 1.099,
Kind::Ctrl => 0.636,
Kind::Glb => 0.278,
Kind::Sp4H => if from_lc { 0.649 } else { 0.543 },
Kind::Sp12H => if from_lc { 1.232 } else { 1.231 },
Kind::Carry => 0.556,
...
}
}
模型:每個 pip 一個延遲,由「被驅動的 wire 種類」決定 (dst 的 Kind;從 LC 輸出第一跳用輸出驅動器的值,所以 Sp4H 有 from_lc 的分支)。iCE40 的線不是等長的:span-4 短、span-12 長、 全域網是專用樹——繞線器在 4.7 混入的 hop_delay / 0.6 就是這張表。
flowchart LR
subgraph 一級邏輯的延遲預算
FFQ["FF clk→Q 1.391 ns"] --> H1["hop(0.3–1.2 ns × 段數)"]
H1 --> LUT["LUT in_k→out 0.87–1.29 ns"]
LUT --> H2["hop ×N"]
H2 --> SU["下一級 setup 0.73–1.23 ns"]
end4.8.2 圖模型:endpoint 而非 cell#
STA 的節點不是 cell,而是 wire × 電路角色的交點(Ep enum, sta.rs:34-43):
pub enum Ep {
LcOut(usize), // LC 的輸出 wire(起點)
LcIn(usize, u8), // LC 的 in_k wire(終點之一)
IoIn(usize), // 輸入腳
IoOut(usize), // 輸出腳
RamOut(usize), // BRAM 資料輸出
RamIn(usize, f64), // BRAM 輸入(帶各自的 setup)
}
mod.rs 的 build() 在組織繞線問題時順便填了 eps: HashMap<u32, Ep> (wire → endpoint)。STA 先把它反查成兩張表:
lc_in_wire[(l, k)]:LC \(l\) 的 in \(k\) 接到哪條 wire;sink_route[w] = (net, delay, pips):這條 wire 是網 \(n\) 的 sink, 從網源頭到這裡的 hop 總延遲 \(d\) 是多少(sta.rs:92-109沿 pip 的dst → src回溯累加)。
於是「一級組合」的到達時間遞迴非常乾淨:
其中 \(A_{\mathrm{src}}\) 依網源頭身分取值(sta.rs:137):
let s = match net_src[*ni] { Ep::LcOut(j) => out_arr[j], Ep::IoIn(_) => IO_IN,
Ep::RamOut(_) => RAM_CLK_Q, _ => 0.0 };
Some(s + d)
——LC 輸出取該 LC 的到達時間、輸入腳取 IO_IN、BRAM 輸出取 RAM_CLK_Q。FF 輸出的到達時間恆為 CLK_Q(sta.rs:182): 時脈是理想的(全域網,無 skew,見 4.8.6),所以所有 FF 同時翻轉, \(A_{\mathrm{out}}\) 重新歸零為 CLK_Q。
4.8.3 進位鏈:兩個並行的時鐘#
進位鏈讓圖變複雜:一個 carry LC 有兩個輸出要分別追蹤—— LUT 輸出(sum)與 carry-out。實作用兩組陣列 out_arr/cout_arr (sta.rs:128-132),LC 求值時同時算兩者(sta.rs:164-181):
let cin = match chain_prev[l] { Some(pv) => cout_arr[pv], None => 0.0 };
let mut best = (0.0f64, From::None);
for k in 0..4u8 {
if let Some(a) = input_arr(l, k, &out_arr) { ... v = a + LUT[k]; ... }
}
if lc.in3_chain && cin + LUT[3] > best.0 { best = (cin + LUT[3], From::Carry); }
if lc.carry {
let mut c = (cin + CARRY_CIN, From::Carry);
for (k, dl) in [(1u8, CARRY_IN1), (2u8, CARRY_IN2)] { ... }
cout_arr[l] = c.0; cout_from[l] = c.1;
}
carry-out 的到達時間:
一個 32-bit 加法器的關鍵路徑幾乎總是進位鏈本身:
32 級的鏈約 \(0.278 \times 31 + \dots \approx 10\) ns——這就是為什麼 pack(4.6.2)拚了命讓鏈不斷:鏈上一個中斷就得過繞線, 一個 hop 就是 0.5–1.2 ns,比 4 級 carry 還貴。
求值順序:LC 圖是 DAG(經 FF 切斷),但 Rust 不用拓撲排序庫, 而是帶 memo 的顯式堆疊(sta.rs:151-185):out_arr[l] = NaN 表示未算,(l, false) 先壓棧收集依賴、(l, true) 再回來求值—— iterative post-order,避免深設計爆 recursion stack。
4.8.4 終點與 Fmax#
到達時間算完後掃三種終點(sta.rs:187-216):
- FF 的 setup: \(A_{\mathrm{in}_k} + \mathrm{SETUP}[k]\) ;
- BRAM 輸入: \(A_{\mathrm{src}} + d_{\mathrm{route}} + \mathrm{ram\_setup}(\mathrm{pin})\) (每種 pin 的 setup 不同,
ram_setup查表:RADDR 0.384、WE 0.252…); - 輸出腳: \(A_{\mathrm{src}} + d_{\mathrm{route}} + \mathrm{IO\_OUT}\) 。
注意這個不等式不減去 clock skew(見 4.8.6),也不含 clock-to-clock jitter——它是理想時脈下的 FF-to-FF 週期下界。
4.8.5 關鍵路徑回溯與 net criticality#
STA 的輸出不只是數字。求值過程記錄每個 LC 的 out_from[l]: From (最佳輸入是哪一腳 / 來自 carry),終點確定後就能沿記錄走回源頭 (sta.rs:219-288),產生人類可讀的路徑報告:
match out_from[l] {
From::Input(k) => {
rev.push(Step { what: format!("{} in{}→out", loc(l), k), delay: LUT[k as usize], at: 0.0 });
cur = lc_in_wire.get(&(l, k)).and_then(|w| route_step(*w, ...));
}
From::Carry => {
rev.push(Step { what: format!("{} carry-in→out", loc(l)), delay: LUT[3], at: 0.0 });
via_carry = chain_prev[l]; // 沿鏈往下追
}
...
}
route_step 把每段繞線也寫進路徑(繞線 n123(4 段)), 最後 at 欄位累加出每步的絕對時間——IDE 的 ⓘ 彈窗就是吃這份 Vec<Step>。
net criticality 則需要一次反向傳遞:對每個網問「從源頭出發、 經過我、再加上我之後最壞還有多長」。實作先算每個 LC 的 tail (tail_out/tail_cout:從此 LC 的輸出/carry-out 到任一終點的最長路, sta.rs:295-352,同樣是 iterative post-order),然後:
let net_crit: Vec<f32> = nets.iter().enumerate().map(|(ni, net)| {
...
worst = worst.max(src + d + after);
(worst / period).min(1.0) as f32
}).collect();
crit 回到兩處:繞線的 RNet.crit(4.7.6 的混合成本)與佈局的 \(w_n = 1 + 6 c_n^8/\sqrt{\mathrm{fanout}}\) (4.6.3)。這就是「兩輪 place & route」的資料閉環:
flowchart TD
P1["第 1 輪:線長退火 + 擁擠繞線"] --> STA1["sta::analyze → net_crit"]
STA1 --> W["w_n = 1 + 6·c⁸/√fanout(佈局權重)<br/>RNet.crit = c(繞線成本混合)"]
W --> P2["第 2 輪:時序驅動退火 + 時序繞線"]
P2 --> STA2["再 analyze"]
STA2 --> CMP{"Fmax₂ > Fmax₁ × 1.03?"}
CMP -- 是 --> K2["採用第 2 輪"]
CMP -- 否 --> K1["保留第 1 輪(估計誤差內不值得冒險)"]4.8.6 模型假設與誤差來源#
檔頭註明瞭簡化(sta.rs:5-7):時脈理想(無 skew)、 set/reset 路徑不計時。這在 iCE40 上是合理的:全域緩衝樹是專用的、 平衡的,skew 遠小於 hop 延遲;set/reset 不是每個週期都要及時的路徑。 但要記得:
- 無 skew 意味著 \(F_{max}\) 略偏樂觀。hold-time 檢查完全缺席—— iCE40 同一 tile 內 FF 間的最短延遲由硬體保證,跨 tile 的 hold 靠 路由延遲下界,工具不管它。
- hop 延遲是常數表:真實延遲與扇出、相鄰訊號切換(串擾)有關, NLDM 查表(ASIC 後端那套)在 FPGA 繞線上不存在。
- icetime 用同一份晶片資料但獨立實作,所以
pnr_test.sh把它當 第二意見(pnr_test.sh:41-42):
fmax=$(icetime -C "$CHIPDB" -d up5k -P sg48 -p $B/$m.pcf -t $B/$m.asc 2>/dev/null \
| sed -n 's/^Total path delay: .*(\(.*\) MHz)/\1 MHz/p')
-C "$CHIPDB" 指定同一個 chipdb-5k.txt——icetime 的模型也是 「wire 種類 → 延遲」的表,但它的表在 chipdb 的 .timing 區段 (本工具則寫死在 sta.rs 常數)。兩者通常相差幾個百分點; 差距大表示哪邊的 hop 分類出了錯。
4.8.7 虛擬碼#
analyze(chip, packed, placement, nets, routes, eps):
# 1. 展開路由 → 每個 sink wire 的 (net, route_delay, pips)
for net, route in zip(nets, routes): 對每個 sink 沿 pip 回溯累加 hop_delay
# 2. 前向:LC 到達時間(DAG post-order,FF 輸出重置為 CLK_Q)
A_out[l] = max_k ( A_src(net of in_k) + d_route + LUT[k] )
A_cout[l] = max( A_cin + CARRY_CIN, A_in1 + CARRY_IN1, A_in2 + CARRY_IN2 )
# 3. 終點掃描
worst = max( FF setup, RAM input setup, IO output )
# 4. 回溯關鍵路徑(out_from / cout_from 記錄的決策)
# 5. 反向:tail_out / tail_cout → net_crit = (src + d + tail) / period
return Timing { period, fmax = 1000/period, path, net_crit, ... }
4.8.8 為什麼這樣設計?#
- STA 內建而不是外掛 icetime:繞線器需要
net_crit(4.7.6), 佈局器需要w_n(4.6.3)——一個「在流程中間」的 STA 必須與 pack/place/route 共享資料結構(Packed、Placement、RNet)。 icetime 吃的是 .asc,餵不進這個迴圈。 - 常數寫死而非解析 timings 檔:UP5K 一種裝置、一份 15 個數字的表, 解析器與型別系統的成本不值;
hop_delay的Kind分類反而需要 智慧(chipdb::Kind::of用 wire 名字判斷),值得做成 enum。 From記決策而非整條路:回溯時只需要「這個輸出是從哪一腳來的」 一個選擇,記整條路徑是 \(O(V^2)\) 記憶體。
本章重點#
- 延遲四來源:cell arc(LUT/FF/carry/BRAM/IO,常數表)、routing hop (由被驅動 wire 的
Kind查hop_delay)、clock network(理想 = 0)、 setup。 - 圖模型以 wire 為節點、以
Ep標記電路角色;FF 輸出重置CLK_Q實現「理想時脈」假設。 - 進位鏈需要雙時鐘(LUT 與 carry-out 分別追蹤);鏈每級 0.278 ns, 斷一鍊過一 hop 更貴。
- \(T_{clk} \ge T_{cq}+T_{logic}+T_{route}+T_{su}\) ; \(F_{max}=1000/T\) (ns)。
net_crit由後向 tail 傳遞算出,是時序驅動佈局/繞線第二輪的輸入; 兩輪結果要快過 3% 才換。- 模型不含 skew/hold/jitter;icetime(
-C chipdb)作為獨立第二意見。