跳轉到

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 的位元流。

兩種格式都有意義:

  • .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 區塊。

「反解」為什麼重要?

  1. 驗證:寫出與讀回必須一致;
  2. 燒錄工具的除錯:當 FPGA 不工作時,把位元流反解成人類可讀的形式是第一步;
  3. 本專案:反解是虛擬開發板的基礎(第 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 從位元流到虛擬開發板的完整鏈#

注意這條鏈裡沒有 RTL。 從 .bin 開始,電路完全是從配置位重建的。

這為什麼重要?

因為它驗證了整條工具鏈的每一個環節:

  • 如果位元流寫入器有 bug(設定位排列錯了),虛擬開發板會顯示錯誤的行為;
  • 如果 LC 解碼器有 bug,也一樣;
  • 如果 LUT 覆蓋有 bug,燒進去的邏輯就不對。

而且它是自動的——test.sh 跑完整條鏈並比較結果。

這是「端到端驗證」的典範:不驗證每個元件,而是驗證「從設計到可觀察行為」的完整路徑。任何一環的 bug 都會在最後顯現。

5.3.12 設計規則檢查在 FPGA 流程中的角色#

FPGA 沒有 DRC——硬體的配置工具保證了你的設計可以被實現。

但有些「規則」是隱含的:

規則 檢查位置
接腳不能重複分配 佈局器(「pin {} assigned twice」)
方向必須匹配 虛擬開發板載入時的警告
控制集必須相容 佈局器的交換合法性檢查
進位鏈必須連續且從 tile 邊界開始 打包階段
邏輯單元數不能超過 佈局時無法放置就會失敗

這些檢查散布在流程的各個環節,因為每一個環節只知道自己能知道的資訊。


下一章:第 6 章 ASIC