4.3 ECP5 映射:ecp5.rs#
目標晶片:Lattice ECP5(LFE5U-85F,nextpnr-ecp5 預設
--85k --package CABGA381)。輸入與 iCE40 相同:通用閘級網路。 輸出是 Project Trellis / yosys 認得的LUT4 / CCU2C / TRELLIS_FF。 完整原始碼veryl-eda/src/ecp5.rs(203 行),第一行就說明了共用策略:
use crate::ice40::{find_carries, lut_cover, truth, PAT};
LUT 覆蓋演算法與 iCE40 完全共用,ECP5 檔案裡真正的新東西只有三件: CCU2C 進位鏈的打包、TRELLIS_FF 的字串參數、以及拓撲重排。 本章逐段拆解。
4.3.1 LUT4:與 iCE40 同一個覆蓋器,不同引腳名#
ECP5 的 LUT4 引腳是 A B C D → Z(iCE40 是 I0..I3 → O), 參數叫 INIT 而不是 LUT_INIT,位元序同樣是 A = LSB (ecp5.rs:64-74):
// ecp5.rs — LUT 生成,覆蓋結果來自 ice40::lut_cover
let mut lut_order: Vec<u32> = used.iter().copied().collect();
lut_order.sort_by_key(|n| pos[n]); // 閘序,輸出確定性
for n in lut_order {
let leaves = &best[&n].leaves;
let mut memo: HashMap<u32, u16> = HashMap::new();
for (i, l) in leaves.iter().enumerate() { memo.insert(*l, PAT[i]); }
let tt = truth(n, &gates, &pos, &mut memo);
for (i, p) in ["A", "B", "C", "D"].iter().enumerate() {
conns.push((p, leaves.get(i).map(|w| Bit::W(*w)).unwrap_or(Bit::C0)));
}
conns.push(("Z", Bit::W(n)));
cells.push(Cell { typ: "LUT4".into(), conns,
params: vec![("INIT", format!("{:016b}", tt))] });
}
不足四個輸入的 LUT,空腳接 Bit::C0(常數 0)——這不是隨便的補位: 真值表是按 (A,B,C,D) 的位元加權算的,腳空著對應 PAT 遮罩為 ...0000,真值表不受影響,因此不需要像某些流程那樣把未用腳 綁到已用腳。
4.3.2 CCU2C:一格兩位元的進位鏈#
ECP5 的進位硬體是 CCU2C:一個 slice cell 裡有兩個半格 (half 0 與 half 1),每個半格有一個 LUT4 與一個 LUT2,硬體式子 (與 ccu2c() 模擬函式一致):
yosys 對「加法半格」的標準編程是(ecp5.rs:14-17 的註解與常數):
/// CCU2C half that adds A + B + CIN (C = 0, D = 1: LUT4 = A ^ B, LUT2 = A)
pub const ADD_INIT: u16 = 0x96AA;
/// CCU2C half that only passes the carry on (LUT4 = 1: COUT = CIN)
pub const PASS_INIT: u16 = 0xFFFF;
驗證 ADD_INIT = 0x96AA = 1001_0110_1010_1010₂:
- LUT4 在索引 \(A + 2B\) ( \(C=0, D=1\) )的四個位置 (2,3,6,7 → bits 2,3,6,7 = 0,1,1,0)……
直接看語意:LUT4 = \(A \oplus B\) ,LUT2 = \(A\) ,代入硬體式:
正是全加器。
為什麼需要 PASS_INIT = 0xFFFF#
CCU2C 的半格是「LUT2 + 進位注入」的混合體,硬體強迫每個半格都要 有輸入。當一條進位鏈無法兩兩配對(奇數長、或中間某位元的和 不在鏈上),多出來的半格仍要佔著 COUT 的通路,於是把它編程成 恆等函數:
程式裡的分支(ecp5.rs:96-105):
let init1 = if let Some(j) = j {
done.insert(j);
let b1 = &bits[&j];
conns.extend([("A1", b1.x), ("B1", b1.y), ("C1", Bit::C0),
("D1", Bit::C1), ("COUT", Bit::W(j))]);
if let Some(s) = b1.sum { conns.push(("S1", Bit::W(s))); }
ADD_INIT // 真的加第二位元
} else {
conns.extend([("A1", Bit::C0), ("B1", Bit::C0), ("C1", Bit::C0),
("D1", Bit::C0), ("COUT", Bit::W(i))]);
PASS_INIT // 只當進位導管
};
兩格合併的條件:中間進位不得有別人用#
一個 CCU2C 的兩半之間,half0 的 COUT 是內部硬接線——它不會 出現在網路上。所以 bit \(i\) 與 bit \(i{+}1\) 能塞進同一格的條件是: bit \(i\) 的 carry-out 只被 bit \(i{+}1\) 的 carry-in 使用 (ecp5.rs:80-92,uses 統計每條 wire 被 LUT 葉、roots 與進位 鏈輸入引用的次數):
let j = next_of.get(&Bit::W(i)).copied()
.filter(|j| !done.contains(j) && uses.get(&Bit::W(i)) == Some(&1));
四個條件同時成立才合併:
next_of:bit \(i\) 的 carry-out 恰是 bit \(i{+}1\) 的 carry-in(鏈相鄰);!done.contains(j):bit \(i{+}1\) 還沒被別的格子收走;uses[ci] == 1:中間進位唯一的使用者是下一個進位鏈位元;- 隱含條件:連進位來源本身有需要——
need集合來自used_fixed, 亦即 LUT 覆蓋已確認「有人讀這個進位」。
為什麼這樣設計:把兩個位元壓進一格省一半的 cell 與繞線資源 (進位在格內是硬接線,不出 fabric)。但如果中間進位還要餵 LUT (例如比較器把它輸出到旗標),塞進內部線就等於把訊號藏起來了, 所以 uses == 1 是硬約束,不是最佳化。
FPGA 的進位鏈是純鏈,不能分支:一個 COUT 最多直接餵一個 CIN (nextpnr-ecp5 會以 "CCU2C unsupported" 拒絕分支鏈)。若網路裡 同一個 carry-out 訊號被兩個下游 CCU2C 當 CIN(例如兩條鏈共用一個 中間進位),第二個讀取者必須走 fabric。程式的處理 (ecp5.rs:110-127):偵測重複的 CIN 來源,插入一顆 buffer LUT (INIT = PAT[0],即 \(Z = A\) ),讓第二個 CIN 從 LUT 繞線進來:
// a carry chain does not branch: a COUT feeds at most one CIN directly,
// any other carry-in gets it through the fabric (a buffer LUT; nextpnr
// adds the feed-out / feed-in)
let couts: std::collections::HashSet<Bit> = cells.iter()
.filter(|c| c.typ == "CCU2C").filter_map(|c| c.port("COUT")).collect();
let mut seen: std::collections::HashSet<Bit> = Default::default();
let mut bufs = vec![];
for c in cells.iter_mut().filter(|c| c.typ == "CCU2C") {
let Some(k) = c.conns.iter().position(|x| x.0 == "CIN") else { continue };
let ci = c.conns[k].1;
if !couts.contains(&ci) || seen.insert(ci) { continue; } // 首次使用免費
let y = Bit::W(next_wire); next_wire += 1;
bufs.push(Cell { typ: "LUT4".into(),
conns: vec![("A", ci), ("B", Bit::C0), ("C", Bit::C0),
("D", Bit::C0), ("Z", y)],
params: vec![("INIT", format!("{:016b}", PAT[0]))] });
c.conns[k].1 = y;
}
cells.extend(bufs);
seen.insert(ci) 的用法值得注意:HashSet::insert 回傳 false 表示 「已經存在」,所以第一個讀取者直接通過,第二個以後才插 buffer。 這裡用 LUT 而不是真 buffer cell(ECP5 沒有 BUF 在 nextpnr 的 模型裡)——Z = A 的 LUT4 就是 1-bit 的可繞線 repeater,跟 4.2 節 iCE40 反相器用 LUT 是同一個哲學。
和(sum)輸出的所有權#
兩個 carry 位元可能共用同一個 xor(x ^ y)作為和——elab 層的 常數折疊有時會讓兩個加法位元看到同一對運算元。程式以 owner 表 只讓第一個位元輸出和(sum.filter(|s| owner.get(s) == Some(n))), 另一個的和由 LUT 覆蓋另外生成。
4.3.3 TRELLIS_FF:極性是 mux,不是 cell 變體#
iCE40 把 FF 極性編在型號裡(SB_DFFN*,04.2 節);ECP5 相反: slice 裡的時脈與 set/reset 到 FF 之間各有一個多工器,配置字串決定 接法。ecp5.rs:131-146:
// flip-flops: $_DFF_P_ / $_DFF_PN0_ / $_SDFF_NP1_ ...
// (clock and reset polarity are muxes in the slice)
for c in nl.cells.iter().filter(|c| c.is_ff()) {
let t = c.typ.as_str();
let f: Vec<char> = t.trim_start_matches("$_")
.trim_start_matches('S').trim_start_matches("DFF_").chars().collect();
let (q, d, ck) = (c.port("Q").unwrap(), c.port("D").unwrap(),
c.port("C").unwrap());
let mut conns = vec![("CLK", ck), ("DI", d), ("Q", q)];
let mut params = vec![
("GSR", "DISABLED".to_string()),
("CEMUX", "1".into()),
("CLKMUX", if f[0] == 'N' { "INV" } else { "CLK" }.into())];
if let Some(r) = c.port("R") {
conns.push(("LSR", r));
params.push(("LSRMUX", if f[1] == 'N' { "INV" } else { "LSR" }.into()));
params.push(("REGSET", if f[2] == '1' { "SET" } else { "RESET" }.into()));
params.push(("SRMODE", if t.starts_with("$_SDFF") { "LSR_OVER_CE" }
else { "ASYNC" }.into()));
} else {
params.push(("REGSET", "RESET".into()));
}
cells.push(Cell { typ: "TRELLIS_FF".into(), conns, params });
}
四個字串參數的語意:
| 參數 | 值 | 意義 |
|---|---|---|
CLKMUX | CLK / INV | 時脈直通 / 反相(對應 $_DFF_N_*) |
CEMUX | 1 | clock enable 恆為 1(elab 不產生 $_DFFE_*) |
LSRMUX | LSR / INV | set/reset 極性(對應名字第二碼 P/N) |
REGSET | RESET / SET | LSR 動作時 FF 是清 0 或置 1(第三碼 0/1) |
SRMODE | ASYNC / LSR_OVER_CE | 非同步($_DFF_*)/ 同步($_SDFF_*) |
GSR | DISABLED | 不用晶片級全域 set/reset |
兩個架構事實值得停下來:
- REGSET 同時是「重置值」與「上電值」:ECP5 FF 上電時的值由 REGSET 決定(這與 iCE40「上電恆為 0」不同,第 04.1 節的比較表)。 這就是
veryl-eda/test.sh做 ECP5 形式等價時要attrmap -remove init移除 yosys 的 init 屬性的原因。 - 無 reset 的 FF 也寫
REGSET=RESET:即使$_DFF_P_沒有 R 腳, slice 的 mux 仍需一個確定值;程式選 RESET(LSR 沒接、不會觸發)。
4.3.4 is_string_param:JSON 與 Verilog 寫出器的分歧#
ECP5 的參數有兩種型別:真值表(16-bit 位元向量)與 mux 選擇 (字串)。yosys JSON 對兩者的編碼不同——位元向量是 "016010010...", 字串若恰好由 0/1 組成會有歧義,yosys 的慣例是字串結尾加一個空格。 ecp5.rs:156-159:
/// parameters that are strings, not bit vectors (yosys JSON marks a string
/// of 0 / 1 with a trailing space)
pub fn is_string_param(cell: &str, k: &str) -> bool {
matches!(cell, "TRELLIS_FF" | "CCU2C") && !matches!(k, "INIT0" | "INIT1")
}
output.rs 的 JSON writer 據此決定是否補空格;Verilog writer 則相反, 所有參數都照字面寫。若這個分類出錯:"INV" 之類的字串會被當成 位元向量解析失敗,"10" 之類的位元向量會被 yosys 當字串。 表格化的分類函式(而非散落在寫出器裡的 if)讓 netsim、 gatesim.rs、測試都能引用同一份真相。
4.3.5 topo:ECP5 為何需要顯式重排#
iCE40 映射沿用原 netlist 順序(04.2 節),ECP5 卻必須重排,原因有二:
- buffer LUT 是後插入的,出現在向量尾端,但它的輸入可能驅動 位於前面的 CCU2C 的 CIN;
- LUT 與 CCU2C 分兩段生成(先全部 LUT、再全部 CCU2C),CCU2C 的 資料輸入接的 LUT 一定在前——這半邊沒問題——但 buffer LUT 破壞了 「兩段各自有序」的性質。
ecp5::topo 是迭代式 DFS 後序(ecp5.rs:179-203):
/// combinational cells in topological order
fn topo(cells: Vec<Cell>) -> Vec<Cell> {
let mut driver: HashMap<u32, usize> = HashMap::new();
for (i, c) in cells.iter().enumerate() {
for (p, b) in &c.conns {
if let (true, Bit::W(w)) = (is_out(p), b) { driver.insert(*w, i); }
}
}
let mut state = vec![0u8; cells.len()];
let mut order = vec![];
for i in 0..cells.len() {
// iterative depth-first post-order
let mut stack = vec![(i, false)];
while let Some((k, done)) = stack.pop() {
if done { state[k] = 2; order.push(k); continue; }
if state[k] != 0 { continue; }
state[k] = 1;
stack.push((k, true));
for (p, b) in &cells[k].conns {
if let (false, Bit::W(w)) = (is_out(p), b) {
if let Some(&d) = driver.get(w) {
if state[d] == 0 { stack.push((d, false)); }
}
}
}
}
}
...
}
注意它只對組合 cell 有效(FF 的 D→Q 是時序邊,呼叫端先過濾)。 迭代而非遞迴,是因為大設計(如 RISC-V SoC 的 8 KB 記憶體周邊) 組合深度可能數千,遞迴有 stack overflow 風險。三態 state (0 未訪、1 在堆疊、2 完成)避免同一節點重複進入後序。
4.3.6 ccu2c:軟體模型 = 硬體模型#
netsim 與 veryl-sim 的 gates.rs 需要 ECP5 cell 的行為模型。 ecp5::ccu2c 不另寫模擬邏輯,而是照 yosys cells_sim.v 的模型 原樣實作(ecp5.rs:162-176,half 即 4.3.2 開頭的硬體式子, get 以閉包注入、讓同一函式服務 2-state netsim 與 4-state 的 x 傳播):
pub fn ccu2c(c: &Cell, get: impl Fn(&str) -> bool) -> (bool, bool, bool) {
let init = |k: &str| u16::from_str_radix(&p(k), 2).unwrap_or(0);
let lut = |init: u16, a: bool, b: bool, cc: bool, d: bool|
(init >> (a as u16 | (b as u16) << 1 | (cc as u16) << 2 | (d as u16) << 3)) & 1 == 1;
let half = |init: u16, inject: bool, ins: [bool; 4], cin: bool| {
let l4 = lut(init, ins[0], ins[1], ins[2], ins[3]);
let l2 = (init >> (ins[0] as u16 | (ins[1] as u16) << 1)) & 1 == 1;
let s = l4 ^ (!inject && cin);
let co = (!l4 && !inject && l2) || (l4 && cin);
(s, co)
};
let (s0, c0) = half(init("INIT0"), p("INJECT1_0") != "NO",
[get("A0"), get("B0"), get("C0"), get("D0")], get("CIN"));
let (s1, c1) = half(init("INIT1"), p("INJECT1_1") != "NO",
[get("A1"), get("B1"), get("C1"), get("D1")], c0);
(s0, s1, c1)
}
為什麼這樣設計:模擬器與硬體的一致性是 gate-level 驗證的全部 價值。把行為模型寫成「讀同一份 cell 參數 → 按廠商模型計算」, 任何參數編碼的錯誤會立刻在模擬中顯現(test.sh 的形式等價與 gate-level 共模擬就是這個用意)。get 以閉包注入讓同一函式同時 服務 2-state netsim 與 4-state 的 x 傳播。
4.3.7 流程總覽#
flowchart TB
NL["通用 netlist<br/>$_AND_/$_OR_/... + $_DFF_*"] --> C["find_carries<br/>(ice40 共用)"]
C --> SUM["sum 所有權判定<br/>(xor 查表, owner)"]
SUM --> LC["lut_cover<br/>(ice40 共用)"]
LC --> LUT["LUT4 生成<br/>A,B,C,D→Z, INIT"]
LC --> CCU["CCU2C 生成<br/>uses==1 → 兩格合併<br/>否則 PASS_INIT"]
CCU --> BR["進位分支偵測<br/>第二個 CIN 插 buffer LUT"]
LUT --> T["topo 重排<br/>(迭代 DFS 後序)"]
BR --> T
NL --> FF["TRELLIS_FF<br/>CLKMUX/LSRMUX/REGSET/SRMODE"]
T --> OUT["Netlist → nextpnr-ecp5"]
FF --> OUT與 iCE40 的對照表:
| 主題 | iCE40 | ECP5 |
|---|---|---|
| 進位 cell | 獨立 SB_CARRY(majority) | CCU2C(兩半格,含 sum 輸出) |
| 進位打包 | LUT + CARRY 同 LC,靠 I1/I2/I3 對齊 | 兩位元同 cell,靠 uses == 1 |
| 進位分支 | 不允許(鏈必須從 LC0 開始,pack.rs 檢查) | 第二個 CIN 插 buffer LUT |
| FF 極性 | 型號編碼(SB_DFFN*),active-low 插反相器 | 字串 mux(CLKMUX = "INV"),免費 |
| FF 上電 | 恆 0 | REGSET 決定 |
| 參數型別 | 全是位元向量 | 混合,is_string_param 分類 |
| 拓撲順序 | 原順序倖存 | ecp5::topo 顯式重排 |
| --- |
4.3.8 本章重點#
- ECP5 映射與 iCE40 共用
lut_cover/find_carries/truth; 新程式碼只處理架構差異——這是「演算法層 vs 架構層」分層的直接示範。 CCU2C一格兩位元:合併條件是鏈相鄰 + 中間進位uses == 1; 未配對的半格編程INIT1 = 0xFFFF純粹當進位導管( \(c_{out}=c_{in}\) )。- 進位鏈不可分支:nextpnr-ecp5 會以 "CCU2C unsupported" 拒絕; 多餘的 CIN 讀取者改走 fabric,由
INIT = PAT[0]的 buffer LUT 提供。 ADD_INIT = 0x96AA:LUT4 = \(A \oplus B\) 、LUT2 = \(A\) ,硬體式 給出 \(s = A\oplus B\oplus c_{in}\) 、 \(c_{out} = \mathrm{maj}\) 。TRELLIS_FF的極性是 slice 內 mux:CLKMUX/LSRMUX/REGSET/SRMODE字串參數;REGSET同時決定重置值與上電值(與 iCE40 的恆 0 不同)。is_string_param是 JSON(字串補尾空格)與 Verilog 寫出器的分歧點, 也是 2-state 模擬器解析參數的依據。- buffer LUT 的插入打破原 netlist 順序,
ecp5::topo用迭代 DFS 後序 恢復拓撲序,供模擬器與 pack 消費。