6.6 位元流反解(board/bitstream.rs)#
原始碼:
veryl-eda/src/board/bitstream.rs(271 行)。 把 iCE40UP5K 的.bin/.asc位元流解回電路, 讓虛擬開發板跑的正是「本來會燒錄的東西」。
6.6.1 為什麼反解是可行的#
SRAM 型 FPGA 的位元流就是組態記憶體的內容:
每一個 LUT 的 16 個真值表位元、每一個 FF 的模式位元、 每一個 routing mux 的選擇位元、block RAM 的初始內容, 都在位元流裡有固定的位置。晶片上電時把這些位元灌進 組態 RAM,硬體就直接「變成」該電路。因此:
讀懂位元流 = 讀懂晶片上電後的電路。
不需要反組譯、不需要啟發式——每個位元的座標 (tile, row, col)與語義都是 iCE40 晶片資料庫 (pnr::chipdb,IceStorm 的 chipdb-5k.txt)給定的。 icebox 工具鏈的 .asc 文字格式把二進位位元流 展開成人可讀的座標列表,本模組直接吃兩種格式。
6.6.2 Config:兩種輸入、一種表示#
pub struct Config {
pub tiles: HashMap<(i32, i32), Vec<Vec<bool>>>,
/// block RAM contents by ramb tile: 256 words of 16 bits (INIT_0 word 0 first)
pub ram: HashMap<(i32, i32), Vec<u16>>,
pub extra: HashSet<(usize, usize, usize)>,
}
parse_asc 逐行解析:
.logic_tile X Y後面的0/1列 → 該 tile 的位元陣列;.ram_data X Y後面的 16 行 64 位十六進位 → block RAM 初始 256 個字(INIT_k,最高位在前);.extra_bit X Y B→ 全域網路等特殊位元。
pub fn parse_bin(bin: &[u8]) -> R<Config> { Self::parse_asc(&crate::pnr::bitstream::bin_to_asc(bin)?) }
.bin 先經 bin_to_asc(pnr/bitstream.rs)反組譯成 .asc 文字——icepack 的位元流壓縮格式在那裡被解開。 也就是說本模組只面對一種表示:座標化的位元。
6.6.3 decode:從位元到 Design#
decode(chip, cfg) -> (Design, Report) 的三個步驟:
flowchart LR
A["1. routing<br/>pip 位元樣式 → union-find"] --> B["2. 驅動源<br/>LC 輸出 / carry / IO / 全域 / RAM"]
B --> C["3. 元件<br/>LUT+FF、RAM、IO"]
C --> D["Design + Report"]第一步:routing = union-find#
routing mux 的本質:某組組態位元的值等於某個 pip 的 樣式(pattern)時,pip 的源線與目的線導通。 全部位元皆 0 = 沒有連接:
let mut group_val: HashMap<(u8, u8, u32), u32> = HashMap::new();
for p in &chip.pips {
if p.pat == 0 { continue; }
let v = *group_val.entry((p.x, p.y, p.group)).or_insert_with(|| {
chip.groups[p.group as usize].iter().enumerate().fold(0u32, |a, (i, b)| a | ((cfg.bit(p.x as i32, p.y as i32, *b) as u32) << i))
});
if v == p.pat { uf.union(p.src, p.dst); }
}
同一個 mux 群(group)的位元組成一個數,等於 pip 的 樣式就 union。union-find 之後,每個連通分量就是一根 電氣 net。全域網路(glb_netwk)有兩個來源: padin_glb_netwk 的 extra bits(腳位直驅全域),或 gbufin tile 的 fabout 硬連線。
第二步:驅動源#
每個 union-find 根只能有一個驅動者,重複驅動是警告 (真實晶片會短路):
let drive = |root_of: u32, what: String, ...| {
let r = uf.find(root_of);
if let Some(prev) = driven.insert(r, what.clone()) {
rep.warnings.push(format!("{} and {} drive the same wire {}", prev, what, chip.label(root_of)));
}
};
驅動者包括:LC 輸出、carry 輸出、IO 輸入腳(pad)、 block RAM 讀出、CarryInSet 常數。
第三步:邏輯單元#
每個 logic tile 的 8 個 LC 有 20 個組態位元。關鍵是 位元排列——LUT_INIT 的第 k 位在組態位元的 LUT_PERM[k] 位置(place and route 就是照這個寫的):
const LUT_PERM: [usize; 16] = [4, 14, 15, 5, 6, 16, 17, 7, 3, 13, 12, 2, 1, 11, 10, 0];
...
let lut: u16 = (0..16).fold(0, |a, k| a | ((v[LUT_PERM[k]] as u16) << k));
let (carry, ff) = (v[8], v[9]);
未組態的 LC(LUT 全零、無 carry、無 FF)直接跳過—— 它的輸出反正只會是 0,未驅動的線預設讀 0。 carry 也還原成 3 輸入多數閘(與 6.5 節的引擎同一個 0b1110_1000 真值表);FF 還原成 Ff(含 clock enable、 set/reset、極性)。
IO 與命名#
IO 的方向由 PIN_TYPE 位元判斷:
// PIN_TYPE[5:2] = 0110: simple output; [1:0] = 01: simple input
let output = t3 && t4 && !t2 && !t5;
let input = t0 && !t1;
輸出腳接 io_z/D_OUT_0,輸入腳則創一個 pad net 並把 D_IN_0 綁到它。IO 的名字就是封裝腳號("35" 這樣的字串):
} else if input || used_glb {
let pad = new_net(&mut d);
d.inputs.push((pin.clone(), pad));
rep.pins_in.push(pin.clone());
這就是 6.5 節說的「named IO by pin」的來源。
6.6.4 腳位命名回 pcf:pins_pcf / board_from_pins#
board/icebreaker.rs 提供兩個橋接:
pins_pcf(&d):把解碼出的腳號轉成一份 pcf 文字 (set_io 35 35這種自指形式),讓Board::new能綁定;board_from_pins(dd):直接從腳位構板。
wasm 端的模式選擇(api.rs::board_flash)把它串起來:
let (d, rep) = CHIP.with(|c| -> R<(Design, bitstream::Report)> {
bitstream::decode(c.as_ref()...?, &bitstream::Config::parse_bin(&bin)?)
})?;
let pins_pcf = icebreaker::pins_pcf(&d);
let mut b = icebreaker::board(d.clone(), Some(&pins_pcf))?;
b.warnings.extend(rep.warnings.iter().cloned());
注意警告的一條:Fmax 低於 12 MHz 會提醒 「真板子可能跑不動這個模擬」——反解讓我們拿得到 P&R 的時序結果,這是 netlist 模式沒有的額外保真。
6.6.5 board_flash:完整流程#
wasm 的 board_flash(api.rs:374):
flowchart LR
V["Veryl 原始碼"] --> SYN["synth_netlists<br/>解析→elab→最佳化→ice40 map"]
SYN --> PNR["place_and_route_on<br/>(own P&R,載入的 chipdb)"]
PNR --> ASC[".asc"]
ASC --> BIN["asc_to_bin<br/>.bin"]
BIN --> DEC["Config::parse_bin + decode<br/>→ Design(腳號命名)"]
DEC --> BOARD["icebreaker::board<br/>虛擬板電源開"]這條鏈刻意繞遠路:先產生位元流、再從位元流解回設計, 而不是直接用 netlist 建 Design。好處:
- 板上跑的行為包含 P&R 的真實連接——如果路由有 bug, 模擬也會跟著錯,等於多一層 P&R 的驗證;
- IO 名字從腳號來,使用者看到的板子與真硬體腳位一致;
board_program(換程式不重新 P&R)需要.asc做patch_ram——只有經過位元流的模式才有這個能力。
對照 board_load(netlist 模式,快、少一層)與 board_load_bin(任意外部位元流,例如 nextpnr + icepack 產生的 .bin):
| 指令 | 來源 | IO 命名 | 保真層 |
|---|---|---|---|
board_load | netlist | 埠名(pcf) | 邏輯 |
board_flash | own P&R 位元流 | 腳號 | 邏輯 + 佈局繞線 |
board_load_bin | 任意 .bin | 腳號 | 位元流原樣 |
6.6.6 位元流如何被 patch:換程式不重跑 P&R#
board_program 對位元流模式的處理(api.rs:493):
Loaded::Bitstream { asc, map } => {
for (m, words) in veryl_eda::bram::program_lanes(&names, &bytes)? { *asc = veryl_eda::pnr::patch_ram(asc, map, &m, &words)?.0; }
let bin = veryl_eda::pnr::bitstream::asc_to_bin(asc)?;
let (d, _) = ... decode ...;
}
ram_map 記住每個 block RAM 被放到哪個 tile, patch_ram 只改寫那些 tile 的 .ram_data 行, 再組回 .bin、反解、上電。使用者體感是「改 C 程式, 秒級重新開機」——背後是 RAM 初始位元的精準替換。
6.6.8 Report:反解的品質回報#
Report 是反解的副產品,也是 UI 的原料:
pub struct Report {
pub lcs: usize,
pub rams: usize,
pub pins_in: Vec<String>,
pub pins_out: Vec<String>,
pub warnings: Vec<String>,
}
lcs/rams:板上顯示的資源用量(netlist 模式給的是 LUT/FF/RAM 計數,位元流模式只能給 LC 與 RAM—— carry 和 FF 都已併進 LC);pins_in/pins_out:讓頁面把腳位畫成可互動的板面;warnings:雙驅動、不支援的 IO 模式 (PIN_TYPE不是簡單輸入/輸出,如三態或差分)都會 帶著腳號與模式位元回報。
6.6.9 為什麼這樣設計#
- 吃
.asc而非直接解.bin:座標化的文字是 IceStorm 生態的通用語言,icebox_vlog、icetime 都用它; 本模組保持單一表示。 - union-find 而非逐 pip 建圖:pip 只是「兩線導通」, net 的本來面目就是連通分量;union-find 的路徑壓縮讓 幾萬個 pip 也是瞬間。
- LUT_PERM 常數:位元排列寫死在常數裡, 與
pnr/bitstream.rs的寫入端對稱——改一邊就會被cargo test bitstream抓住。 - Report 而非例外:反解總是成功(位元流是合法的), 但語義上有警告(雙驅動、不支援的 IO 模式)—— 這些應該顯示給使用者而不是中止。
6.6.8 本章重點#
- 位元流是組態 RAM 的直接映射:LUT 位元、FF 模式、 pip 樣式、RAM 初始值各有固定座標,反解是查表不是猜。
Config::parse_asc / parse_bin→decode(chip, cfg): pip 樣式比對 → union-find 得 net;驅動源逐一登記 (雙驅動警告);LUT 經LUT_PERM還原。- IO 以封裝腳號命名;
pins_pcf把腳號轉回 pcf 給Board::new。 - wasm
board_flash:synth → own P&R → bin → decode → 上電;board_program靠.ram_datapatch 換程式。 - 副產品的保真:Fmax 警告、真實路由、真實腳位。