5.1 標準單元流程總覽#
原始碼:
veryl-eda/src/asic/(本書第 5 章的主題);本章對應mod.rs與lib.rs。
第 2 章的合成(synthesis)把 Veryl 行為描述變成由抽象閘($_AND_、$_DFF_…)組成的 閘級網路(gate netlist)。這樣的網路「能算」,但不能「做」:世界上沒有賣一顆叫 $_AND_ 的晶片。ASIC 實體設計(physical design)的任務,是把這份邏輯網路變成 一疊真正的幾何圖形(GDS),交給晶圓廠製造。
本章先給整條流程的全貌,之後各節深入每一步的原始碼。
5.1.1 流程總覽#
flowchart TD
A[邏輯閘級網路<br/>netlist::optimize 後] --> B["技術映射 map::map<br/>(5.2)"]
B --> C["佈局 place::place<br/>(5.3)"]
C --> D["全域繞線 route::route<br/>(5.4)"]
D --> E{overflow = 0?}
E -- 否 --> F[降低 utilization<br/>換 seed 重來]
F --> C
E -- 是 --> G["STA sta::analyze<br/>(理想時鐘)"]
G --> H["時序驅動重佈局<br/>timing_weights + 再退火"]
H --> I["CTS cts::build<br/>(5.6)"]
I --> J[再繞線 + STA<br/>含傳播時鐘]
J --> K{hold 違規?}
K -- 是 --> L["cts::fix_hold<br/>插入延遲 BUF"]
L --> J
K -- 否 --> M["PDN pdn::insert_taps/build"]
M --> N["詳細繞線 detail::route<br/>(5.5)"]
N --> O{conflicts = 0?}
O -- 否且為 PDK --> P[utilization × 0.85<br/>重新 physical()]
P --> C
O -- 是 --> Q["DRC drc::check"]
Q --> R["GDS gds::write"]
R --> S[DEF / Verilog / bitstream 報告]每一步的輸入輸出:
| 步驟 | 函式(mod.rs 中被 flow_with 呼叫) | 輸入 | 輸出 |
|---|---|---|---|
| 技術映射 | map::map | 抽象閘網路 Netlist | 標準單元網路(NAND2、AOI21、DFFR…) |
| 佈局 | place::place | 映射後網路 + utilization | RowPlacement(每 cell 的 (row, site)、IO pin 位置、退火 history) |
| 全域繞線 | route::route | 網路 + 佈局 | Routing(每 net 的 gcell 段、線長、overflow) |
| STA | sta::analyze | 網路 + 佈局 + 繞線 | Timing(Fmax、setup/hold 違規清單、net criticality) |
| CTS | cts::build | 網路 + 佈局(原地修改) | CLKBUF 樹 + Cts 統計 |
| hold 修復 | cts::fix_hold | STA 的 hold_viol 清單 | 插入延遲 BUF |
| PDN | pdn::insert_taps / pdn::build | 網路 + 佈局 | 電源軌、strap、via、filler、taps |
| 詳細繞線 | detail::route | 以上全部 | Detail(軌道級線段、via、conflicts/opens) |
| DRC | drc::check | 版圖形狀 | 違規清單(第 5.9 節) |
| GDS | gds::write | 版圖 | GDSII 檔(第 5.10 節) |
mod.rs 的 physical() 是這條鏈的實體:
// veryl-eda/src/asic/mod.rs(節錄)
fn physical(mapped: &Netlist, seed: u64, util: f64, extra_hold: f64) -> Result<...> {
loop {
placement = place::place(&mapped, seed, util, &Default::default(), None);
// a short probe decides: a routable die needs few rounds
let probe = route::route_with(&mapped, &placement, 8);
if (probe.overflow > probe.iters[0] / 50 || probe.demand > 0.5) && util > 0.35 {
util *= 0.8; // 晶片面積太小,繞不動:換更大的 die
continue;
}
routing = if probe.overflow == 0 { probe } else { route::route(&mapped, &placement) };
break;
}
let mut timing = sta::analyze(&mapped, &placement, &routing, extra_hold);
// timing-driven pass:以 criticality 加權線長再退火一次,取較快且可繞的結果
let weights = timing_weights(&mapped, &timing.net_crit);
...
let cts = cts::build(&mut mapped, &mut placement); // 時鐘樹
let mut routing = route::route(&mapped, &placement); // 含時鐘樹再繞
// hold:在違規輸入前插延遲 BUF,最多三輪
// PDN:taps、strap、filler,然後軌道級詳細繞線繞過它們
let taps = pdn::insert_taps(&mapped, &mut placement);
let pdn = pdn::build(&mapped, &placement, &taps);
let detail = detail::route(&mapped, &placement, &routing, &pdn);
...
}
注意順序背後的因果:CTS 必須在佈局定案之後(緩衝器要放在實際空位上)、 在繞線之前(時鐘網路要一起繞);詳細繞線必須在 PDN 之後(電源 strap 佔掉部分軌道,訊號線要繞開)。這些相依關係就是流程圖中不能任意調換次序的原因。
5.1.2 為什麼要「利用率」與重試#
物理設計本質上是資源分配:die 面積固定,cell 佔面積、繞線佔軌道。利用率 (utilization)= cell 總面積 / die 面積。太高 → 繞線擠不進去(overflow); 太低 → 線長暴增、Fmax 下降。flow_with 的策略:
// veryl-eda/src/asic/mod.rs(節錄)
let lib_tech = !crate::pdk::tech().is_vlib();
let mut util = if lib_tech { 0.55 } else { 0.65 };
let (mapped, placement, ...) = loop {
let r = physical(&mapped0, seed + attempt as u64, util, extra_hold)?;
attempt += 1;
if !lib_tech || (r.8.conflicts == 0 && r.8.opens == 0) || attempt >= 4 || r.9 < 0.2 { break ... }
util = r.9 * 0.85; // 詳細繞線失敗 → die 放大 15%
};
vlib(內建教學庫)的網格規則簡單,65% 幾乎總能繞通;SKY130 的軌道更密、 keep-out 更多,起點降到 55%,且詳細繞線若有 conflict/open 就自動把 die 放大重跑(最多四次)。這是「繞不通就換更大的晶片」這句工程常識在程式裡的樣子。
5.1.3 vlib:內建標準單元庫的雙層結構#
實體設計必須「知道」每顆單元的幾何與時序。真實工具鏈用 Liberty(.lib)與 LEF 檔描述;本專案的做法是兩層:
- 邏輯層(vlib,
lib.rs):一份固定的、製程無關的單元表——INV BUF CLKBUF NAND2 NOR2 AND2 OR2 XOR2 XNOR2 AOI21 OAI21 MUX2 TIEL TIEH DFF*, 每顆單元有width(佔幾個 site)、delay(本徵延遲)、load(每 fanout 延遲)、setup。延遲模型是教育版線性式:
- PDK 層(
src/pdk/,第 5.11 節):Tech提供 row/site 幾何、LEF 引腳、 Liberty NLDM 時序、以及綁定表——把邏輯名(NAND2)映射到 PDK 單元名 (SKY130 的sky130_fd_sc_hd__nand2_1)與其存取點。
所有後端程式碼都只對邏輯名工作;要到寫 DEF / GDS 的最後一刻才翻譯成 PDK 名。 lib.rs 中的取用函式展示這層抽象:
// veryl-eda/src/asic/lib.rs(節錄)
/// width in sites of a logical cell in the current technology
pub fn width(name: &str) -> u32 {
crate::pdk::tech().binding(name).map(|b| b.width).unwrap_or_else(|| cell(name).width)
}
/// row height and site width (µm) of the current technology
pub fn row_h() -> f64 { crate::pdk::tech().row_h }
pub fn site_w() -> f64 { crate::pdk::tech().site_w }
以及「這是不是教學庫」的判斷,用來包住只有真實 PDK 才有的行為:
let lib_rules = !tech.is_vlib(); // detail.rs:SKY130 專屬軌道規則
let vlib = tech.is_vlib(); // mod.rs def():DEF 名稱與 pin 形狀
為什麼這樣設計? 若把 SKY130 名稱硬寫進 map/place/route,換一個 PDK 就要改遍 所有模組;反過來,若只有邏輯名而沒有 PDK 層,就寫不出合法的 DEF/GDS。雙層結構 讓演算法與製程正交:第 5.2–5.6 節的演算法讀 width()/row_h()/site_w() 時根本 不在乎底下是 vlib 還是 SKY130;is_vlib() 只在「該製程特有的物理規則」出現處 (pin 存取、軌道 keep-out、via 檢查)當 guard。此外 vlib 的 EXTRA 表 (NAND3 NOR3 AOI22 OAI22…)說明綁定表也可反向運作:映射器先用 has() 問「這個技術有沒有」,有才敢產生該單元(見 5.2)。
5.1.4 Output:一次流程的所有中間結果#
// veryl-eda/src/asic/mod.rs
pub struct Output {
pub mapped: Netlist, // 映射後網路
pub placement: place::RowPlacement,
pub routing: route::Routing,
pub timing: sta::Timing,
pub area: f64, // 總 cell 面積 (µm²)
pub counts: Vec<(String, usize)>, // (單元型別, 數量),多者在前
pub fmax_wl: f64, // 只看線長的佈局下的 Fmax(理想時鐘)
pub fmax_ideal: f64, // CTS 前的 Fmax
pub cts: cts::Cts,
pub hold_bufs: usize, // 修 hold 插的 BUF 數
pub detail: detail::Detail,
pub pdn: pdn::Pdn,
pub power: power::Power,
}
保留 fmax_wl 與 fmax_ideal 不是為了除錯,而是為了量化每個最佳化步驟的貢獻: fmax_wl → fmax_ideal 的差距是時序驅動佈局賺到的,fmax_ideal → timing.fmax 的差距(通常是負的)是時鐘樹偏移付出的代價——這正是 5.6 節要討論的 skew 成本。
最後,flow_with 還用詳細繞線的實際線長覆寫全域繞線的估計值,再做最終 STA:
let dlen: HashMap<Bit, f64> = detail.nets.iter().map(|n| (n.bit, n.length)).collect();
for n in routing.nets.iter_mut() { if let Some(l) = dlen.get(&n.bit) { n.length = *l; } }
let timing = sta::analyze(&mapped, &placement, &routing, extra_hold);
並做一次隨機輸入模擬估功耗(power::estimate)。物理設計的尾聲永遠是: 用最精確的幾何,回頭校正所有估計。
5.1.5 複雜度概觀#
| 步驟 | 時間複雜度 | 瓶頸 |
|---|---|---|
| 技術映射 | \(O(n)\) (樹上 DP,每節點候選數為常數) | — |
| 佈局退火 | \(O(\text{rounds} \times n \times \text{增量成本})\) | 增量 HPWL(只重算被動到的 net) |
| 全域繞線 | \(O(\text{iters} \times \sum_{\text{net}} k \cdot A^*)\) | A* 每終端一次 |
| 詳細繞線 | \(O(\text{iters} \times \sum_{\text{net}} k \cdot A^*_{\text{corridor}})\) | 走廊把 A* 限制在局部 |
設計哲學一以貫之:把全域問題切成局部問題(樹覆蓋切樹、退火只重算增量、 詳細繞線只在自己的走廊裡搜尋),讓瀏覽器裡的 WASM 版也能在秒級跑完整流程。
5.1.6 一次真實輸出長什麼樣#
用一個小設計(v2c asic)跑完流程後,Output 的人類可讀摘要大致是:
area: 1234 cells, 8120 µm² utilization 63%
cells: NAND2 ×412 DFF ×188 AOI21 ×97 ...
fmax (wirelength placement, ideal clock): 312 MHz
fmax (timing-driven, ideal clock): 351 MHz ← 時序驅動佈局的收益
fmax (after CTS, propagated clock): 338 MHz ← 時鐘樹 skew 的代價
CTS: clk → 188 sinks, 23 CLKBUFs, 5 levels
hold buffers: 6
detail route: 0 conflicts, 0 opens
讀懂這份報告等於讀懂本章:每一行對應流程圖的一個方塊,而三個 Fmax 的 遞進(312 → 351 → 338)正是 5.3(時序驅動佈局)與 5.6(時鐘樹代價)兩章 的主題。counts 按數量排序也透露設計的性格——組合邏輯重的設計 NAND2 會一枝獨秀,控制密集的設計 DFF 與 MUX2 會突出。
5.1.7 與 FPGA 流程的對照#
同一份閘級網路在專案裡有兩個物理歸宿,對照著看更能看清 ASIC 後端的 每一步「在解什麼」:
| 面向 | iCE40 FPGA(第 4 章) | ASIC(本章) |
|---|---|---|
| 基本單元 | LUT4 + SB_DFF(固定的) | 標準單元(映射期自由選擇) |
| 映射 | LUT 覆蓋(布林拆解) | 樹覆蓋(單元 pattern) |
| 佈局 | 固定 Fabric 格點(blecher) | 列/site + 退火 |
| 繞線 | 晶片內建 switch box 拓撲 | 自建 gcell/軌道網格 |
| 時鐘 | 專用全域時鐘網路(硬體給定) | CTS 自建緩衝樹 |
| 位流/版圖 | bitstream(配置 LUT/路由 SRAM) | GDS(真實幾何) |
FPGA 的物理資源是製造時固定的,所以後端的自由度小、要解的是 「適配」;ASIC 的資源由後端自己創造(die 大小、單元選擇、時鐘樹), 所以每一步都是開放式最佳化。這也解釋了為什麼 ASIC 流程多了 utilization 重試、CTS、PDN 這些 FPGA 流程沒有的環節。
本章重點#
- ASIC 後端 = 映射 → 佈局 → 繞線 → STA → CTS → hold 修復 → PDN → 詳細繞線 → DRC → GDS,次序由資料相依決定(CTS 在佈局後繞線前;詳細繞線在 PDN 後)。
- 利用率是核心旋鈕:
physical()內繞線探測失敗就降 util 放大 die;flow_with()外層對 PDK 在詳細繞線不乾淨時以util × 0.85重試(≤4 次)。 - vlib 是雙層結構的邏輯層:後端一律講邏輯名,
Tech綁定表在最後翻譯成 PDK 名;is_vlib()guard 包住製程專屬的物理規則。 Output保留fmax_wl/fmax_ideal,讓每個最佳化步驟的收益/代價可被量測。