3.5 記憶體推斷:$mem 命名網協定與 SB_RAM40_4K#
陣列是綜合裡最尴尬的結構:預設把它精細化成幾百顆 flip-flop + 解碼器, 小則浪費、大則放不進晶片。veryl-eda 的做法分三層:elab 期記錄使用 模式 → 以
$mem:…命名網標記記憶體埠 → 映射期(bram.rs)把符合 條件者換成 block RAM。全程 flip-flop 網表始終有效,block RAM 只是 「換掉它」的可選變換。
3.5.1 什麼樣的陣列算是記憶體?#
判據在 elab::MemInfo 的註解裡(elab.rs:750-763):
/// How an array is used, recorded while it is elaborated as flip-flops: if
/// it is written only as whole elements from one always_ff (at most one
/// write per cycle) and read only as whole elements, it is a memory with one
/// write port and read ports.
#[derive(Default)]
struct MemInfo {
bad: bool,
/// (block, clock, rising edge, we, waddr, wdata)
write: Option<(usize, Bit, bool, Bit, Vec<Bit>, Vec<Bit>)>,
/// (address, data placeholder wires)
reads: Vec<(Vec<Bit>, Vec<Bit>)>,
}
| 使用形態 | 判定 |
|---|---|
always_ff { ram[addr] = data; }(整元素、單一區塊、每拍至多一次) | ✔ 寫埠 |
x = ram[addr];(整元素讀) | ✔ 讀埠 |
位切片寫 ram[a][2:0] = v、非索引整體讀、同拍多次寫 | ✘ bad = true |
write()(elab.rs:1249-1266)在賦值瞬間記錄寫埠:
let ok = matches!(sel, Sel::Index(_)) && val.len() == ew as usize && self.ff.is_some()
&& env.get(&we).map(|v| v[0] == Bit::C0).unwrap_or(false) && !env.contains_key("$done");
if ok {
env.insert(we, vec![Bit::C1]);
env.insert(mem_key(name, "wa"), pad(&idx, wa));
env.insert(mem_key(name, "wd"), val.to_vec());
} else {
self.mems.entry(key).or_default().bad = true;
}
四個條件:必須是元素選擇(非切片)、寫入寬度恰為元素寬、在 always_ff 內、且本拍尚未寫過(每拍至多一個寫埠)。讀埠在 eval() 的 Expr::Index 分支記錄(elab.rs:1512-1525):
if let Some((_, r)) = self.mems.get(&key).and_then(|m| m.reads.iter().find(|(a, _)| *a == idx)) {
return Ok(r.clone());
}
let r = self.b.wires(v.len() as u32);
for (p, d) in r.iter().zip(&v) { if let Bit::W(w) = p { self.alias.insert(*w, *d); } }
self.mems.entry(key).or_default().reads.push((idx, r.clone()));
return Ok(r);
同一個位址運算式第二次出現(ram[addr] 兩處讀)會共用同一批 placeholder, alias 把它們指向同一棵 mux 樹——所以讀埠數按「位址運算式的個數」計, 而不是按程式中出現次數計。
3.5.2 $mem:NAME:{clk,we,wa,wd,raN,rdN} 命名網協定#
synthesize_stages_with 尾端把合格的 MemInfo 攤平成命名網(elab.rs:1721-1735):
for (q, m) in mems {
let (_, ck, _, we, wa, wd) = m.write.as_ref().unwrap();
mem_nets.push((format!("$mem:{}:clk", q), vec![*ck]));
mem_nets.push((format!("$mem:{}:we", q), vec![*we]));
mem_nets.push((format!("$mem:{}:wa", q), wa.clone()));
mem_nets.push((format!("$mem:{}:wd", q), wd.clone()));
for (i, (ra, rd)) in m.reads.iter().enumerate() {
mem_nets.push((format!("$mem:{}:ra{}", q, i), ra.clone()));
mem_nets.push((format!("$mem:{}:rd{}", q, i), rd.clone()));
}
}
協定一覽(NAME 為層級化訊號名,例如 soc.mem):
| 網名 | 內容 |
|---|---|
$mem:NAME:clk | 寫時鐘(1 位) |
$mem:NAME:we | 寫使能 |
$mem:NAME:wa / :wd | 寫位址 / 寫資料 |
$mem:NAME:raN / :rdN | 第 N 個讀埠的位址 / 資料 |
關鍵設計:此刻陣列本身仍被精細化成 flip-flop(名為 NAME 的網,共 depth×width 位),讀資料 rdN 是 alias 到位址解碼 mux 樹的 placeholder。 $mem: 網只是標籤,宣告「這些東西可以被換掉」。所以:
- 不用 block RAM 的流程(ASIC、gatesim、gatesim 前的最佳化)完全不受影響;
rebuild特別保護這些網:記憶體寫埠邏輯即使尚未被讀也保留(因為映射器 可能把它變成 RAM),netlist.rs:536-547 的roots擴充就是幹這件事;- stuck-at-0 掃除(§3.6)明文豁免從未被寫過的記憶體——它可以被
set_mem_init灌入程式,是 ROM 而不是常數 0。
3.5.3 bram.rs:從網表裡「認出」記憶體#
bram::find_mems(bram.rs:49)反向操作:掃描網表,找 $mem:…:clk 標籤, 然後驗證底下的 flip-flop 結構長得像一塊 RAM:
let plain_ff = |i: usize| nl.cells[i].typ == "$_DFF_P_" && nl.cells[i].port("C") == Some(clk);
// the storage: rising-edge flip-flops on the write clock ...
for (k, b) in store.iter().enumerate() {
match q_cell.get(b) { Some(&i) if plain_ff(i) => elems.push(i),
Some(_) => continue 'mem, // 結構不符:放棄
None => missing[k % width] = true }
}
儲存體必須是寫時鐘上的 $_DFF_P_(資料位元沒有讀者時其 flip-flop 已被 sweep 清掉,這種「缺失」是允許的,missing 陣列記錄之)。接著每個讀埠的 rd 位元必須直接驅動:
- 一顆 plain DFF 的 D(同步讀暫存器),或
- 一顆
$_MUX_再驅動 DFF 的 D,且 mux 型態為D = en ? rd : Q(讀使能),
} else if p == "B" && c.typ == "$_MUX_" && !port_bits.contains(&c.out()) {
// enable: D = en ? rd : Q
let uu = users.get(&c.out())...;
if uu.len() != 1 || uu[0].1 != "D" || !plain_ff(uu[0].0)
|| c.port("A") != Some(nl.cells[uu[0].0].out()) { continue 'mem; }
gone.push(i);
(uu[0].0, c.port("S").unwrap())
}
這正是 §3.4.5 的 clock-enable pattern!同步讀 = 「always_ff { rd <= ram[addr]; }」 經過 §3.4 的 DFF 推斷自然長成的形狀。非同步讀(組合塊裡直接 x = ram[addr])的 rd 位元會驅動一般的組合邏輯,不滿足模式 ⇒ 留在 flip-flop 實作——因為 iCE40 block RAM 只有暫存讀埠。
其他檢查:深度必須是 2 的冪(depth != 1 << abits 則放棄)、位址高出 abits 的位必須常數 0、超過 2048 深度的記憶體在 infer 裡被剔除重來 (保持 flip-flop)。通過全部檢查後,元素 DFF、讀暫存器、enable mux 一律 標記 dead,sweep 清走。
3.5.4 拼成 SB_RAM40_4K#
bram::infer(bram.rs:136)把每個讀埠複製一份完整記憶體(iCE40 block RAM 是單讀埠的),寬度方向並聯多顆:
let mode = match m.depth { 0..=256 => 0, 257..=512 => 1, 513..=1024 => 2, _ => 3 };
let bw = 16 >> mode; // 16/8/4/2 bits per word
let blocks = m.width.div_ceil(bw);
for r in &m.reads {
for k in 0..blocks {
let mut conns = vec![("RCLK", m.clk), ("RCLKE", r.en), ("RE", Bit::C1),
("WCLK", m.clk), ("WCLKE", Bit::C1), ("WE", m.we)];
for i in 0..11 { conns.push((RADDR[i], a(&r.addr, i))); conns.push((WADDR[i], a(&m.waddr, i))); }
...
let mut params = vec![("READ_MODE", format!("{:032b}", mode)),
("WRITE_MODE", format!("{:032b}", mode))];
for name in INIT { params.push((name, "0".repeat(256))); }
params.push((SLOT, format!("{}:{}", m.name, k)));
rams.push(Cell { typ: "SB_RAM40_4K".into(), conns, params });
}
}
- 深度決定
READ_MODE/WRITE_MODE(256×16 … 2048×2),data_pin(mode, d)按 yosyscells_sim.v的規則把資料位元接到正確的 WDATA/RDATA 腳。 RCLKE = r.en:讀使能被放進 clock-enable 腳。
3.5.5 MEM_SLOT 與程式燒錄#
映射之後,網表裡只剩 SB_RAM40_4K cell 與它的 MEM_SLOT = "NAME:k" 參數 (bram.rs:185):
pub const SLOT: &str = "MEM_SLOT";
它不是 iCE40 的硬體參數——yosys JSON 會把它當屬性寫出。有了它, 後端可以在任意階段把初始內容灌進 RAM:
/// Give memory `name` initial contents in a mapped iCE40 netlist
pub fn set_mem_init(nl: &mut Netlist, name: &str, words: &[u64]) -> usize {
for c in nl.cells.iter_mut().filter(|c| c.typ == "SB_RAM40_4K") {
let Some((m, k, mode)) = slot_of(c) else { continue };
if m != name { continue; }
let init = init_strings(mode, k, words);
for (p, v) in c.params.iter_mut() {
if let Some(i) = INIT.iter().position(|x| x == p) { *v = init[i].clone(); }
}
}
}
init_strings 處理 iCE40 那套著名的位址重排(word a 的 bit d 對應 RAM bit (d << mode) | lane,lane = a >> 8)。更高層的 program_lanes(bram.rs:227)把 RISC-V 的 4 條 byte lane (rvsoc.b0.mem…b3.mem)從一段 .bin 一次填齊——這就是 RISC-V SoC 流程「C 編譯 → bitstream 上電即跑」的最後一環:
flowchart LR
C[prog.c] --> BIN[.bin]
BIN --> PL["program_lanes<br/>4 × byte lane"]
PL --> SMI["set_mem_init<br/>INIT_0..INIT_F"]
SMI --> JSON[yosys JSON]
JSON --> PNR[own P&R / nextpnr]
PNR --> ASC[.asc]
ASC -.->|pnr::patch_ram| ASC2[patched .asc]pnr::patch_ram 延伸到已佈局的 bitstream:連 place & route 都做完後 仍能改 RAM 初值(佈局不變,只改 INIT 位元)。
3.5.6 數值例子:一個 64×8 的讀寫記憶體#
module TinyMem {
input clk: clock;
input we: logic<1>;
input waddr: logic<6>;
input wdata: logic<8>;
input raddr: logic<6>;
output rdata: logic<8>;
var mem: logic<8>[64];
always_ff {
if we { mem[waddr] = wdata; }
}
always_ff {
rdata = mem[raddr]; // 同步讀:rdata 是暫存器
}
}
三個階段的樣貌:
- elab 後:
mem是 512 位 flip-flop(64 顆$_DFF_P_× 8); 寫入側 64 組decode(waddr, k)+ mux;rdata的 D 來自 64:1 的select()mux 樹(Builder::select,netlist.rs:277,log 深度逐級 二分)。網表標上$mem:mem:{clk,we,wa,wd,ra0,rd0}。 bram::find_mems:we恆真?否——we是輸入,OK;儲存體 512 顆 plain DFF ✔;rd0的 8 個位元各自驅動一顆 plain DFF 的 D(rdata暫存器)✔ ⇒ Mem { depth: 64, width: 8, 1 read port }。bram::infer:depth 64 ≤ 256 ⇒ mode 0(256×16),bw = 16,blocks = 1。單顆SB_RAM40_4K,WDATA[0..8] 接wd、 RDATA[0..8] 接rdata的 D、MASK 全 0、RCLKE 接C1。 netlist 從 ~600 閘塌縮到 1 個 RAM block + 少量粘合邏輯。
3.5.7 netsim / gatesim 如何看待兩種形態#
同一份 flip-flop 網表可以被兩種方式模擬(netsim.rs):
- generic:直接跑 flip-flop 網表——記憶體就是 64×8 顆 DFF。
- iCE40:跑
SB_RAM40_4Kcell 的模型(依 yosyscells_sim.v的 READ_MODE/WRITE_MODE 行為)。
test.sh 用 icebox_vlog / co-simulation 把「RAM 版 bitstream」與「flip-flop 版網表」互相對照——這正是 §3.5.4 所說「flip-flop 是 ground truth」的驗證 閉環:兩種形態模擬一致,才證明 find_mems 的結構辨識沒有看走眼。
為什麼這樣設計#
- flip-flop 是 ground truth,block RAM 是變換:所有流程共用一個網表; 記憶體轉換可以嘗試、失敗、退回,不需要分支的 IR。
- 驗證而非信任:
find_mems不相信 elab 的標籤,逐一檢查 flip-flop 結構——這使 gatesim / netsim 用同一份 flip-flop 網表模擬時與 RAM 版本 行為一致(test.sh的 co-simulation oracle 靠這點)。 $done/ 每拍一寫的限制在 elab 期就淘汰不合法的使用(bad = true), 錯誤可以指出確切陣列名,而不是讓映射器莫名其妙失敗。
本章重點#
- 記憶體資格:單一
always_ff、整元素寫、每拍至多一寫、整元素讀;讀埠 按位址運算式去重。 $mem:NAME:{clk,we,wa,wd,raN,rdN}是標籤網,flip-flop 精細化始終 存在;不轉 RAM 的流程完全不受影響。bram::find_mems依結構驗證(plain DFF 儲存體 +D = en ? rd : Q的 同步讀)——與 §3.4 的 CE pattern 是同一件事的兩面。- 非同步讀、深度非 2 冪、深度 > 2048 都留在 flip-flop。
MEM_SLOT屬性讓set_mem_init/program_lanes/pnr::patch_ram能在映射、甚至佈局之後灌入程式內容。