1.3 rueda 的架構:一個指令串起所有工具#
對應原始碼:
AGENTS.md、reda/src/main.rs、Cargo.toml、eda/src/netlist.rs上一節:1.2 硬體描述語言的演進
1.3.1 一個 Cargo workspace 的地圖#
rueda 不是「很多工具的集合」,而是一個 Cargo workspace。所有 crate 都在同一個 Cargo.lock 裡、同一次 cargo build 裡編譯。
rueda/
├── reda/ CLI:每個工具一個子指令
├── eda/ 與語言無關的後端
│ ├── src/ 網表、block RAM、輸出格式、閘級模擬、時序約束、功耗
│ ├── src/fpga/ iCE40 / ECP5 映射、自己的佈局佈線與位元流、虛擬 iCEBreaker
│ ├── src/asic/ 標準單元流程:佈局、時脈樹、繞線、時序、DRC/LVS、GDS
│ ├── src/pdk/ Liberty / LEF / DEF 解析、SKY130 綁定
│ ├── bits/ 與前端共用的位元向量
│ └── tools/ SKY130 下載、KLayout 的 GDS / LVS / 天線檢查
├── veryl/ Veryl 語言
│ ├── sim/ 事件驅動模擬器
│ ├── synth/ Veryl → 網表、閘級模擬
│ ├── emu/ Veryl → WebAssembly(週期式)
│ ├── examples/ 設計範例、RISC-V SoC、RV64 開發板
│ └── tutorial/ 教學與虛擬實驗課
├── fluxa/ Fluxa 語言(高階合成:任務與通道)
│ ├── sim/ 模擬器(多執行緒或協作式排程)
│ ├── synth/ Fluxa → 網表
│ ├── emu/ fluxa-emu
│ └── docs/ 語言規格與版本紀錄
├── riscv/ RISC-V 軟體
│ ├── rvtools/ C 編譯器、組譯器、RV32 指令集模擬器
│ └── rvemu/ RV64 開發板模擬機(含 WebAssembly JIT)
└── web/ 網頁 IDE(TypeScript);wasm/:它的 WebAssembly 核心
1.3.2 唯一的一條架構規則#
AGENTS.md 的第一段就寫著這條規則:
The gate netlist
eda::netlist::Netlist(yosys internal cells:$_AND_,$_DFF_P_, …) is the boundary between the languages and the back end.eda/must not depend on any language front end (noveryl-parser, noveryl_*, nofluxa); front ends depend oneda, never the reverse.
這條規則的實際含義:
flowchart LR
subgraph FE["語言前端(可以互相不知道)"]
V["Veryl 前端<br/>veryl/synth, veryl/emu"]
F["Fluxa 前端<br/>fluxa/synth, fluxa/emu"]
end
subgraph CORE["語言無關的核心"]
NL["eda::netlist::Netlist<br/>yosys 內部單元"]
SIM["eda::netsim 閘級模擬"]
BRAM["eda::bram 記憶體推斷"]
OUT["eda::output 網表輸出"]
end
subgraph BE["後端(不知道語言)"]
FPG["FPGA:ice40 / ecp5<br/>pnr 佈局佈線、位元流"]
ASIC["ASIC:map / place / cts<br/>route / sta / drc / lvs / gds"]
end
V --> NL
F --> NL
NL --> SIM
NL --> BRAM
NL --> OUT
NL --> FPG
NL --> ASIC為什麼這條規則值得這麼強調?
因為「後端不小心用到語言的前端資訊」是最常見的 EDA 架構腐化。一個具體的例子:記憶體推斷。
如果 eda 知道語言是 Veryl,它就可以直接看 Veryl 的 AST 找出陣列。但這樣一來:
- 每個新的語言前端都要重新實作一次記憶體推斷;
- 語言的語意變化(例如
@reg的語意)會直接影響後端,後端就無法獨立驗證。
rueda 的解法是:語言前端在展開時把記憶體「記錄」成具名網路線,用一套約定的命名把介面傳遞給後端:
$mem:NAME 儲存位元
$mem:NAME:clk 寫入時脈
$mem:NAME:we 寫入使能
$mem:NAME:wa 寫入位址
$mem:NAME:wd 寫入資料
$mem:NAME:ra0 讀取位址(port 0)
$mem:NAME:rd0 讀取資料(port 0)
$mem:NAME:init 初始內容
後端的 eda::bram::find_mems 只看網表的結構(找 flip-flop、找位址解碼),去「證明」這看起來像記憶體的東西。證明失敗就退回 flip-flop 實作。
這個設計還帶來一個額外好處:你可以手寫網表餵給後端。任何能產生 yosys JSON 的工具(商用的、合開的)都可以和 rueda 的後端對接。
1.3.3 網表 IR:位元層級的最小公分母#
eda/src/netlist.rs 的核心型別:
/// 一個位元:編譯期常數,或一條網線(net id >= 2,如 yosys JSON)
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub enum Bit {
C0,
C1,
W(u32),
}
#[derive(Clone, Debug)]
pub struct Cell {
pub typ: String,
/// (連接埠名稱, 位元);輸出埠是 "Y"(閘)、"O"(LUT)或 "Q"(正反器)
pub conns: Vec<(&'static str, Bit)>,
/// 單元參數(如 LUT_INIT,msb 優先)
pub params: Vec<(&'static str, String)>,
}
#[derive(Clone)]
pub struct Port { pub name: String, pub dir: Dir, pub bits: Vec<Bit> }
#[derive(Clone)]
pub struct Netlist {
pub name: String,
pub ports: Vec<Port>,
/// 具名網線(階層式名稱)
pub nets: Vec<(String, Vec<Bit>)>,
/// 組合單元依拓樸順序,接著是正反器
pub cells: Vec<Cell>,
}
四個設計決定,每個都有理由:
(a) 位元層級,不是「匯流排層級」。 向量是 Vec<Bit>。這聽起來很低效,但換來的是:整個工具鏈只有一種資料結構。加法器是 32 顆全加器、一個 32 位元匯流排還是 32 個 1 位元閘,在 IR 層沒有差別,差別只出現在「產生器」裡。
(b) 單元名稱用 yosys 內部命名($_AND_、$_DFF_P_)。這意味著 output.rs 寫出的 JSON 可以直接餵給 yosys 與 nextpnr。rueda 不需要自己的網表格式——它複用整個開源生態的格式。
(c) 匯流排埠用「索引化的純量接腳」表示:conns 裡出現 "RADDR[0]"、"RADDR[1]",而不是一個陣列。輸出時再依 [i] 後綴重新群組:
// 接腳 "NAME[i]" 是埠 NAME 的第 i 位
let mut groups: Vec<(&str, Vec<(usize, &Bit)>)> = vec![];
for (p, b) in &c.conns {
let (base, i) = match p.split_once('[') {
Some((n, r)) => (n, r.trim_end_matches(']').parse().unwrap_or(0)),
None => (*p, 0),
};
// ...
}
這是刻意的:單元的接腳數量在編譯期就固定(&'static str),所以不可能存一個 Vec<Vec<Bit>> 而還要支援「動態接腳數」。扁平表示配合事後群組,是最省事又最有效率的方案。
(d) cells 保證「組合邏輯在前、正反器在後」的拓樸順序。 這個不變式被多處依賴:
- 閘級模擬器(
netsim::settle)走一遍 cells 就能得到正確的組合邏輯值,不需要反覆迭代; - 時序分析(
asic::sta)用「一次前向傳播算到達時間、一次後向傳播算緊要度」,完全不需要拓樸排序; rebuild的 DFS 遇到重複節點就報「組合迴路」。
網線編號從 2 開始:
Bit::W(u32)的u32 >= 2。因為 yosys JSON 裡 net 0 與 net 1 保留給常數"0"與"1"。這個約定讓 JSON 輸出器可以直接把Bit::W(w)印成數字、C0/C1印成字串。
1.3.4 輸出埠的單一判準#
整個工具鏈需要知道「單元的哪個接腳是驅動端」。rueda 用一個函式回答:
/// 輸出接腳(通用、iCE40、vlib 單元;SKY130 的 X / LO / HI;ECP5 的 Z, S0, S1, COUT, DOA*, DOB*)
pub fn is_out(p: &str) -> bool {
matches!(p, "Y" | "Q" | "O" | "CO" | "X" | "LO" | "HI" | "Z" | "S0" | "S1" | "COUT")
|| p.starts_with("RDATA") || p.starts_with("DOA") || p.starts_with("DOB")
}
這個函式被用在:
- JSON 輸出的方向推斷(
port_dir); - Verilog 輸出的
wire/reg宣告區分; - 閘級模擬器決定「誰驅動這個位元」;
- 死邏輯掃描時決定哪些接腳要沿著反向追蹤。
沒有 if-else 鏈。這是一個小例子,但說明了這個專案的風格:把技術相關的差異壓縮到最少的地點,讓其他所有邏輯保持技術無關。
1.3.5 reda:一個指令,像 git 一樣#
$ reda sim counter.veryl # 跑測試平台;語言由副檔名決定
$ reda synth -o adder.json adder.veryl # 合成成閘級網表
$ reda fpga --pnr veryl --pcf pins.pcf --bin top.bin top.veryl
$ reda asic --pdk sky130 --gds alu.gds alu.veryl
$ reda gatesim ice40 fifo.veryl # 在合成後的網表上跑測試平台
$ reda emu --top Rv64Soc -o soc.wasm *.veryl
$ reda emu fir.fx # Fluxa 的測試
$ reda cc --run hello.c # 給 RISC-V SoC 的 C 程式
$ reda rvemu fw_payload.bin # RV64 開發板模擬機
$ reda help [子指令]
設計上值得說明的幾點:
(a) 語言由副檔名決定。 .veryl 走 Veryl 前端,.fx 走 Fluxa 前端。reda/src/main.rs 依副檔名分派到各 crate 的 cli::main。
(b) 每個工具 crate 暴露 cli::main(args)(不含程式名)。 沒有第二個二進位檔。新增一個工具 = 一個 crate 裡的 cli 模組 + reda/src/main.rs 裡的 COMMANDS 表與 match 分支 + reda/tests/cli.rs 裡的一個案例。
(c) 前端與後端的分派是「先判語言,再判工具」。
flowchart TD
A["reda <子指令> 檔案"] --> B{"副檔名?"}
B -->|".veryl"| C["veryl_*::cli::main"]
B -->|".fx"| D["fluxa_*::cli::main"]
B -->|".c"| E["rvtools::cli::cc"]
C --> F{"子指令"}
D --> F
F -->|sim| G["事件驅動 / KPN 模擬器"]
F -->|synth| H["產生網表"]
F -->|fpga| I["iCE40 / ECP5"]
F -->|asic| J["標準單元流程"]
F -->|gatesim| K["閘級模擬"]
F -->|emu| L["編譯成 WebAssembly"]1.3.6 支援矩陣#
| 語言 | 模擬 | 合成 / FPGA / ASIC | 閘級模擬 | 網頁 IDE |
|---|---|---|---|---|
Veryl(.veryl) | reda sim | reda synth / fpga / asic / gatesim | 有 | 有 |
Fluxa(.fx) | reda sim | reda synth / fpga / asic / gatesim | 有 | 有 |
兩種語言寫的硬體都能開 Linux:Veryl 的 Rv64Soc 與用 Fluxa 寫的同一塊板子(fluxa/rv64/linux/soc.fx)開的是同一個 OpenSBI + Linux + busybox 映像。
這是這個架構最好的驗證:兩個完全獨立的前端,最後產生的系統行為一致。如果網表 IR 真的做到了技術無關,這件事就必然成立。
1.3.7 建置與測試#
./build.sh # target/release/reda 與 web/dist(--no-web:只建 reda)
./test.sh # 整個 workspace 的 cargo test、教學流程、模擬機、Fluxa 處理器、npm test
./test.sh --full # 再加上 yosys 形式等價、nextpnr 流程(需要 OSS CAD Suite)
./run.sh serve # 用本機伺服器開 web/dist 的 IDE
./test.sh --full 特別值得一提:它跑的是用官方 veryl build 產生的 SystemVerilog 與 rueda 產生的網表之間的形式等價性檢查(yosys + slang)。這是「不信任自己的前端」的驗證策略,細節見第 4 章。
1.3.8 這個架構帶來的三個教訓#
教訓一:中介表示的選擇決定了一切。 網表 IR 選成「位元層級 + yosys 內部單元命名 + 扁平接腳」,就同時換來了:與開源生態的相容性、跨語言共用後端、以及技術無關的所有保證。若網表 IR 選成「Verilog AST」,以上全部會失去。
教訓二:架構規則必須能被工具強制檢查。 「eda/ 不得依賴語言前端」不是靠紀律維持的——Cargo 的 crate 依賴圖本身就會強制它。eda/Cargo.toml 裡沒有 veryl-parser,就沒有辦法(即使有人想)讓 eda 用到它。
教訓三:把所有工具塞進一個命令,降低了整體成本。 使用者要記住的不是「模擬器叫什麼、布線器叫什麼」,而是「reda 底下有什麼子指令」。對於教學用途,這個差別是決定性的。
下一章:第 2 章 Veryl — 從語言到網表的第一步。