5.3 位元流格式與 CRC#
對應原始碼:
eda/src/fpga/pnr/bitstream.rs(.asc⇄.bin)、eda/src/fpga/board/bitstream.rs(位元流反解) 上一節:5.2 佈局與繞線:退火與 PathFinder
5.3.1 從設計到燒錄#
整個 FPGA 流程的最後一步是把配置資料變成可以送進 FPGA 的位元流。
flowchart LR
A["已路由的設計<br/>(LC 的設定 + 佈線的 pip)"] --> B[".asc 文字格式<br/>(人類可讀)"]
B --> C[".bin 二進位格式<br/>(硬體需要)"]
C --> D["USB / JTAG<br/>燒進 FPGA"]兩種格式都有意義:
.asc是中繼格式,可以 diff、可以人工檢查、可以在沒有工具的情況下編輯;.bin是硬體格式,FPGA 的配置介面只吃這個。
rueda 的 bitstream.rs 是 IceStorm icepack 的移植,board/bitstream.rs 則是 iceunpack 的移植。
5.3.2 .asc 格式#
.comment from rueda
.device 5k
.logic_tile 3 5
<54 binary chars>
<54 binary chars>
... (16 rows)
.io_tile 0 4
<18 chars> × 16 rows
.ram_data 6 3
<64 hex digits> × 16 rows
.extra_bit <bank> <x> <y>
| 段落 | 意義 |
|---|---|
.comment | 註解(會被寫進位元流的 initblop) |
.device 5k | 裝置型號 |
.logic_tile <x> <y> | 一個邏輯 tile 的設定(54 個二進位字元 × 16 行) |
.io_tile <x> <y> | 一個 IO tile(18 個字元 × 16 行) |
.ram_data <x> <y> | BRAM 的初始內容(64 個十六進位數字 × 16 行) |
.extra_bit <bank> <x> <y> | 不在任何 tile 裡的設定位 |
二進位字串是 MSB 優先,54 個字元代表一個 tile 的 54 個設定位。
5.3.3 UP5K 的幾何#
這些常數是硬體規格,寫死在程式裡:
const CHIP_W: i32 = 24; const CHIP_H: i32 = 30;
const CRAM_W: usize = 692; const CRAM_H: usize = 336;
const BRAM_W: usize = 160; const BRAM_H: usize = 2 * 128;
const COLS: [usize; 13] = [54, 54, 54, 54, 54, 54, 42, 54, 54, 54, 54, 54, 54];
13 欄 tile:12 欄邏輯(每欄 54 個設定位),加一欄 42 的(RAM 欄)。
tile 的類型:
fn tile_type(x: i32, y: i32) -> &'static str {
if (x == 0 || x == CHIP_W + 1) && (y == 0 || y == CHIP_H + 1) { return "corner"; }
if x == 0 || x == CHIP_W + 1 {
return match y { 5|10|15|23 => "dsp0", 6|11|16|24 => "dsp1",
7|12|17|25 => "dsp2", 8|13|18|26 => "dsp3", _ => "ipcon" };
}
if y == 0 || y == CHIP_H + 1 { return "io"; }
if x == 6 || x == 19 { return if y % 2 == 1 { "ramb" } else { "ramt" }; }
"logic"
}
RAM 欄在 x = 6 與 x = 19,而且上下交替 ramb / ramt——這是硬體的結構(RAM 的資料匯流排要往上或往下延伸)。
dsp0..dsp3 在左右邊緣的特定列上——那幾列是硬體乘法器的位置。
tile_width:
// corner: 0, ramb/ramt: 42, io: 18, 其他: 54
5.3.4 CRAM 位元的排列#
配置記憶體(CRAM)的位元排列是整個格式裡最瑣碎的部分。
四個 bank,由 2 位的象限碼選擇:
// bank = top | (right << 1)
bank 內,tile 是鏡射的:右半邊的晶片由右往左排,上三分之一由下往上排。
const PERMX: [usize; 18] = [23,25,26,27,16,17,18,19,20,14,32,33,34,35,36,37,4,5];
const PERMY: [usize; 16] = [0,1,3,2,4,5,7,6,8,9,11,10,12,13,15,14];
PERMX / PERMY 是奇偶交錯的排列表——硬體的配置記憶體在行列方向上都有這種交錯(為了讓位元在走線上更容易佈局)。
上 bank 的判定:
// top = ty > CHIP_H * 2 / 3(列 21..31 是上 bank)
5.3.5 BRAM 位元的排列#
// BRAM 有 4 個 bank,160 欄(奇數 bank 是半寬 80),高度 256;
// BRAM 列索引 = 256*by + (16*(bx/16) + 15 - bx%16)
15 - bx%16 是 16 位群組內的位元反轉——又是硬體的物理排列。
5.3.6 .bin 格式的完整結構#
[0xFF, 0x00, <comment bytes, NUL-terminated>, 0x00, 0xFF] initblop(.comment 區塊)
0x7E, 0xAA, 0x99, 0x7E, 0x51, 0x00, 0x01, 0x05 前導(同步字 + 填充)
0x92, 0x00, 0x20 「不要 CRC」命令;0x20 = 暖開機啟用
0x62, 0x02, 0xB3, 0x82, 0x00, 0x00 CRAM 資料命令,寬度 = CRAM_W-1 = 691
for each bank b in 0..4:
0x72, h>>8, h, 0x11, b, 0x01, 0x01 設定 CRAM bank / 高度
<打包的 CRAM 位元,msb 優先> (bank 0/2:336 列 × 692 欄)
0x00, 0x00 CRC 類的結尾
0x72, 0x00, 0x80 BRAM 區塊大小 = 128
for each bank b in 0..4:
0x11, b 設定 BRAM bank
for off in (0..256).step_by(128):
0x82, off>>8, off, 0x62, (w-1)>>8, (w-1), 0x01, 0x03 BRAM 資料區塊
<128 列 × 寬度位元>
0x00, 0x00
0x22, crc>>8, crc, 0x01, 0x06, 0x00 最終 CRC + 完成
5.3.6.1 指令/資料 nibble 協定#
這是 Lattice 位元流的通用格式:一個位元組的高 nibble 是指令,低 nibble 是後續資料的位元組數。
指令 0x72 = 設定 CRAM 的 bank 與高度
指令 0x62 = CRAM/BRAM 資料
指令 0x82 = BRAM 資料(含偏移)
指令 0x11 = 設定 bank
指令 0x22 = CRC
「指令碼自己也被算進 CRC」 是一個容易漏掉的細節:
// CRC 涵蓋 0x22 指令碼本身
w(&mut out, &mut crc, 0x22);
let c = crc;
for b in [(c >> 8) as u8, c as u8, 0x01, 0x06, 0x00] { w(&mut out, &mut crc, b); }
5.3.6.2 CRC-16/CCITT-FALSE#
fn crc16(crc: &mut u16, byte: u8) {
for i in (0..8).rev() {
let x = if ((*crc >> 15) ^ ((byte as u16 >> i) & 1)) != 0 { 0x1021 } else { 0 };
*crc = (*crc << 1) ^ x;
}
}
多項式 0x1021,初始值 0xFFFF。CRC 在前導之後被重設為 0xffff。
預設值與 icepack 一致:
| 設定 | 值 |
|---|---|
| 頻率範圍 | low |
| 暖開機 | 啟用 |
nosleep | 否 |
5.3.7 bin_to_asc:反解#
反解器掃描位元組串找同步字 7E AA 99 7E,然後解碼指令/資料 nibble 協定:
let (cmd, n) = (b >> 4, (b & 15) as usize);
let mut value = 0usize;
for _ in 0..n { value = (value << 8) | byte(i)? as u8; i += 1; }
match cmd {
0 => match value {
1 | 3 => { /* CRAM (1) 或 BRAM (3) 資料:height 列 × width 位,然後兩個零位元組 */ }
6 => break, // 喚醒
_ => {} // CRC 重設、重啟…
},
1 => bank = value,
6 => width = value + 1,
7 => height = value,
8 => offset = value,
9 => warmboot = value & 0x20 != 0,
_ => {}
}
然後重新輸出 .comment、.device 5k、.warmboot disabled(若有需要)、每個 tile 的 16 行二進位、.extra_bit 行、.ram_data 區塊。
「反解」為什麼重要?
- 驗證:寫出與讀回必須一致;
- 燒錄工具的除錯:當 FPGA 不工作時,把位元流反解成人類可讀的形式是第一步;
- 本專案:反解是虛擬開發板的基礎(第 5.3.8 節)。
5.3.8 位元流反解成電路#
eda/src/fpga/board/bitstream.rs 把位元流還原成一個可模擬的電路。
這是本專案最漂亮的一個設計:虛擬開發板跑的不是 RTL,而是真正燒進 FPGA 的那份位元流。
5.3.8.1 用 union-find 重建連接#
// 對每一個 pip,如果目的多工器的設定位等於這個 pip 的樣式,就把 src 接到 dst
for p in &chip.pips {
if p.pat == 0 { continue; } // 全 0 = 沒有連接
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); }
}
這就是硬體配置語意的精確模型:一個 pip 被「啟用」當且僅當目的多工器的設定位符合它的樣式。
設定位全 0 表示「沒有連接」——這是硬體的預設(多工器沒有被設定成連到任何來源)。
5.3.8.2 辨識驅動者#
反解後需要知道每條網線的驅動端:
| 類型 | 說明 |
|---|---|
| 邏輯單元輸出 | lutff_z/out |
| 進位輸出 | lutff_z/cout |
| IO 輸入 | 從接腳進來 |
| 全域網路 | 從 fabout,或從接腳經 padin_glb_netwk.N |
| block RAM 輸出 | |
CarryInSet | 把進位輸入多工器接到常數 1 |
進位的模型是一個三輸入多數閘:
d.comb.push(Comb { ins: [ins[1], ins[2], cin_net, C0], table: 0b1110_1000, out: co_net });
0b1110_1000 的每一位索引是 in1 | in2<<1 | cin<<2——真值表在「至少兩個輸入為 1」時為 1。這正是全加器的進位。
沒有驅動者的網線讀 0。 未連接的時脈致能視為啟用:
for f in d.ffs.iter_mut() { if let Some(e) = f.en { if !has_driver[e as usize] { f.en = None; } } }
5.3.8.3 LC 解碼#
let lut: u16 = (0..16).fold(0, |a, k| a | ((v[LUT_PERM[k]] as u16) << k));
let (carry, ff) = (v[8], v[9]);
用第 5.1.11 節的 LUT_PERM 表把硬體的設定位還原成邏輯真值表。 這個表在寫入器與反解器之間共用——如果兩邊不一致,燒進去的東西跟模擬的東西會不一樣。
5.3.8.4 IO 模式解碼#
從 IOB_z.PINTYPE_k 的位元:
PIN_TYPE[5:2] = 0110→ 簡單輸出PIN_TYPE[1:0] = 01→ 簡單輸入
其他組合會產生警告(「只模擬簡單輸入與輸出」)。
5.3.8.5 .ram_data 的解析#
// 每行是一個 INIT_k(64 個十六進位數字,msb 優先);字 k = 位元 16k..16k+15
5.3.9 虛擬開發板#
eda/src/fpga/board/icebreaker.rs 定義了 iCEBreaker(iCE40UP5K-SG48)。
pub const CLOCK_PIN: u32 = 35;
pub const CLOCK_HZ: f64 = 12e6;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Kind {
Clock,
Button { pressed: bool }, // 按下時驅動的位準
Led { on: bool }, // 點亮時的位準
Uart { rx: bool }, // true = 進入 FPGA (RX),false = 離開 (TX)
Pmod,
}
完整的接腳表(30 個):
| 接腳 | 名稱 | 種類 | 備註 |
|---|---|---|---|
| 35 | CLK | Clock | 12 MHz 振盪器 |
| 10 | BTN_N | Button | 板上,低有效 |
| 11 | LEDR_N | Led | 板上紅燈,低有效 |
| 37 | LEDG_N | Led | 板上綠燈,低有效 |
| 6 | RX | Uart | FTDI → FPGA |
| 9 | TX | Uart | FPGA → FTDI |
| 26,27,25,23,21 | LED1..LED5 | Led | 拆下區 |
| 20,19,18 | BTN1..BTN3 | Button | 拆下區 |
| 4,2,47,45,3,48,46,44 | P1A1..P1A10 | Pmod | PMOD1A(接腳 1-4, 7-10) |
| 43,38,34,31,42,36,32,28 | P1B1..P1B10 | Pmod | PMOD1B |
方向衝突檢查:
Kind::Led {..} | Kind::Uart { rx:false } if is_in =>
b.warnings.push(format!("pin {} ({}) is an output of the board, but the design uses it as an input", ...));
這是一個實用的設計檢查:如果設計把 LED 當成輸入,模擬結果一定不對,與其默默跑出奇怪的結果,不如在載入時就警告。
5.3.9.1 PCF 檔案#
# iCEBreaker (iCE40UP5K-SG48) 的接腳,供教學設計使用。
set_io -nowarn i_clk 35 # 12 MHz 振盪器
set_io -nowarn i_rst 10 # BTN_N,板上按鈕(按下 = 0)
set_io -nowarn o_ledr 11 # LEDR_N,板上紅燈(亮 = 0)
...
PCF 解析刻意做最小:
// 剝掉 # 註解,以空白切分,接受 set_io <name> <pin>,忽略任何以 - 開頭的選項
let args: Vec<&&str> = p[1..].iter().filter(|a| !a.starts_with('-')).collect();
if args.len() >= 2 { m.insert(args[0].to_string(), args[1].to_string()); }
-nowarn 被丟掉,因為解析器不需要它。
注意:解析器保留每個名稱的第一個 set_io,但不偵測重複的接腳——重複接腳的檢查在佈局器裡(「pin {} assigned twice」)。
5.3.10 周邊模型#
eda/src/fpga/board/periph.rs 實作了三個必須每個時脈都看的周邊。
5.3.10.1 UART#
8N1,cpb = 每位元的時脈數(最小 2)。
TX(FPGA → 主機) 用「在每個位元的中點取樣」解碼:
let k = (t as f64 / self.cpb - 0.5).round();
let at_mid = ((k + 0.5) * self.cpb).round() as u64 == t;
if at_mid && k >= 1.0 && k <= 8.0 && level { self.rx_byte |= 1 << (k as u32 - 1); }
if at_mid && k >= 9.0 { if level { self.received.push(self.rx_byte as u8); } else { self.framing_errors += 1; } }
.round() 而不是 .floor() 是一個刻意的修正,原始碼註解說明:
在小數位元時間
t/cpb - 0.5可能刚好差一點不到 k
這個 off-by-half 的問題是真實存在的:用 floor 會在某些 cpb 下取樣到錯誤的位置。
RX(主機 → FPGA) 則是產生的:起始位(0)、8 個資料位(LSB 優先)、停止位(1),然後閒置。
5.3.10.2 七段顯示器#
pub struct Seg {
pub segs: [Option<u32>; 8], // a..g, dp 的輸出網路(None = 未連接)
pub sels: Vec<(Option<u32>, bool)>, // 數位選擇:(網路, 選擇該數位的位準)
pub on: bool, // 點亮某段的位準
pub lit: Vec<[u64; 8]>, // 每個數位、每段的點亮週期數
}
沒有數位選擇時那一位永遠亮(sels 空 → [(None, true)])。
5.3.10.3 VGA#
pub struct Vga {
pub hs: Option<u32>, pub vs: Option<u32>,
pub r: Vec<u32>, pub g: Vec<u32>, pub b: Vec<u32>, // MSB 優先
pub active_low: bool, // 同步脈衝是低有效(640×480 就是這樣)
line: Vec<u16>, lines: Vec<Vec<u16>>,
pub frame: Option<(usize, usize, Vec<u16>)>,
pub frames: u64,
}
const MAX_W: usize = 2048; const MAX_H: usize = 1024;
一個時脈一個像素。 一行從 HSYNC 的有效邊緣開始,一個畫面從 VSYNC 開始。
顏色從實際接上的接腳數量縮放:
fn chan(pins: &[u32], v: &[u8]) -> u16 {
if pins.is_empty() { return 0; }
let n = pins.len() as u32;
let x = pins.iter().fold(0u32, |a, p| (a << 1) | v[*p as usize] as u32);
((x * 15 + ((1 << n) - 1) / 2) / ((1 << n) - 1)) as u16
}
這個縮放是硬體的必然:如果設計只接了 4 個紅色位,模擬器必須把 4 位映射到 16 級亮度。
像素是 12 位:R<<8 | G<<4 | B。
RLE 編碼((顏色, 次數) 對,小端 u16)讓畫面能有效地透過 JSON 傳輸。
5.3.11 從位元流到虛擬開發板的完整鏈#
flowchart LR
A["Veryl 設計"] --> B["reda synth"]
B --> C["reda fpga --pnr veryl"]
C --> D[".asc"]
D --> E[".bin"]
E --> F["反解成電路<br/>(union-find + LC 解碼)"]
F --> G["閘級模擬<br/>(netsim)"]
G --> H["虛擬開發板<br/>LED / 按鈕 / UART / VGA"]注意這條鏈裡沒有 RTL。 從 .bin 開始,電路完全是從配置位重建的。
這為什麼重要?
因為它驗證了整條工具鏈的每一個環節:
- 如果位元流寫入器有 bug(設定位排列錯了),虛擬開發板會顯示錯誤的行為;
- 如果 LC 解碼器有 bug,也一樣;
- 如果 LUT 覆蓋有 bug,燒進去的邏輯就不對。
而且它是自動的——test.sh 跑完整條鏈並比較結果。
這是「端到端驗證」的典範:不驗證每個元件,而是驗證「從設計到可觀察行為」的完整路徑。任何一環的 bug 都會在最後顯現。
5.3.12 設計規則檢查在 FPGA 流程中的角色#
FPGA 沒有 DRC——硬體的配置工具保證了你的設計可以被實現。
但有些「規則」是隱含的:
| 規則 | 檢查位置 |
|---|---|
| 接腳不能重複分配 | 佈局器(「pin {} assigned twice」) |
| 方向必須匹配 | 虛擬開發板載入時的警告 |
| 控制集必須相容 | 佈局器的交換合法性檢查 |
| 進位鏈必須連續且從 tile 邊界開始 | 打包階段 |
| 邏輯單元數不能超過 | 佈局時無法放置就會失敗 |
這些檢查散布在流程的各個環節,因為每一個環節只知道自己能知道的資訊。
下一章:第 6 章 ASIC