Skip to main content

logicaffeine_compile/codegen_sva/
wpe_offchip.rs

1//! The whole-program engine with its bulk memories off the die: the same loader and interpret FSM, whose
2//! heap, frame stack and config image — any of them — live in a QSPI PSRAM behind one
3//! [`psram_edge`](super::psram::emit_psram_edge_verilog), each through its own
4//! [`psram_cache`](super::psram::emit_psram_cache_verilog), the caches sharing the edge through a
5//! [`psram_arbiter`](super::psram::emit_psram_arbiter_verilog).
6//!
7//! Every cache keeps the port of the block RAM it replaces — a registered read one cycle after its address,
8//! refreshed every cycle — and while any of them serves a miss it raises `mstall`, which freezes the whole
9//! loader/interpret FSM. A program's Show records, its result and its counted run cycles are therefore the
10//! on-chip engine's exactly; a miss costs only wall-clock time. The one input that does not wait is the
11//! UART: the receiver holds one byte while the FSM is stalled, so no byte is lost as long as every stall
12//! the loader can meet is shorter than a byte ([`OffchipMemory::check_uart`]).
13//!
14//! The memories an engine sends off-chip are named by the floorplan's own [`ArrayRole`]s, so the plan that
15//! decides what leaves the die ([`plan_wpe`](super::wpe_floorplan::plan_wpe)) and the RTL that takes it
16//! away speak one vocabulary.
17
18use super::psram::{
19    emit_psram_arbiter_verilog, emit_psram_cache_hit_function, emit_psram_cache_hit_wires,
20    emit_psram_cache_verilog, emit_psram_edge_verilog, emit_psram_model_verilog,
21    psram_cache_hit_ports, PsramCache, PsramEdge, EDGE_WORD_ADDR_BITS,
22};
23use super::wpe_floorplan::ArrayRole;
24use super::wprog_engine_rtl::{
25    emit_wprog_engine_soc_offchip_body, emit_wprog_engine_soc_testbench_board, MemBackend, WImage,
26};
27use std::fmt::Write as _;
28
29/// The engine memories behind one PSRAM edge, each with its own cache; `None` keeps a memory on the die.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct OffchipMemory {
32    pub edge: PsramEdge,
33    pub heap: Option<PsramCache>,
34    pub frames: Option<PsramCache>,
35    /// The config image, as three lanes of one word per cell — {code, kind}, {op1, op0}, {op3, op2} — each
36    /// behind a cache of this geometry.
37    pub cells: Option<PsramCache>,
38}
39
40/// Why an engine cannot have these memories off-chip.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum OffchipUnplaceable {
43    /// Nothing is off-chip; the on-chip emitters build that engine.
44    NothingOffChip,
45    /// The walk reads the config image through a registered port only under [`MemBackend::ConfigBram`];
46    /// the other backends read it combinationally, which no cache can serve.
47    CellsNeedConfigBram { backend: MemBackend },
48    /// The regions the memories need do not fit the part.
49    RegionsLargerThanPart { words: u64, part_words: u64 },
50    /// A stall the loader can meet outlasts one UART byte, so a byte could complete while the one before it
51    /// still waits.
52    StallOutlastsByte { stall_cycles: u64, byte_cycles: u64 },
53    /// The edge could still be initializing the part when the loader first touches it.
54    InitOutlastsLoad { init_cycles: u64, load_cycles: u64 },
55}
56
57/// One memory's place in the part: a naturally aligned power-of-two block of words at `base`, so its
58/// address is the region's high bits over the low `bits` bits of its own index.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct Region {
61    pub name: &'static str,
62    pub base: u32,
63    pub bits: u32,
64}
65
66/// The fewest bytes the loader takes before it can touch the part. A program whose every config memory
67/// and parameter list is empty streams exactly these, ending in the go byte that starts the heap clear;
68/// every other stream writes its first cell, its first preloaded word or its heap later still.
69pub fn wire_bytes_before_first_part_access() -> u64 {
70    WImage::default().wire_bytes(&[]).len() as u64
71}
72
73/// System cycles of one UART byte at `clks_per_bit`: a start bit, eight data bits and a stop bit.
74pub fn uart_byte_cycles(clks_per_bit: u64) -> u64 {
75    10 * clks_per_bit
76}
77
78fn ceil_log2(n: usize) -> u32 {
79    n.max(1).next_power_of_two().trailing_zeros()
80}
81
82impl OffchipMemory {
83    /// The floorplan roles this engine takes off the die.
84    pub fn roles(&self) -> Vec<ArrayRole> {
85        [
86            (self.heap, ArrayRole::Heap),
87            (self.frames, ArrayRole::FrameStack),
88            (self.cells, ArrayRole::ConfigImage),
89        ]
90        .into_iter()
91        .filter_map(|(c, r)| c.map(|_| r))
92        .collect()
93    }
94
95    /// The part's words within the edge's word address.
96    pub fn part_words(&self) -> u64 {
97        (self.edge.part.capacity_bytes as u64 / 8).min(1 << EDGE_WORD_ADDR_BITS)
98    }
99
100    /// Each off-chip memory's region for the cap profile `caps`, largest first so every region is aligned
101    /// to its own size: the three cell lanes (`MAXCELL` words each), the frame stack (`DEPTH x NREG`) and
102    /// the heap (`HEAP`), each rounded up to a power of two. An index past its memory's cap wraps inside
103    /// its own region and never reaches another memory's.
104    pub fn regions(&self, caps: [usize; 10]) -> Vec<Region> {
105        let [nreg, _, maxcell, _, _, _, _, heap, depth, _] = caps;
106        let mut r: Vec<(&'static str, u32)> = Vec::new();
107        if self.cells.is_some() {
108            let bits = ceil_log2(maxcell);
109            r.extend([
110                ("cell_lane0", bits),
111                ("cell_lane1", bits),
112                ("cell_lane2", bits),
113            ]);
114        }
115        if self.frames.is_some() {
116            r.push(("frames", ceil_log2(depth * nreg)));
117        }
118        if self.heap.is_some() {
119            r.push(("heap", ceil_log2(heap)));
120        }
121        r.sort_by(|a, b| b.1.cmp(&a.1));
122        let mut base = 0u32;
123        r.into_iter()
124            .map(|(name, bits)| {
125                let region = Region { name, base, bits };
126                base += 1 << bits;
127                region
128            })
129            .collect()
130    }
131
132    /// Refuse a combination the engine cannot build: nothing off-chip, the config image off-chip without
133    /// the registered config port, or regions the part cannot hold.
134    pub fn check(&self, caps: [usize; 10], backend: MemBackend) -> Result<(), OffchipUnplaceable> {
135        if self.roles().is_empty() {
136            return Err(OffchipUnplaceable::NothingOffChip);
137        }
138        if self.cells.is_some() && backend != MemBackend::ConfigBram {
139            return Err(OffchipUnplaceable::CellsNeedConfigBram { backend });
140        }
141        let words: u64 = self.regions(caps).iter().map(|r| 1u64 << r.bits).sum();
142        if words > self.part_words() {
143            return Err(OffchipUnplaceable::RegionsLargerThanPart {
144                words,
145                part_words: self.part_words(),
146            });
147        }
148        Ok(())
149    }
150
151    /// The longest stall the loader can meet while bytes are still arriving. Until it takes the go byte
152    /// the loader touches the part only to write — one config-image field per four bytes, one preloaded
153    /// heap word per eight — and the walk fetches no cells, so the worst is one write miss in the heap or
154    /// cell caches that evicts a dirty line. Stalls in the run are not bounded by the UART: the host sends
155    /// nothing until the result frame is back.
156    pub fn loading_stall_cycles(&self) -> u64 {
157        [self.heap, self.cells]
158            .into_iter()
159            .flatten()
160            .map(|c| c.miss_stall_cycles(&self.edge))
161            .max()
162            .unwrap_or(0)
163    }
164
165    /// Refuse a UART that could lose a byte. The receiver holds one byte while the FSM is stalled, so a byte
166    /// is lost only if another completes before the stall ends; requiring every loading stall to be shorter
167    /// than a byte means no byte ever waits at all. The first access to the part comes no sooner than
168    /// [`wire_bytes_before_first_part_access`] bytes after reset, and the edge must have initialized the
169    /// part by then, or that access would wait out the power-up too.
170    pub fn check_uart(&self, clks_per_bit: u64) -> Result<(), OffchipUnplaceable> {
171        let byte_cycles = uart_byte_cycles(clks_per_bit);
172        let stall_cycles = self.loading_stall_cycles();
173        if stall_cycles >= byte_cycles {
174            return Err(OffchipUnplaceable::StallOutlastsByte {
175                stall_cycles,
176                byte_cycles,
177            });
178        }
179        let load_cycles = wire_bytes_before_first_part_access() * byte_cycles;
180        let init_cycles = self.edge.init_cycles();
181        if init_cycles >= load_cycles {
182            return Err(OffchipUnplaceable::InitOutlastsLoad {
183                init_cycles,
184                load_cycles,
185            });
186        }
187        Ok(())
188    }
189
190    /// The fastest UART (fewest system cycles per bit) [`check_uart`](Self::check_uart) accepts.
191    pub fn min_clks_per_bit(&self) -> u64 {
192        let by_stall = self.loading_stall_cycles() / 10 + 1;
193        let by_init = self.edge.init_cycles() / (10 * wire_bytes_before_first_part_access()) + 1;
194        by_stall.max(by_init)
195    }
196
197    /// The Verilog that wires the off-chip memories to the part: a cache per memory (three for the config
198    /// image), the arbiter, the edge on the PSRAM pins, and `mstall`, the OR of every cache's `busy`.
199    fn glue(&self, caps: [usize; 10]) -> String {
200        let aw = EDGE_WORD_ADDR_BITS;
201        let regions = self.regions(caps);
202        let addr = |name: &str, index: &str| {
203            let r = regions
204                .iter()
205                .find(|r| r.name == name)
206                .expect("a region per off-chip memory");
207            match aw - r.bits {
208                0 => format!("{index}[{}:0]", aw - 1),
209                high => format!(
210                    "{{{high}'d{}, {index}[{}:0]}}",
211                    r.base >> r.bits,
212                    r.bits - 1
213                ),
214            }
215        };
216        struct Port<'a> {
217            inst: String,
218            cache: &'a PsramCache,
219            req_r: String,
220            ra: String,
221            rhit: String,
222            req_w: String,
223            wa: String,
224            whit: String,
225            wd: &'static str,
226            wm: &'static str,
227            rd: String,
228        }
229        // The template's tasks register each address's hit through one function per memory, `<q>_hit_at(a)` of the
230        // engine address `a`: the lookup in that memory's cache (or, for the config image, each lane's) of the
231        // address the cache sees for it.
232        let mut hit_at = String::new();
233        let mut ports: Vec<Port> = Vec::new();
234        let mut cell_wires = String::new();
235        if let Some(c) = &self.heap {
236            ports.push(Port {
237                inst: "heap_cache".into(),
238                cache: c,
239                req_r: "hq_r".into(),
240                ra: addr("heap", "heap_ra"),
241                rhit: "hq_rhit".into(),
242                req_w: "hq_w".into(),
243                wa: addr("heap", "hq_wa"),
244                whit: "hq_whit".into(),
245                wd: "hq_wd",
246                wm: "2'b11",
247                rd: "heap_rd".into(),
248            });
249            let _ = write!(
250                hit_at,
251                "  function hq_hit_at(input [31:0] a); hq_hit_at = heap_cache_hit({}); endfunction\n",
252                addr("heap", "a")
253            );
254        }
255        if let Some(c) = &self.frames {
256            ports.push(Port {
257                inst: "frame_cache".into(),
258                cache: c,
259                req_r: "sq_r".into(),
260                ra: addr("frames", "sf_ra"),
261                rhit: "sq_rhit".into(),
262                req_w: "sq_w".into(),
263                wa: addr("frames", "sq_wa"),
264                whit: "sq_whit".into(),
265                wd: "sq_wd",
266                wm: "2'b11",
267                rd: "sf_rd".into(),
268            });
269            let _ = write!(
270                hit_at,
271                "  function sq_hit_at(input [31:0] a); sq_hit_at = frame_cache_hit({}); endfunction\n",
272                addr("frames", "a")
273            );
274        }
275        if let Some(c) = &self.cells {
276            cell_wires.push_str(
277                "  // The walk reads the current cell every cycle of the run, as the on-chip registered read does.\n\
278                 \x20 wire cell_fetch = (ph == P_RUN);\n\
279                 \x20 wire [31:0] cell_at = c_cellptr + ci;\n\
280                 \x20 wire [63:0] cell_rd0, cell_rd1, cell_rd2;\n\
281                 \x20 assign cc_kind_r = cell_rd0[31:0]; assign cc_code_r = cell_rd0[63:32];\n\
282                 \x20 assign cc_op0_r = cell_rd1[31:0];  assign cc_op1_r = cell_rd1[63:32];\n\
283                 \x20 assign cc_op2_r = cell_rd2[31:0];  assign cc_op3_r = cell_rd2[63:32];\n",
284            );
285            let mut lanes = Vec::new();
286            for k in 0..3 {
287                let lane = format!("cell_lane{k}");
288                lanes.push(format!("{lane}_hit({})", addr(&lane, "a")));
289                ports.push(Port {
290                    inst: lane.clone(),
291                    cache: c,
292                    req_r: "cell_fetch".into(),
293                    ra: addr(&lane, "cell_at"),
294                    rhit: format!("cq_rhit[{k}]"),
295                    req_w: format!("cq_w[{k}]"),
296                    wa: addr(&lane, "cq_wa"),
297                    whit: format!("cq_whit[{k}]"),
298                    wd: "cq_wd",
299                    wm: "cq_wm",
300                    rd: format!("cell_rd{k}"),
301                });
302            }
303            lanes.reverse();
304            let _ = write!(
305                hit_at,
306                "  function [2:0] cq_hit_at(input [31:0] a); cq_hit_at = {{{}}}; endfunction\n",
307                lanes.join(", ")
308            );
309        }
310        let n = ports.len();
311        let mut g = format!(
312            "  // The off-chip memories, one cache each, sharing the PSRAM edge through the arbiter. Each lives in its\n\
313             \x20 // own naturally aligned power-of-two region of the part: its address is the region's high bits over\n\
314             \x20 // its own index.\n\
315             \x20 wire [{n1}:0] mem_busy;\n\
316             \x20 wire mstall = |mem_busy;\n\
317             \x20 wire [{n1}:0] mc_valid, mc_ready, mc_we;\n\
318             \x20 wire [{awn}:0] mc_addr; wire [{lw}:0] mc_len; wire [{dw}:0] mc_wdata;\n\
319             \x20 wire me_valid, me_ready, me_we, me_take, me_rvalid;\n\
320             \x20 wire [{am}:0] me_addr; wire [7:0] me_len; wire [63:0] me_wdata, me_rdata;\n\
321             {cell_wires}",
322            n1 = n - 1,
323            awn = n as u32 * aw - 1,
324            lw = n * 8 - 1,
325            dw = n * 64 - 1,
326            am = aw - 1,
327        );
328        for (k, p) in ports.iter().enumerate() {
329            g.push_str(&emit_psram_cache_hit_wires(&p.inst, p.cache));
330            let _ = write!(
331                g,
332                "  psram_cache #(.IW({iw}), .OW({ow})) {inst} (\n\
333                 \x20   .clk(clk), .rst(rst), .req_r({rq}), .ra({ra}), .req_w({wq}), .wa({wa}), .wd({wd}), .wm({wm}),\n\
334                 \x20   {hits},\n\
335                 \x20   .rd({rd}), .busy(mem_busy[{k}]), .e_valid(mc_valid[{k}]), .e_ready(mc_ready[{k}]), .e_we(mc_we[{k}]),\n\
336                 \x20   .e_addr(mc_addr[{a} +: {aw}]), .e_len(mc_len[{l} +: 8]), .e_wdata(mc_wdata[{d} +: 64]),\n\
337                 \x20   .e_wdata_take(me_take), .e_rdata(me_rdata), .e_rdata_valid(me_rvalid));\n",
338                iw = p.cache.index_bits,
339                ow = p.cache.offset_bits,
340                inst = p.inst,
341                hits = psram_cache_hit_ports(&p.inst, &p.rhit, &p.whit),
342                rq = p.req_r,
343                ra = p.ra,
344                wq = p.req_w,
345                wa = p.wa,
346                wd = p.wd,
347                wm = p.wm,
348                rd = p.rd,
349                a = k as u32 * aw,
350                l = k * 8,
351                d = k * 64,
352            );
353        }
354        for p in &ports {
355            g.push_str(&emit_psram_cache_hit_function(&p.inst, p.cache));
356        }
357        g.push_str(&hit_at);
358        let _ = write!(
359            g,
360            "  psram_arbiter #(.N({n})) mem_arb (\n\
361             \x20   .clk(clk), .rst(rst), .c_valid(mc_valid), .c_ready(mc_ready), .c_we(mc_we), .c_addr(mc_addr), .c_len(mc_len),\n\
362             \x20   .c_wdata(mc_wdata), .e_valid(me_valid), .e_ready(me_ready), .e_we(me_we), .e_addr(me_addr), .e_len(me_len),\n\
363             \x20   .e_wdata(me_wdata));\n\
364             \x20 psram_edge mem_edge (\n\
365             \x20   .clk(clk), .rst(rst), .req_valid(me_valid), .req_ready(me_ready), .req_we(me_we), .req_addr(me_addr),\n\
366             \x20   .req_len(me_len), .wdata(me_wdata), .wdata_take(me_take), .rdata(me_rdata), .rdata_valid(me_rvalid),\n\
367             \x20   .ce_n(psram_ce_n), .sclk(psram_sclk), .sio_out(psram_sio_out), .sio_oe(psram_sio_oe), .sio_in(psram_sio_in));"
368        );
369        g
370    }
371}
372
373/// The engine with `memory`'s memories off the die: the template's off-chip regions for each of them and
374/// the generated glue, then the FPU modules, the cache, the arbiter and the edge, in one file.
375pub fn emit_wprog_engine_soc_verilog_offchip(
376    name: &str,
377    caps: [usize; 10],
378    backend: MemBackend,
379    memory: &OffchipMemory,
380) -> Result<String, OffchipUnplaceable> {
381    memory.check(caps, backend)?;
382    let body = emit_wprog_engine_soc_offchip_body(name, caps, backend, &memory.roles());
383    assert_eq!(
384        body.matches("//__OFFCHIP_GLUE__").count(),
385        1,
386        "the template carries one glue site"
387    );
388    let cache = memory
389        .heap
390        .or(memory.frames)
391        .or(memory.cells)
392        .expect("checked: something is off-chip");
393    Ok(format!(
394        "{}{}\n{}\n{}",
395        body.replace("//__OFFCHIP_GLUE__", &memory.glue(caps)),
396        emit_psram_cache_verilog(&cache),
397        emit_psram_arbiter_verilog(),
398        emit_psram_edge_verilog(&memory.edge),
399    ))
400}
401
402/// The over-the-wire host for the off-chip engine, on a board carrying the part: the same stimulus and
403/// checks as the on-chip testbench, the clock at the edge's system period, the part's pin model on the SIO
404/// pads, and a monitor that fails the run the moment a received byte is overwritten before the engine takes
405/// it. Prints a `FRAME` line per program, then `PSRAM-VIOLATIONS` and `PSRAM-STALL-CYCLES`.
406pub fn emit_wprog_engine_soc_offchip_testbench(
407    name: &str,
408    n_vectors: usize,
409    clks_per_bit: usize,
410    max_bytes: usize,
411    memory: &OffchipMemory,
412) -> String {
413    emit_wprog_engine_soc_offchip_testbench_clocked(
414        name,
415        n_vectors,
416        clks_per_bit,
417        max_bytes,
418        memory,
419        memory.edge.sys_period_ps,
420    )
421}
422
423/// [`emit_wprog_engine_soc_offchip_testbench`] with the clock at `clk_period_ps` — any period in the edge's
424/// clock range, where its cycle counts meet every timing of the part.
425pub fn emit_wprog_engine_soc_offchip_testbench_clocked(
426    name: &str,
427    n_vectors: usize,
428    clks_per_bit: usize,
429    max_bytes: usize,
430    memory: &OffchipMemory,
431    clk_period_ps: u32,
432) -> String {
433    let half_ns = clk_period_ps as f64 / 2000.0;
434    format!(
435        "{}\n{}",
436        emit_wprog_engine_soc_testbench_board(name, n_vectors, clks_per_bit, max_bytes, true)
437            .replace("__HALF_NS__", &format!("{half_ns}")),
438        emit_psram_model_verilog(&memory.edge.part),
439    )
440}