跳轉到

6.6 位元流反解(board/bitstream.rs)#

原始碼:veryl-eda/src/board/bitstream.rs(271 行)。 把 iCE40UP5K 的 .bin / .asc 位元流解回電路, 讓虛擬開發板跑的正是「本來會燒錄的東西」。

6.6.1 為什麼反解是可行的#

SRAM 型 FPGA 的位元流就是組態記憶體的內容:

\[ \text{bitstream bit} \xleftrightarrow{\text{1 對 1}} \text{組態 RAM bit} \]

每一個 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) 的三個步驟:

第一步: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):

這條鏈刻意繞遠路:先產生位元流、再從位元流解回設計, 而不是直接用 netlist 建 Design。好處:

  1. 板上跑的行為包含 P&R 的真實連接——如果路由有 bug, 模擬也會跟著錯,等於多一層 P&R 的驗證;
  2. IO 名字從腳號來,使用者看到的板子與真硬體腳位一致;
  3. 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 為什麼這樣設計#

  1. 吃 .asc 而非直接解 .bin:座標化的文字是 IceStorm 生態的通用語言,icebox_vlog、icetime 都用它; 本模組保持單一表示。
  2. union-find 而非逐 pip 建圖:pip 只是「兩線導通」, net 的本來面目就是連通分量;union-find 的路徑壓縮讓 幾萬個 pip 也是瞬間。
  3. LUT_PERM 常數:位元排列寫死在常數裡, 與 pnr/bitstream.rs 的寫入端對稱——改一邊就會被 cargo test bitstream 抓住。
  4. 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_data patch 換程式。
  • 副產品的保真:Fmax 警告、真實路由、真實腳位。