5.2 佈局與繞線:退火與 PathFinder#
對應原始碼:
eda/src/fpga/pnr/place.rs、eda/src/fpga/pnr/route.rs、eda/src/fpga/pnr/sta.rs、eda/src/fpga/pnr/chipdb.rs上一節:5.1 LUT 覆蓋與進位鏈 下一節:5.3 位元流格式
5.2.1 FPGA 佈局的約束#
FPGA 佈局不是 ASIC 佈局。它受到三種結構性限制:
- 固定結構:晶片上的邏輯單元位置是固定的,不能移動。
- 進位鏈必須連續:一條進位鏈佔據同一欄的連續 LC。
- 控制集共享:一個 tile 的 8 個 LC 共用時脈與重置。
因此 FPGA 佈局是「把設計塞進一個既定的格子」,而不是「把格子畫在設計周圍」。
5.2.2 裝置資料庫:chipdb-5k.txt#
FPGA 工具需要知道晶片上有什麼。rueda 用的是 IceStorm 的 chipdb-5k.txt。
5.2.2.1 尋找路徑#
pub fn default_path() -> Option<String> {
if let Ok(p) = std::env::var("VERYL_EDA_CHIPDB") { return Some(p); }
let home = std::env::var("HOME").unwrap_or_default();
let cands = [
"/opt/homebrew/share/icebox/chipdb-5k.txt".to_string(),
"/usr/local/share/icebox/chipdb-5k.txt".to_string(),
"/usr/share/icebox/chipdb-5k.txt".to_string(),
"/Users/Shared/ccc/oss-cad-suite/share/icebox/chipdb-5k.txt".to_string(),
format!("{}/oss-cad-suite/share/icebox/chipdb-5k.txt", home),
];
cands.into_iter().find(|p| std::path::Path::new(p).exists())
}
環境變數優先,否則試五個常見位置。找不到時的錯誤訊息是明確的:
"IceStorm chipdb-5k.txt not found (set VERYL_EDA_CHIPDB)"
5.2.2.2 檔案格式#
這是一個分節的文字格式。以 . 開頭的行是節標頭,資料行直到下一個 . 開頭的行。# 開頭是註解,會重置當前節。
| 節 | 意義 | 解析成 |
|---|---|---|
.device 5k <w> <h> <n> | 裝置名稱、寬、高、網路數 | w、h、各陣列大小 |
.pins <package> | 封裝接腳 | pins、pin_order |
.gbufin | 每個 fabout 的 (x, y, global_index) | gbufin |
.gbufpin | 可直接驅動全域網路的接腳 | gbufpin |
.extra_bits | tile 之外的設定位 | extra_bits |
.ieren | 輸入接腳到內部索引 | ieren |
.colbuf | 每欄緩衝器的來源 tile | colbuf_src |
.net <id> | 每個網路出現的 (x, y, name) | net_box、net_label、kind |
.buffer / .routing | (x, y, dst_net) <pattern> <src_net> B<row>[<col>]… | pips、groups |
.<type>_tile_bits | 設定位的座標 | dims、tile_bits |
.<type>_tile | 各 tile 的位置 | tiles、tile_type |
設定位在 tile 內的表示是 B<row>[<col>]:
fn bitpos(s: &str) -> R<BitPos> {
let s = s.strip_prefix('B').ok_or_else(|| format!("bad bit '{}'", s))?;
let (r, c) = s.split_once('[').ok_or_else(|| format!("bad bit '{}'", s))?;
let c = c.trim_end_matches(']');
Ok((r.parse()..., c.parse()...))
}
多工器的樣式是一個二進位字串,第 i 個字元對應 group[i](目的多工器的第 i 個設定位),LSB 優先解碼:
let pat = p[0].bytes().enumerate().fold(0u32, |a, (i, b)| a | (((b == b'1') as u32) << i));
5.2.2.3 記憶體的取捨#
fn wanted(name: &str) -> bool {
name.starts_with("lutff_") || name.starts_with("io_") || name.starts_with("glb_netwk_")
|| name == "fabout" || name.starts_with("carry_in") || name.starts_with("ram/")
}
解析器只保留佈局繞線會查詢的網路。 UP5K 有上萬條網路,但佈局繞線只碰邏輯單元的輸入輸出、IO、全域網路、進位與 RAM。過濾掉不需要的資料,記憶體用量降到 1/10。
但每個網路的邊界框、標籤與種類仍然完整保留,所以繞線的圖是完整的。
5.2.2.4 記憶體中的結構#
pub struct Pip { pub x: u8, pub y: u8, pub src: u32, pub dst: u32,
pub group: u32, // Chip::groups 的索引
pub pat: u32 } // 該 group 的位元值,位 i = group[i]
pub struct Chip {
pub w: i32, pub h: i32,
pub tiles: Vec<((i32,i32), String)>,
pub tile_type: HashMap<(i32,i32), String>,
pub dims: HashMap<String, (usize, usize)>,
pub tile_bits: HashMap<String, HashMap<String, Vec<BitPos>>>,
pub by_name: HashMap<(i32,i32,String), u32>,
pub net_box: Vec<(i16,i16,i16,i16)>,
pub net_label: Vec<(i32,i32,String)>,
pub kind: Vec<Kind>,
pub pips: Vec<Pip>,
pub groups: Vec<Vec<BitPos>>,
pub fanout: Vec<Vec<u32>>,
pub pins: HashMap<String, (i32,i32,u8)>,
...
}
Kind 從網路名稱推導:
| 名稱前綴 | Kind |
|---|---|
local_g* | Local |
lutff_*/in_* | LutIn |
lutff_*/out | LutOut |
glb_netwk_* | Glb |
sp4_* / span4_* | Sp4H / Sp4V |
sp12_* | Sp12H / Sp12V |
ram/RDATA* | LutOut |
ram/*ADDR|WDATA|MASK* | LutIn |
carry_in* | Carry |
fabout | Fabout |
io_* | Io |
5.2.3 佈局:模擬退火#
5.2.3.1 成本函數#
let hpwl = |n: usize, slot: &Vec<usize>, ram_site: &Vec<usize>| -> f64 {
let (mut x0, mut y0, mut x1, mut y1) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
for pn in &net_pins[n] {
let (x, y) = pos(pn, slot, ram_site);
x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y);
}
if x0 == i32::MAX { 0.0 } else { ((x1 - x0) + (y1 - y0)) as f64 * net_w[n] }
};
半周長線長(HPWL) 是佈局成本函數的標準選擇:
理由:它是對實際線長的一階近似。真實線長是曼哈頓距離的總和(受 routing channel 的形狀影響),HPWL 是它的下界,但相關性極高。
全域網路(時脈、重置)與常數完全排除在成本之外——它們走全域網路,不佔用一般佈線資源。
net_w[n] 是時序權重(佈線階段是 1.0)。
5.2.3.2 退火排程#
初始溫度:
// 200 次隨機移動的代價差的標準差,乘 2,下限 1.0
let m = d.iter().sum::<f64>() / d.len() as f64;
(d.iter().map(|x| (x-m)*(x-m)).sum::<f64>() / d.len() as f64).sqrt().max(1.0) * 2.0
每個溫度階段的移動次數:
let moves_per_t = ((movable.len() + p.rams.len()) as f64).powf(1.33) as usize * 4 + 100;
指數 1.33 是經典的「單元數越多,移動次數略超線性」的啟發式。
接受準則(Metropolis):
if delta <= 0.0 || rng.unit() < (-delta / temp).exp() { accept } else { undo }
溫度排程(依接受率調整):
temp *= if rate > 0.96 { 0.5 } else if rate > 0.8 { 0.9 } else if rate > 0.15 { 0.95 } else { 0.8 };
rlim = (rlim * (1.0 - 0.44 + rate)).clamp(1.0, ncols.max(H) as f64);
冷卻的邏輯:幾乎全部都被接受時(rate > 0.96)快速冷卻(×0.5);接受率中等時緩慢冷卻;接受率很低時最慢(×0.8),因為這時退火還有進步空間。
移動半徑 rlim 用同樣的接受率訊號調整:rate = 0 時半徑縮小 44%,rate = 1 時不變。
終止條件:
if temp < 0.005 * cost.max(1.0) / net_pins.len() as f64 { break; }
溫度門檻是「每個網路的平均成本」的一個百分比——這讓溫度的尺度與設計大小無關。
5.2.3.3 移動的合法性#
// 選一個隨機 LC,選一個半徑內的隨機目的地,選目的地 tile 裡 8 個 LC 中的一個
let r = rlim.max(1.0) as i32;
let nci = (ci + rng.below((2*r+1) as usize) as i32 - r).clamp(0, ncols-1);
let ny = (ay + rng.below((2*r+1) as usize) as i32 - r).clamp(1, H);
let sb = nci as usize * PER_COL + (ny as usize - 1) * 8 + rng.below(8);
如果目標位置被占用,兩個 LC 交換。 固定的 LC(進位鏈)永遠不會被移走。
交換後要重新檢查控制集合法性:一個 tile 只能容納控制集相同的 LC(或是沒有正反器的 LC)。
5.2.3.4 初始合法佈局#
退火之前先建立一個合法佈局:
- 進位鏈優先,最長的先放,欄位順序是「由中間向兩側」:
let col_order: Vec<usize> = (0..cols.len())
.map(|i| if i % 2 == 0 { mid + i/2 } else { mid - 1 - i/2 })
.filter(|c| *c < cols.len()).collect();
「由中間向兩側」的理由:進位鏈是垂直的,從中間開始讓上下平衡,減少水平佈線。
欄內的掃描每次前進 8 個 slot(整個 tile),確保一條鏈永遠從某個 tile 的 LC 0 開始——因為進位鏈必須從 tile 的進位輸入接起。
- 其餘 LC 逐 tile 放入同一欄序中控制集相符的第一個空位。
- RAM 放在前
rams.len()個 site。
5.2.3.5 RAM site 的排序#
sites.sort_by_key(|&(x,y)| ((y - H/2).abs(), x, y));
RAM site 由中間往外排。 因為 RAM 的資料匯流排在垂直方向更長,放在中間可以讓資料匯流排的垂直線最短。
5.2.4 繞線:PathFinder 協商#
FPGA 繞線的模型:
每條晶片網路(wire)只能載一個訊號。 一個 pip 進入某條 wire,就是設定那條 wire 的輸入多工器。
因此繞線問題是:把每個網路的 sink 用一組 pip 連到 source,且不共用任何 wire。
5.2.4.1 資料結構#
pub struct RNet {
pub name: String,
pub src: u32,
pub sinks: Vec<u32>,
/// 要使用的全域網路:(fabout wire, global wire, 直接從 global 到的 sinks)
pub global: Option<(u32, u32, Vec<u32>)>,
pub crit: f32, // 時序緊要度 0..1
}
5.2.4.2 協商迴圈#
for iter in 0..200 {
for &ni in &order { // 依 sink 數遞減排序
// ...
if iter > 0 && !nodes_of[ni].iter().any(|w| occ[*w as usize] > 1) { continue; } // 只重繪衝突的網路
// A* 搜尋
}
let over: Vec<usize> = (0..n).filter(|w| occ[w] > 1).collect();
iters.push(over.len());
if over.is_empty() { return Ok((routes, iters)); }
for w in over { hist[w] += 0.5 * (occ[w] - 1) as f32; }
pres *= 1.8;
}
這是經典的 PathFinder:
| 機制 | 作用 |
|---|---|
hist[w] += 0.5 × (occ[w] - 1) | 歷史成本:某條 wire 被超用過,就變得更貴,讓後來的網路避開 |
pres *= 1.8 | 當前成本:每輪迭代,佔用越嚴重的 wire 成本成長越快 |
| 只重繪衝突網路 | 撕開重繪(rip-up and reroute):沒衝突的不動 |
「歷史成本」與「當前成本」分離是 PathFinder 的核心:
- 當前成本讓「現在擠」的 wire 變貴;
- 歷史成本讓「一直很擠」的 wire 逐漸累積更高的懲罰。
兩者相乘:
let over = occ[v as usize] as f32;
let cong = (1.0 + hist[v as usize]) * (1.0 + pres * over);
5.2.4.3 A* 搜尋#
let mut heap = BinaryHeap::new();
for &t in &tree_order {
dist[t as usize] = 0.0; prev[t as usize] = u32::MAX; seen[t as usize] = gen;
heap.push(Item(tdist(t, tx, ty) * 0.3, t));
}
while let Some(Item(_, u)) = heap.pop() {
if u == sink { found = true; break; }
let fan = chip.fanout[u as usize].iter().map(|p| (*p, chip.pips[*p as usize].dst))
.chain(extra_fanout.get(&u).into_iter().flatten().copied());
for (p, v) in fan {
let o = owner[v as usize];
if o >= 0 && o != ni as i32 { continue; } // 被保留的 wire
let over = occ[v as usize] as f32;
let cong = (1.0 + hist[v as usize]) * (1.0 + pres * over);
let c = if crit > 0.0 {
let d = super::sta::hop_delay(chip.kind[u as usize], chip.kind[v as usize]) as f32 / 0.6;
crit * d + (1.0 - crit) * cong
} else { cong };
let nd = du + c;
if seen[v as usize] != gen || nd < dist[v as usize] { ... }
}
}
四個關鍵設計:
(a) 從「整棵樹」而不是單一節點出發。 初始堆裡放入樹上所有節點(距離 0)。這樣每個新 sink 都從樹上最合適的位置接上,形成一棵樹。
(b) owner[] 保留機制。 網路的 source 與 sink 所在的 wire 被保留給那個網路,其他網路不能經過。這是路由安全的關鍵——否則兩個網路會爭同一個 LC 的輸入接腳。
(c) 時序混合。 緊要度 \(c\) 時,邊成本變成
0.6 ns 是「典型 hop」的正規化常數。這樣成本函數的量綱一致(兩個項都是「越大越差」)。
緊要的路線會選快的 wire,不緊要的會選不擠的 wire。
(d) 啟發函數。
// tdist(v, target) × 0.3:目標點到 wire 邊界框的曼哈頓距離
0.3 的權重讓啟發函數「不完全可接受」(admissible-ish),這是加權 A*:它會找到更好的解,但可能探索更多節點。在繞線裡這是對的取捨——我們要的是品質,不是最短時間。
5.2.4.4 全域網路#
時脈與重置是特殊的:
// 網路先被繞到一個 fabout wire,然後用一個偽 pip 代表硬接的
// fabout → glb_netwk_g 連接,全域網路的 sinks 直接由全域 wire 連出
let pip_dst = |p: u32| -> u32 {
if p >= u32::MAX - extra_pips.len() as u32 { extra_pips[(u32::MAX - p) as usize].1 }
else { chip.pips[p as usize].dst }
};
全域網路的成本是 0(Kind::Fabout => 0.0)——走全域網路不花費一般佈線資源。
5.2.4.5 樹的建構#
// sink 依到 source 的距離排序(近的先),每個用 A* 從目前的樹接上,
// 然後把回溯路徑嫁接進樹
「近的先」 是重要的啟發式:先接最近的 sink,樹的形狀更緊湊。
5.2.5 時序分析#
eda/src/fpga/pnr/sta.rs 用 IceStorm 的 timings_up5k.txt(slow corner)。
5.2.5.1 延遲模型#
const CLK_Q: f64 = 1.391; // clk -> Q
const LUT: [f64; 4] = [1.285, 1.232, 1.205, 0.874]; // in_k -> out
const SETUP: [f64; 4] = [1.232, 1.060, 0.980, 0.728]; // in_k -> setup
const CARRY_IN1: f64 = 0.675; // in_1 -> carry
const CARRY_IN2: f64 = 0.609; // in_2 -> carry
const CARRY_CIN: f64 = 0.278; // carry-in -> carry-out
const RAM_CLK_Q: f64 = 1.179; // RCLK -> RDATA
const IO_IN: f64 = 1.105; // PRE_IO -> DIN0
const IO_OUT: f64 = 0.187; // 輸出墊
LUT[k] 隨著輸入編號 k 遞減——因為 LUT 的輸入 0 通常來自最近的來源(延遲較短),這是硬體的實測特性。
block RAM 輸入的建立時間:
pub fn ram_setup(pin: &str) -> f64 {
if pin.starts_with("RADDR") { 0.384 } else if pin.starts_with("WADDR") { 0.424 }
else if pin.starts_with("WDATA") { 0.305 } else if pin.starts_with("MASK") { 0.517 }
else if pin == "WE" { 0.252 } else if pin == "RE" { 0.185 } else { 0.503 }
}
5.2.5.2 Hop 延遲#
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::Glb2Local => 0.583,
Kind::Glb => 0.278,
Kind::Sp4H => if from_lc { 0.649 } else { 0.543 },
Kind::Sp4V => if from_lc { 0.649 } else { 0.596 },
Kind::Sp12H=> if from_lc { 1.232 } else { 1.231 },
Kind::Sp12V=> if from_lc { 1.232 } else { 1.072 },
Kind::Carry => 0.556,
Kind::Fabout => 0.0,
Kind::Io => 0.0,
Kind::LutOut | Kind::Other => 0.5,
}
}
延遲由「目的 wire 的種類」決定(不是起點)。因為一個 wire 的延遲是它自己的(routing wire 的延遲來自它的長度與負載)。
Kind::Glb(全域網路)只有 0.278 ns,而 Kind::Local(本地佈線)有 1.099 ns——接近 4 倍的差距。這就是為什麼時鐘要用全域網路。
Kind::Fabout 與 Kind::Io 是 0:它們是連接點,不是真正的 wire。
5.2.5.3 三趟演算法#
(a) 路由延遲。 對每個非全域網路,從每個 sink 沿著 pip 鏈往回走到 source,累加 hop_delay(kind[pip.src], kind[pip.dst])。
(b) 前向到達時間。 對每個 LC,用顯式堆疊的 DFS 計算 out_arr[l]:
- 到達輸入 k = 驅動 LC 的輸出到達時間 + 路由延遲,然後
+ LUT[k]; - 進位 LC 從
cin + CARRY_CIN或in_1 + CARRY_IN1/in_2 + CARRY_IN2算cout_arr[l]; - 有正反器的 LC 的
out_arr = CLK_Q(時脈理想,無偏斜)。
在 FF 處切斷迴圈——這是 DFS 不爆棧的原因。
(c) 端點。
| 端點 | 延遲 |
|---|---|
| FF 輸入 | 到達 + SETUP[k] |
| RAM 輸入 | 到達 + 路由 + 建立時間 |
| 輸出接腳 | 到達 + 路由 + IO_OUT |
最差的就是關鍵路徑; \(f_{\max} = 1000 / \text{period}\) (MHz,週期是 ns)。
5.2.5.4 關鍵路徑報告#
// 沿著路徑往回走,產生 Vec<Step>,每個 Step 有
// what(人類可讀的說明)、delay、at(累積時間)
「人類可讀的說明」很重要——時序報告是設計者唯一會讀的東西。
5.2.5.5 網路緊要度#
// 後向最長路徑:tail_out[l] = 從 l 的輸出到端點的最長延遲
let net_crit: Vec<f32> = nets.iter().enumerate().map(|(ni, net)| {
if net.global.is_some() { return 0.0; }
let src = src_arr(ni, &out_arr);
let mut worst = 0.0f64;
for &s in &net.sinks {
let d = sink_route.get(&s).map(|x| x.1).unwrap_or(0.0);
let after = match sink_ep(s) { ... };
worst = worst.max(src + d + after);
}
(worst / period).min(1.0) as f32
}).collect();
全域網路的緊要度是 0——它們不在關鍵路徑上(時脈是理想的)。
這個值餵回繞線的成本函數(第 5.2.4.3 節),形成「佈局 → 佈線 → 時序 → 佈線」的迭代閉環。
5.2.6 迭代閉環#
flowchart LR
A["映射"] --> B["佈局<br/>退火,HPWL 成本"]
B --> C["繞線<br/>PathFinder"]
C --> D["時序分析<br/>hop delay"]
D --> E["網路緊要度"]
E --> C
E -.->|"重佈局(權重更新)"| B這是現代 P&R 工具的核心迴圈。 只佈線一次無法得到好的時序,因為佈線的結果決定了哪些路徑是關鍵的。
5.2.7 與 ASIC 佈局繞線的對比#
| FPGA(本章) | ASIC(第 6 章) | |
|---|---|---|
| 佈局對象 | 固定網格上的 LC | 連續座標上的單元 |
| 成本 | HPWL | 加權 HPWL + 密度懲罰 |
| 繞線圖 | 晶片的 pip/wire 圖 | 網格圖(gcell)+ 層 |
| 容量 | 每條 wire 只能一個訊號 | 每個 gcell 邊有容量(CAP) |
| 演算法 | PathFinder | PathFinder + 走廊限制 |
| 特色 | 控制集、進位鏈連續性 | 密度、天線、PDN 阻塞 |
共同的骨架是 PathFinder,這是 1990 年代 Dean 的經典貢獻,兩個領域都沿用。
下一節:5.3 位元流格式與 CRC