5.7 靜態時序分析(ASIC)#
原始碼:
veryl-eda/src/asic/sta.rs(約 266 行)。本節假定你已讀過 5.1–5.6: 我們手上有一份映射後的標準單元網路(Netlist)、一份佈局(RowPlacement, 每 cell 的 (row, site))、一份繞線(Routing,每 net 的長度),以及(在 SKY130 上) 一份 Liberty 庫(NLDM 時序表,第 5.11 節)。操作面(怎麼跑、報表怎麼看)見
manual/09-asic.md;這裡講原理與原始碼。
STA(Static Timing Analysis)不需要任何輸入向量:它把電路當成有向圖, 節點是 cell 的輸出 net,邊是 cell 的延遲,然後對所有路徑做最長路徑分析。 這正是它能取代「跑十萬個 testbench 找最壞情況」的原因——最壞情況就是最長路徑。
5.7.1 延遲模型:兩套並存#
sta.rs 支援兩種延遲來源,由「有沒有 Liberty 庫」決定:
// veryl-eda/src/asic/sta.rs
let tech = crate::pdk::tech();
let nldm = tech.liberty.is_some();
let (rpu, cpu) = tech.wire_rc();
- vlib 教學庫(無 Liberty):延遲 = 本質延遲 + 負載係數 × 扇出:
// veryl-eda/src/asic/sta.rs
let out_delay = |i: usize| {
let c = &nl.cells[i];
if c.typ == "SRAM" { return super::lib::SRAM_CLK_Q; }
let l = cell(&c.typ);
l.delay + l.load * *fanout.get(&c.out()).unwrap_or(&0) as f64
};
這是一階線性模型: \(d = d_0 + k \cdot FO\) 。它足夠讓退火與 CTS 有方向感, 但不能宣稱是奈秒級的精確數字。
- Liberty / NLDM(SKY130):每個 cell 的每個 timing arc 是一張 二維表(輸入 slew × 輸出負載),查表用
Tech::arc(見 5.11 節):
負載 \(C_{\text{load}}\) 由三部分組成(sta.rs 中 load 的建構):
// veryl-eda/src/asic/sta.rs(節錄)
let mut load: HashMap<Bit, f64> = HashMap::new();
if nldm {
for c in &nl.cells { for (p, b) in &c.conns { if !is_out(p) { *load.entry(*b).or_default() += tech.pin_cap(&c.typ, p); } } }
for p in nl.ports.iter().filter(|p| p.dir == Dir::Out) { for b in &p.bits { *load.entry(*b).or_default() += 0.002; } }
for (b, l) in &len { *load.entry(*b).or_default() += cpu * l; }
}
其中 \(C_{\text{wire}} = c_{pu} \cdot \ell\) 來自 LEF 的線電容(pF/µm)× 繞線長度, $cpu * l 那一行。為什麼這樣設計:NLDM 表的橫軸就是「負載」, 把金屬電容併進去,等於假設分佈 RC 線可以用一個集總電容近似—— 這是 short-net 近似,在 130 nm 的晶片尺寸上是合理的;殘餘的分佈效應 另外用 Elmore 項補(下節)。
5.7.2 NLDM 查表與線性內插#
Tech::arc(pdk/mod.rs)對每個輸出腳的 arcs 找 related == from 的組合弧, 取 rise 與 fall 兩張表的較大值(worst case):
// veryl-eda/src/pdk/mod.rs(節錄)
pub fn arc(&self, logical: &str, pin: &str, slew: f64, load: f64) -> Option<(f64, f64)> {
...
for a in o.arcs.iter().filter(|a| a.related == from && !a.kind.starts_with("setup") ...) {
let d = [&a.cell_rise, &a.cell_fall].iter().filter_map(|t| t.as_ref().map(|t| t.at(slew, load))).fold(0.0, f64::max);
let s = [&a.rise_transition, &a.fall_transition]...;
if best.map(|x| d > x.0) != Some(false) { best = Some((d, s)); }
}
best
}
內插本身在 liberty::Table::at(第 5.11 節有完整程式碼),是雙線性:
graph LR
subgraph NLDM 表 cell_rise
A["(s1, c1)"] -- "fj 沿 y" --> B["(s1, c2)"]
C["(s2, c1)"] -- "fj" --> D["(s2, c2)"]
B -- "fi 沿 x" --> E["延遲 d"]
D -- "fi" --> E
end數學上,令查詢點落在格子 \((i, j)\) 內,正規化座標 \(0 \le f_i, f_j \le 1\) :
為什麼線性而不是樣條:查表發生在 STA 的內層迴圈(每 cell 每弧一次), 且 Liberty 表本身是由 HSPICE 掃出來的離散點;線性內插在相鄰點之間的誤差 小於製程漂移本身的誤差,樣條只會增加不確定的「平滑假象」。 表尾用線性外插(seg 函式的 clamp 行為),避免 slew 超出表範圍時直接報錯。
5.7.3 線延遲:Elmore RC#
sta.rs 的 wire 閉包計算「driver 到某 sink」的線延遲。NLDM 模式下:
// veryl-eda/src/asic/sta.rs
let wire = |b: Bit, to: (f64, f64)| {
let s = src(b);
let dist = (s.0 - to.0).abs() + (s.1 - to.1).abs();
// Elmore: the driver-to-sink resistance charging half its own wire and a typical pin (Ω·pF = ps)
if nldm { rpu * dist * (cpu * dist / 2.0 + 0.002) * 1e-3 } else { wire_delay(*len.get(&b).unwrap_or(&0.0), dist) }
};
對應的公式(Manhattan 距離 \(x\) ):
這正是單端 Elmore 延遲:一段長 \(x\) 的線,其自身電阻 \(r_{pu} x\) 對「半段線的電容 + 一顆典型 pin 電容」充電的時間常數。 距離越遠,二次項 \(\propto x^2\) 越主導——這解釋了為什麼 STA 的 critical path 報告裡,同一條長 net 到不同 sink 的 wire delay 不同(far sink 更差)。
vlib 模式用經驗式 wire_delay(0.00015*len + 0.0004*dist + 1e-6*dist²), 形式相同、係數手調,只是為了讓教學流程的 Fmax 有合理量級。
flowchart LR
D["driver cell<br/>output"] -->|"Elmore RC<br/>r·x·(c·x/2 + Cpin)"| S1["近 sink"]
D --> S2["遠 sink<br/>x² 項主導"]5.7.4 slew 的傳播#
NLDM 模式下 slew(輸出轉換時間)跟延遲一樣是「會傳染」的: 一級的輸出 slew 是下一級查表的橫軸。sta.rs 維護 slew: HashMap<Bit, f64>, 每級輸出取該 cell 所有輸入弧輸出 slew 的最大值:
// veryl-eda/src/asic/sta.rs(節錄,組合 cell 的推進迴圈)
let mut so_max = 0.0f64;
for (p, b) in &c.conns {
if is_out(p) || b.is_const() { continue; }
let (d, so) = gate(i, p, slew_of(b, &slew));
so_max = so_max.max(so);
...
}
net_at.insert(c.out(), arrival[i]);
slew.insert(c.out(), so_max);
輸入腳與常數 net 的 slew 定為 50 ps(slew_of 的 fallback)。 為什麼要帶 slew:NLDM 的遞迴性質——slew 劣化 → 下一級更慢 → slew 更劣化。 若固定 slew 只查一次表,長鏈會系統性樂觀。代價是 STA 必須照拓撲序推進 (netlist 的 cells 恰好已按拓撲排序、flip-flop 在最後,見 5.2 節的映射輸出), 不能反覆迭代到收斂——這裡接受單遍近似。
5.7.5 時鐘傳播:insertion delay 與 skew#
CTS(5.6 節)插入 CLKBUF 之後,時鐘不再是「瞬時到達每個 DFF」。 sta.rs 先把 CLKBUF 依樹序推進(它們在建入時保證父先於子):
// veryl-eda/src/asic/sta.rs(節錄)
let propagated = nl.cells.iter().any(|c| c.typ == "CLKBUF");
for (i, c) in nl.cells.iter().enumerate().filter(|(_, c)| c.typ == "CLKBUF") {
let a = c.port("A").unwrap();
let (d, so) = gate(i, "A", slew_of(&a, &slew));
let t = clk_at.get(&a).copied().unwrap_or(0.0) + wire(a, at(i)) + d;
clk_at.insert(c.out(), t);
}
let ck_arr = |i: usize| -> f64 {
if !propagated { return 0.0; }
let Some(ck) = nl.cells[i].port("CK") else { return 0.0 };
clk_at.get(&ck).copied().unwrap_or(0.0) + wire(ck, at(i))
};
每個 DFF 的時鐘到達時間 \(\text{clk}_i\) = buffer 鏈延遲 + 線延遲。由此定義:
虛擬時鐘處理 IO:外部世界不知道你晶片裡的時鐘樹。sta.rs 用 「樹的平均延遲」當 IO 的參考時鐘(io_clk):輸入埠假設在 io_clk 時刻 被外部時鐘發射、輸出埠假設在 io_clk 被捕獲。這樣 IO 路徑與內部路徑 用同一把尺量,不會因為樹的深度而系統性偏樂觀或偏悲觀。
5.7.6 setup 與 hold:constraint 表與 slack#
setup/hold 需求來自 Liberty 的 setup_rising / hold_rising constraint 表 (雙橫軸:clock slew × data slew),由 Tech::constraint 查出:
// veryl-eda/src/pdk/mod.rs(節錄)
pub fn constraint(&self, logical: &str, pin: &str, setup: bool, clk_slew: f64, d_slew: f64) -> Option<f64> {
...
p.arcs.iter().filter(|a| a.kind.starts_with(want))
.flat_map(|a| [&a.rise_constraint, &a.fall_constraint])
.filter_map(|t| t.as_ref().map(|t| t.at(clk_slew, d_slew))).reduce(f64::max)
}
sta.rs 在每個 endpoint(DFF 的資料腳、輸出埠)上檢查:
// veryl-eda/src/asic/sta.rs(節錄)
let w = wire(*d, at(i));
let t = net_at.get(d).unwrap_or(&0.0) + w + su - ck; // setup
if t > worst.0 { worst = (t, Some(*d), ...); }
let slack = min_at.get(d).copied().unwrap_or(0.0) + w - (ck + ho + extra_hold); // hold
hold_slack = hold_slack.min(slack);
兩條公式。令資料到達時間為 \(t_d\) (沿最長路徑)、最早資料到達為 \(t_d^{\min}\) (沿最短路徑)、捕獲時鐘到達為 \(t_{ck}\) 、週期 \(T\) :
sta.rs 的 t 實際上累積的是 T + slack(period = worst.0),同一件事的 另一種寫法。注意 setup 用 net_at(最長),hold 用 min_at(最短)—— min_at 在推進迴圈裡同步維護:常數(tie cell)永不切換,所以它的 最早到達是 \(+\infty\) (永不違反 hold):
// veryl-eda/src/asic/sta.rs
min_at.insert(c.out(), if early == f64::MAX { f64::INFINITY } else { early });
extra_hold 參數是測試鉤子:給每個 hold 需求加一個人工數值, 用來「製造」hold 違規,以測試 cts::fix_hold(5.6 節)的修復迴路。
timeline
title setup 與 hold 的時間關係(同一對 launch/capture DFF)
section launch 時鐘邊緣
t=0 : 資料從 launch Q 出發
section 週期內
t = t_d : 資料到達 capture D<br/>需要 t_d + t_setup ≤ T + t_ck (setup)
section capture 時鐘邊緣
t = T + t_ck : 捕獲
同一邊緣之前 : 舊資料不可太早改變<br/>t_d_min ≥ t_ck + t_hold (hold)5.7.7 late / early 雙軌分析#
推進迴圈同時維護兩個陣列,這是整個檔案的核心結構:
| 陣列 | 語意 | 用途 |
|---|---|---|
arrival / net_at | 最晚到達(late,max 遞迴) | setup、Fmax、critical path |
min_at | 最早到達(early,min 遞迴) | hold |
虛擬碼:
net_at[b] = 0 對所有輸入埠(以 io_clk 發射)
min_at[b] = io_clk
for cell in cells(拓撲序,flip-flop 在最後):
for (pin, b) in cell 輸入:
t_late = net_at[b] + wire(b, cell) + d(cell, pin, slew[b])
t_early = min_at[b] + wire(b, cell) + d(cell, pin, slew[b])
arrival[cell] = max(t_late)
min_at[cell.out] = min(t_early)
endpoints(DFF 資料腳、輸出埠):
setup: arrival + wire + setup_req − ck_arr
hold: min_at + wire − (ck_arr + hold_req + extra_hold)
5.7.8 關鍵路徑回溯與 criticality#
找出 worst endpoint 後,用 pred[](每 cell 記住「是哪個輸入腳最晚」) 往回走,把路徑逐段寫成 Step(wire 段、cell 段、launch/capture clock):
// veryl-eda/src/asic/sta.rs(節錄)
match driver.get(&b) {
Some(&i) => {
steps.push(Step { what: label, delay: w, at: arrival[i] + w });
let name = if nldm { tech.cell_name(&c.typ)... } else { c.typ.clone() };
steps.push(Step { what: format!("{}{}", name, ...), delay: cell_d[i], at: arrival[i] });
crit_cells.push(i);
...
}
None => { /* 走到輸入埠:IO 路徑起點 */ }
}
除了單一 critical path,最佳化迴路(5.3 節的 timing-driven placement) 需要的是每一條 net 的 criticality——不是「最糟的一條路」,而是 「每條 net 有多接近成為最糟」。sta.rs 用一次反向掃描算 tail[b] = 從 net b 的輸出端出發、到達任何 endpoint 的最長剩餘延遲:
// veryl-eda/src/asic/sta.rs(節錄)
let tail_of = |b: Bit, ...| -> f64 {
for &j in users.get(&b)... {
let after = if c.is_ff() { cell(&c.typ).setup } else if is_macro(j) { ... } else { cell_d[j] + tail[c.out()] };
t = t.max(wire(b, at(j)) + after);
}
if let Some(pa) = out_pins.get(&b) { t = t.max(wire(b, *pa)); }
t
};
然後
這個比值存在 Timing::net_crit,place::place 拿去當退火的移動權重 (把 critical net 的 cell 拉近,縮短它的 wire 延遲)。 為什麼不對每條路徑都回溯:回溯是 \(O(\text{path})\) ,全路徑回溯是指數的; tail 陣列一次反向 DP 就把所有 net 的「後果」算完, \(O(N)\) 換全域視野。
5.7.9 Timing 結構總覽#
classDiagram
class Timing {
+period: f64
+fmax: f64
+path: Vec~Step~ 關鍵路徑
+crit_cells / crit_nets
+arrival: Vec~f64~
+endpoints: usize
+net_crit: HashMap
+insertion: f64
+skew: f64
+hold_slack: f64
+hold_viol: Vec
}
class Step {
+what: String
+delay: f64 ns
+at: f64 到達時刻
}
Timing *-- Step5.7.10 本章重點#
- STA = 最長路徑分析,不需向量;
sta::analyze(nl, pl, rt, extra_hold)一次 算出 setup(late)與 hold(early)兩軌。 - 延遲模型雙軌:vlib 用 \(d_0 + k\cdot FO\) 線性式;Liberty 用 NLDM 查表 (
Tech::arc,雙線性內插,rise/fall 取 max),負載 = pin 電容 + 線電容。 - slew 隨路徑傳播(遞迴 NLDM),單遍拓撲推進,接受近似換速度。
- 線延遲用單端 Elmore: \(r x (cx/2 + C_{pin})\) ,遠 sink 的 \(x^2\) 項主導。
- 時鐘傳播給出 insertion delay 與 skew;IO 用樹平均延遲的虛擬時鐘。
- setup slack \(= T + t_{ck} - (t_d + t_{su})\) ,hold slack \(= t_d^{min} - (t_{ck} + t_{ho})\) ; hold 檢查的是最短路徑,所以兩軌必須並行維護。
- criticality 用一次反向 DP(
tail)覆蓋所有 net,餵給 timing-driven placement;單一 critical path 只留給報告。