Skip to main content

logicaffeine_compile/codegen_sva/
wpe_caravel.rs

1//! The whole-program engine as a Caravel user project: the off-chip engine
2//! ([`wpe_offchip`](super::wpe_offchip)) on the Caravel user interface, its UART and its PSRAM on user
3//! GPIOs, clocked by `user_clock2` ([`WPE_CLOCK`]) and reset by the management SoC.
4//!
5//! One pin plan ([`WpePins`]) is the source of every place a pin appears: the macro's `io_out`/`io_oeb`
6//! wiring, the power-on GPIO modes of `user_defines.v`, and the testbench that runs a program through the
7//! Caravel pins against the part's pin model. The UART's bit period is derived from the clock and the baud
8//! rate and checked against both the UART's tolerance and the off-chip memories' loading stalls.
9
10use super::fabric_rtl::program_cfg_fabric;
11use super::pdk_target::{HarnessClock, SKY130A};
12use super::psram::{PsramCache, PsramEdge, APS6404L_3SQN};
13use super::wpe_floorplan::{WpeDemands, WpeProfile};
14use super::wpe_offchip::{
15    emit_wprog_engine_soc_offchip_testbench_clocked, emit_wprog_engine_soc_verilog_offchip,
16    OffchipMemory, OffchipUnplaceable,
17};
18use super::wprog_engine_rtl::{
19    eval_wprog_config_counted_heap_preloaded, eval_wprog_config_run, program_main_fabric,
20    serialize_program, wprog_config_image_unchecked, MemBackend, WProgConfig,
21};
22
23/// Caravel's user GPIOs: `io_in`/`io_out`/`io_oeb[37:0]`, of which 0..=4 carry the housekeeping SPI and JTAG.
24pub const CARAVEL_IO: u8 = 38;
25pub const CARAVEL_FIRST_USER_IO: u8 = 5;
26
27/// Where the engine's pins land on Caravel's user GPIOs.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct WpePins {
30    pub uart_rx: u8,
31    pub uart_tx: u8,
32    pub psram_ce_n: u8,
33    pub psram_sclk: u8,
34    pub psram_sio: [u8; 4],
35}
36
37/// The shipped plan: the UART on GPIO 8 and 9, the PSRAM's CE#, SCLK and `SIO[3:0]` on 10..=15 — clear of
38/// the housekeeping pins and of the management UART on 5 and 6.
39pub const WPE_PINS: WpePins = WpePins {
40    uart_rx: 8,
41    uart_tx: 9,
42    psram_ce_n: 10,
43    psram_sclk: 11,
44    psram_sio: [12, 13, 14, 15],
45};
46
47/// How a GPIO comes up at power-on, as `user_defines.v` names the mode.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum GpioMode {
50    Input,
51    Output,
52    Bidirectional,
53}
54
55impl GpioMode {
56    pub fn define(&self) -> &'static str {
57        match self {
58            GpioMode::Input => "GPIO_MODE_USER_STD_INPUT_NOPULL",
59            GpioMode::Output => "GPIO_MODE_USER_STD_OUTPUT",
60            GpioMode::Bidirectional => "GPIO_MODE_USER_STD_BIDIRECTIONAL",
61        }
62    }
63}
64
65/// Why the engine cannot be wired to Caravel as asked.
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub enum WpeCaravelUnplaceable {
68    /// A pin is a housekeeping pin or past the last GPIO.
69    PinNotUser { pin: u8 },
70    /// Two signals share a pin.
71    PinShared { pin: u8 },
72    /// The clock cannot make the baud rate within a UART's tolerance.
73    BaudUnreachable {
74        baud: u32,
75        clks_per_bit: u64,
76        error_ppm: u64,
77    },
78    /// The engine's clock lies outside the range the PSRAM edge is timed for.
79    ClockOutsideEdgeRange {
80        engine_ps: u32,
81        fastest_ps: u32,
82        slowest_ps: u32,
83    },
84    /// Half an SCLK period at the engine's clock does not cover a read's round trip through the harness and
85    /// the board: SCLK out to the part, the part's tACLK, the data back in.
86    ReadWindowShortOfHarness {
87        half_sclk_ps: u64,
88        round_trip_ps: u64,
89    },
90    /// The off-chip memories refuse the configuration or the UART.
91    Memory(OffchipUnplaceable),
92}
93
94/// A UART tolerates a little over 2% of accumulated error across a frame, split between both ends.
95pub const UART_MAX_ERROR_PPM: u64 = 10_000;
96
97/// Caravel's harness between a user-area port and its pad, as efabless characterizes it for user projects: the
98/// `IO_SYNC` budget of the scaffold's `base_user_project_wrapper.sdc`, generated from Caravel's own STA by
99/// `get_cup_sdc.py`. `io_out` reaches its pad in 3.90..=9.12 ns, `io_oeb` in 2.34..=9.32 ns, and a pad
100/// reaches `io_in` in 1.26..=4.55 ns.
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct HarnessDelay {
103    pub out_ps: u64,
104    pub oeb_ps: u64,
105    pub in_ps: u64,
106}
107
108pub const CARAVEL_IO_MAX: HarnessDelay = HarnessDelay {
109    out_ps: 9_120,
110    oeb_ps: 9_320,
111    in_ps: 4_550,
112};
113pub const CARAVEL_IO_MIN: HarnessDelay = HarnessDelay {
114    out_ps: 3_900,
115    oeb_ps: 2_340,
116    in_ps: 1_260,
117};
118
119impl HarnessDelay {
120    /// The delay from a user-area port to the part's pin and back, with `board_ps` of trace each way.
121    pub fn with_board(self, board_ps: u64) -> HarnessDelay {
122        HarnessDelay {
123            out_ps: self.out_ps + board_ps,
124            oeb_ps: self.oeb_ps + board_ps,
125            in_ps: self.in_ps + board_ps,
126        }
127    }
128}
129
130/// The carrier board's trace between a pad and the part, each way: at most 1 ns, about 15 cm of FR-4. A
131/// requirement the board must meet, not a property of the die.
132pub const BOARD_FLIGHT_MAX_PS: u64 = 1_000;
133
134impl WpePins {
135    /// Every used pin with its mode.
136    pub fn used(&self) -> Vec<(u8, GpioMode)> {
137        let mut v = vec![
138            (self.uart_rx, GpioMode::Input),
139            (self.uart_tx, GpioMode::Output),
140            (self.psram_ce_n, GpioMode::Output),
141            (self.psram_sclk, GpioMode::Output),
142        ];
143        v.extend(self.psram_sio.iter().map(|&p| (p, GpioMode::Bidirectional)));
144        v
145    }
146
147    /// Refuse a housekeeping pin, a pin past the last GPIO, or a pin used twice.
148    pub fn check(&self) -> Result<(), WpeCaravelUnplaceable> {
149        let used = self.used();
150        for (i, &(pin, _)) in used.iter().enumerate() {
151            if !(CARAVEL_FIRST_USER_IO..CARAVEL_IO).contains(&pin) {
152                return Err(WpeCaravelUnplaceable::PinNotUser { pin });
153            }
154            if used[..i].iter().any(|&(p, _)| p == pin) {
155                return Err(WpeCaravelUnplaceable::PinShared { pin });
156            }
157        }
158        Ok(())
159    }
160
161    /// The power-on mode of every user GPIO: the plan's pins as they are used, every other one an input.
162    pub fn gpio_modes(&self) -> Vec<(u8, GpioMode)> {
163        (CARAVEL_FIRST_USER_IO..CARAVEL_IO)
164            .map(|pin| {
165                (
166                    pin,
167                    self.used()
168                        .iter()
169                        .find(|(p, _)| *p == pin)
170                        .map_or(GpioMode::Input, |&(_, m)| m),
171                )
172            })
173            .collect()
174    }
175}
176
177/// The engine on Caravel: its clock period, the host's baud rate, and the pin plan.
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct WpeCaravel {
180    pub clk_period_ps: u32,
181    pub baud: u32,
182    pub pins: WpePins,
183}
184
185impl WpeCaravel {
186    /// System cycles per UART bit, the nearest the clock can make the baud rate.
187    pub fn clks_per_bit(&self) -> u64 {
188        let cycles_per_second = 1_000_000_000_000u64 / self.clk_period_ps as u64;
189        (cycles_per_second + self.baud as u64 / 2) / self.baud as u64
190    }
191
192    /// How far that bit period is from the baud rate's, in parts per million.
193    pub fn baud_error_ppm(&self) -> u64 {
194        let ideal_ps = 1_000_000_000_000u64 / self.baud as u64;
195        let actual_ps = self.clks_per_bit() * self.clk_period_ps as u64;
196        ideal_ps.abs_diff(actual_ps) * 1_000_000 / ideal_ps
197    }
198
199    /// A read's round trip from the edge's SCLK flop to its sample flop, outside the macro: the falling edge
200    /// crosses the harness and the board, the part drives after tACLK, and the data crosses back. The edge
201    /// samples half an SCLK period after that falling edge, so the half period must cover it.
202    pub fn read_round_trip_ps(&self, memory: &OffchipMemory) -> u64 {
203        CARAVEL_IO_MAX.out_ps
204            + BOARD_FLIGHT_MAX_PS
205            + memory.edge.part.t_aclk_max_ps as u64
206            + BOARD_FLIGHT_MAX_PS
207            + CARAVEL_IO_MAX.in_ps
208    }
209
210    /// Half an SCLK period at the engine's clock.
211    pub fn half_sclk_ps(&self, memory: &OffchipMemory) -> u64 {
212        memory.edge.half as u64 * self.clk_period_ps as u64
213    }
214
215    /// Refuse pins, a baud rate the clock cannot make, a clock the edge is not timed for, or a UART the
216    /// off-chip memories could lose bytes on. These are the wiring's own rules, which hold in simulation with
217    /// ideal pins; [`WpeCaravel::check_harness`] adds the rule the chip's harness imposes.
218    pub fn check(&self, memory: &OffchipMemory) -> Result<(), WpeCaravelUnplaceable> {
219        self.pins.check()?;
220        if self.baud_error_ppm() > UART_MAX_ERROR_PPM {
221            return Err(WpeCaravelUnplaceable::BaudUnreachable {
222                baud: self.baud,
223                clks_per_bit: self.clks_per_bit(),
224                error_ppm: self.baud_error_ppm(),
225            });
226        }
227        let (fastest_ps, slowest_ps) = (memory.edge.sys_period_ps, memory.edge.slowest_period_ps);
228        if !(fastest_ps..=slowest_ps).contains(&self.clk_period_ps) {
229            return Err(WpeCaravelUnplaceable::ClockOutsideEdgeRange {
230                engine_ps: self.clk_period_ps,
231                fastest_ps,
232                slowest_ps,
233            });
234        }
235        memory
236            .check_uart(self.clks_per_bit())
237            .map_err(WpeCaravelUnplaceable::Memory)
238    }
239
240    /// [`WpeCaravel::check`], and a read window the round trip through Caravel's harness and the board fits:
241    /// the configuration a chip can be built with.
242    pub fn check_harness(&self, memory: &OffchipMemory) -> Result<(), WpeCaravelUnplaceable> {
243        self.check(memory)?;
244        let (half_sclk_ps, round_trip_ps) =
245            (self.half_sclk_ps(memory), self.read_round_trip_ps(memory));
246        if half_sclk_ps <= round_trip_ps {
247            return Err(WpeCaravelUnplaceable::ReadWindowShortOfHarness {
248                half_sclk_ps,
249                round_trip_ps,
250            });
251        }
252        Ok(())
253    }
254}
255
256/// The engine the Caravel user project carries: `wpe_soc_caravel`.
257pub const WPE_CARAVEL_NAME: &str = "caravel";
258
259/// The shipped cap profile, `[NREG, MAXBLK, MAXCELL, MAXWR, MAXARG, MAXLIV, MAXK, HEAP, DEPTH, MAXV]`.
260///
261/// The three memories that live in the part take the measured suite's ceilings: 1072 cells, 32 frames, and a
262/// 32768-word heap — the heap region is zeroed at every program load, one line write-back at a time, so its
263/// size costs load latency rather than die area. The on-die tables are flip-flops, so
264/// they are sized to what the suite's programs need: every program whose per-block cells fit `MAXV` = 64 and
265/// whose heap fits the region demands at most 29 blocks, 104 writes and 52 live-ins (`graph_bfs`), and the
266/// tables take those with headroom. The per-block cell values stay at 64 — `nbody`, `strings` and `sha256`
267/// need 223 to 450, the one array whose growth would not fit the user area.
268pub const WPE_CARAVEL_CAPS: [usize; 10] = [48, 32, 1072, 112, 16, 56, 64, 32_768, 32, 64];
269
270/// The config image reaches the walk through a registered port only under the config-in-BRAM backend.
271pub const WPE_CARAVEL_BACKEND: MemBackend = MemBackend::ConfigBram;
272
273/// The harness clock the engine runs on. Caravel's chip signoff times the core clock, `wb_clk_i`, at 25 ns, and the
274/// management SoC and the bus run on it; `user_clock2` is the PLL's 90-degree phase through a divider of its own, so
275/// the engine runs at its own period while the management SoC stays at the one the chip is signed for.
276pub const WPE_CLOCK: HarnessClock = HarnessClock::User2;
277
278/// The shipped wiring: the pin plan, the 21 ns clock (47.62 MHz on `user_clock2`) and a 115200-baud host.
279///
280/// The clock is the engine's own. Every float unit, the integer multiplier and the conversions are staged so no
281/// stage holds more than one carry chain, leading-one search, barrel shift or compressor tree, and the macro is
282/// hardened at this period (`CaravelProject::macro_period_ns`). The period is what the engine's slow corner meets
283/// (`signoff_blocks::honest_period_ns`): hardened at 18.5 ns and routed, its paths through a compare chain into the
284/// control and out through a fanout tree missed setup at ss_100C_1v60 by 1.13 ns on extracted parasitics, where the
285/// routing stage's repairs had closed on global-route estimates; hardened at 16 ns, the paths from the frame cache's
286/// hit into the stall that freezes every register, and from `blk_term_kind_r`, had missed by 1.48 ns. The off-chip
287/// memory follows it: the edge runs two engine cycles per SCLK half, so the 42 ns half period still covers a read's
288/// round trip through the harness and the board ([`WpeCaravel::read_round_trip_ps`]).
289pub const WPE_CARAVEL: WpeCaravel = WpeCaravel {
290    clk_period_ps: 21_000,
291    baud: 115_200,
292    pins: WPE_PINS,
293};
294
295/// The shipped off-chip memories: the edge's cycle counts are timed at 16 ns with two engine cycles per SCLK
296/// half (a 64 ns SCLK), so every slower clock meets tPU, tRST, tCPH, tCLK and tACLK too, and its bursts are cut
297/// for 40 ns, where tCEM leaves two words a line; heap, frame stack and config image each get eight such lines.
298pub fn wpe_caravel_memory() -> OffchipMemory {
299    let edge = PsramEdge::new(APS6404L_3SQN, 16_000, 2)
300        .and_then(|e| e.clocked_down_to(40_000))
301        .expect("the part runs from 16 to 40 ns");
302    let cache = PsramCache::new(&edge, 3, 1).expect("eight two-word lines");
303    OffchipMemory {
304        edge,
305        heap: Some(cache),
306        frames: Some(cache),
307        cells: Some(cache),
308    }
309}
310
311/// The full-interface Caravel user macro `wpe_user_project`: the engine `wpe_soc_<name>` with `memory`'s
312/// memories off-chip, clocked by [`WPE_CLOCK`], its UART and PSRAM on `caravel.pins` (a bidirectional SIO pad is
313/// driven exactly while the edge drives it: `io_oeb` is its active-low enable), every other GPIO an input.
314///
315/// The engine's reset crosses into its clock domain through two flops, asserted at once and released on the second
316/// edge after its source falls. The source is `wb_rst_i`, or logic-analyzer probe 0 while the management SoC drives
317/// it (`la_oenb[0]` low, efabless's override in `user_proj_example.v`): at power-on the PLL is bypassed and
318/// `user_clock2` is the pad clock, `caravel_clocking.v` moves it to the PLL with a plain select between the two, and
319/// the firmware resets the engine after that switch, which a chip reset cannot do without bypassing the PLL again.
320/// The Wishbone slave runs on `wb_clk_i`, acknowledges every access and reads zero, so the management SoC never
321/// waits on the user area; the rest of the logic analyzer and the interrupts are unused.
322pub fn emit_wpe_user_project_verilog(
323    name: &str,
324    caps: [usize; 10],
325    backend: MemBackend,
326    memory: &OffchipMemory,
327    caravel: &WpeCaravel,
328) -> Result<String, WpeCaravelUnplaceable> {
329    caravel.check(memory)?;
330    // The user macro is an ASIC build, so its multiplies take the compressor tree: the engine's MUL_TREE default
331    // bakes to 1, the way a config-BRAM engine's CFG_BRAM does.
332    let engine = emit_wprog_engine_soc_verilog_offchip(name, caps, backend, memory)
333        .map_err(WpeCaravelUnplaceable::Memory)?
334        .replacen("parameter MUL_TREE = 0", "parameter MUL_TREE = 1", 1);
335    let p = &caravel.pins;
336    let (mut outs, mut oebs) = (Vec::new(), Vec::new());
337    for pin in (0..CARAVEL_IO).rev() {
338        let (out, oeb) = if pin == p.uart_tx {
339            ("uart_tx".to_string(), "1'b0".to_string())
340        } else if pin == p.psram_ce_n {
341            ("psram_ce_n".to_string(), "1'b0".to_string())
342        } else if pin == p.psram_sclk {
343            ("psram_sclk".to_string(), "1'b0".to_string())
344        } else if let Some(k) = p.psram_sio.iter().position(|&s| s == pin) {
345            (format!("psram_sio_out[{k}]"), format!("~psram_sio_oe[{k}]"))
346        } else {
347            ("1'b0".to_string(), "1'b1".to_string())
348        };
349        outs.push(out);
350        oebs.push(oeb);
351    }
352    let sio_in = p
353        .psram_sio
354        .iter()
355        .rev()
356        .map(|s| format!("io_in[{s}]"))
357        .collect::<Vec<_>>()
358        .join(", ");
359    Ok(format!(
360        "{engine}\n\
361         // wpe_user_project — the whole-program engine as a Caravel user macro (codegen_sva::wpe_caravel). UART rx/tx\n\
362         // on GPIO {rx}/{tx}; PSRAM CE# {ce}, SCLK {sclk}, SIO[3:0] {s0}..{s3}; engine clock {clk} ({period} ps), {baud}\n\
363         // baud ({cpb} cycles per bit); the Wishbone slave on wb_clk_i.\n\
364         module wpe_user_project (\n\
365         `ifdef USE_POWER_PINS\n\
366         \x20 inout         vccd1,\n\
367         \x20 inout         vssd1,\n\
368         `endif\n\
369         \x20 input         wb_clk_i,\n\
370         \x20 input         wb_rst_i,\n\
371         \x20 input         wbs_stb_i,\n\
372         \x20 input         wbs_cyc_i,\n\
373         \x20 input         wbs_we_i,\n\
374         \x20 input  [3:0]  wbs_sel_i,\n\
375         \x20 input  [31:0] wbs_dat_i,\n\
376         \x20 input  [31:0] wbs_adr_i,\n\
377         \x20 output        wbs_ack_o,\n\
378         \x20 output [31:0] wbs_dat_o,\n\
379         \x20 input  [127:0] la_data_in,\n\
380         \x20 output [127:0] la_data_out,\n\
381         \x20 input  [127:0] la_oenb,\n\
382         \x20 input  [37:0]  io_in,\n\
383         \x20 output [37:0]  io_out,\n\
384         \x20 output [37:0]  io_oeb,\n\
385         \x20 input         user_clock2,\n\
386         \x20 output [2:0]  user_irq\n\
387         );\n\
388         \x20 wire uart_tx, psram_ce_n, psram_sclk;\n\
389         \x20 wire [3:0] psram_sio_out, psram_sio_oe;\n\
390         \x20 // The engine's reset: wb_rst_i, or probe 0 while the management SoC drives it, into the engine's clock\n\
391         \x20 // domain through two flops, asserted at once and released on the second {clk} edge after it falls.\n\
392         \x20 wire engine_rst_req = wb_rst_i | (la_data_in[0] & ~la_oenb[0]);\n\
393         \x20 reg [1:0] engine_rst;\n\
394         \x20 always @(posedge {clk} or posedge engine_rst_req)\n\
395         \x20   if (engine_rst_req) engine_rst <= 2'b11;\n\
396         \x20   else engine_rst <= {{engine_rst[0], 1'b0}};\n\
397         \x20 wpe_soc_{name} #(.CLKS_PER_BIT({cpb})) engine (\n\
398         \x20   .clk({clk}), .rst(engine_rst[1]), .uart_rx(io_in[{rx}]), .uart_tx(uart_tx),\n\
399         \x20   .psram_ce_n(psram_ce_n), .psram_sclk(psram_sclk), .psram_sio_out(psram_sio_out), .psram_sio_oe(psram_sio_oe),\n\
400         \x20   .psram_sio_in({{{sio_in}}}));\n\
401         \x20 assign io_out = {{{outs}}};\n\
402         \x20 assign io_oeb = {{{oebs}}};\n\
403         \x20 // Every Wishbone access is acknowledged the cycle after it is presented and reads zero.\n\
404         \x20 reg ack;\n\
405         \x20 always @(posedge wb_clk_i) ack <= !wb_rst_i && wbs_cyc_i && wbs_stb_i && !ack;\n\
406         \x20 assign wbs_ack_o = ack;\n\
407         \x20 assign wbs_dat_o = 32'd0;\n\
408         \x20 assign la_data_out = 128'd0;\n\
409         \x20 assign user_irq = 3'd0;\n\
410         \x20 // The rest of the fixed interface is kept and connected so the wrapper wires every pin.\n\
411         \x20 wire _unused = &{{1'b0, la_data_in, la_oenb, wbs_we_i, wbs_sel_i, wbs_dat_i, wbs_adr_i, io_in}};\n\
412         endmodule\n",
413        rx = p.uart_rx,
414        tx = p.uart_tx,
415        ce = p.psram_ce_n,
416        sclk = p.psram_sclk,
417        s0 = p.psram_sio[0],
418        s3 = p.psram_sio[3],
419        period = caravel.clk_period_ps,
420        baud = caravel.baud,
421        cpb = caravel.clks_per_bit(),
422        clk = WPE_CLOCK.port(),
423        outs = outs.join(", "),
424        oebs = oebs.join(", "),
425    ))
426}
427
428/// The period Caravel's chip signoff times its core clock at, in ns: the clock the management SoC runs on.
429fn core_clock_period_ns() -> f64 {
430    SKY130A
431        .user_area
432        .and_then(|ua| ua.harness)
433        .expect("Caravel's harness is characterized")
434        .core_clock_period_ns
435}
436
437/// `user_defines.v`: every user GPIO's power-on mode, from the pin plan.
438pub fn emit_wpe_user_defines(pins: &WpePins) -> String {
439    let mut s = String::from(
440        "// user_defines.v — user GPIO power-on configuration for the LOGOS whole-program engine\n\
441         // (codegen_sva::wpe_caravel): the UART and PSRAM pins as the engine uses them, every other GPIO a\n\
442         // no-pull input.\n\
443         \n\
444         `default_nettype none\n\
445         \n\
446         `ifndef __USER_DEFINES_H\n\
447         `define __USER_DEFINES_H\n\
448         \n\
449         `define GPIO_MODE_INVALID                  13'hXXXX\n\
450         \n\
451         `define GPIO_MODE_MGMT_STD_INPUT_NOPULL    13'h0403\n\
452         `define GPIO_MODE_MGMT_STD_INPUT_PULLDOWN  13'h0c01\n\
453         `define GPIO_MODE_MGMT_STD_INPUT_PULLUP    13'h0801\n\
454         `define GPIO_MODE_MGMT_STD_OUTPUT          13'h1809\n\
455         `define GPIO_MODE_MGMT_STD_BIDIRECTIONAL   13'h1801\n\
456         `define GPIO_MODE_MGMT_STD_ANALOG          13'h000b\n\
457         \n\
458         `define GPIO_MODE_USER_STD_INPUT_NOPULL    13'h0402\n\
459         `define GPIO_MODE_USER_STD_INPUT_PULLDOWN  13'h0c00\n\
460         `define GPIO_MODE_USER_STD_INPUT_PULLUP    13'h0800\n\
461         `define GPIO_MODE_USER_STD_OUTPUT          13'h1808\n\
462         `define GPIO_MODE_USER_STD_BIDIRECTIONAL   13'h1800\n\
463         `define GPIO_MODE_USER_STD_OUT_MONITORED   13'h1802\n\
464         `define GPIO_MODE_USER_STD_ANALOG          13'h000a\n\
465         \n",
466    );
467    for (pin, mode) in pins.gpio_modes() {
468        s.push_str(&format!(
469            "`define USER_CONFIG_GPIO_{pin}_INIT `{}\n",
470            mode.define()
471        ));
472    }
473    s.push_str("\n`endif // __USER_DEFINES_H\n`default_nettype wire\n");
474    s
475}
476
477/// The off-chip engine's host testbench, driving `wpe_user_project` through its Caravel pins at the engine's
478/// clock: the UART on the plan's GPIOs, the part's pin model on the PSRAM GPIOs (a pad driven exactly while
479/// its `io_oeb` is low), every PSRAM pin reaching the part `pads` after the macro drives it and the part's
480/// data reaching `io_in` `pads.in_ps` after the part drives it, the management SoC's clock and reset, and an
481/// idle Wishbone. A pin driven the wrong way at any point after reset fails the run with `PIN-DIRECTION`.
482pub fn emit_wpe_user_project_testbench(
483    name: &str,
484    n_vectors: usize,
485    max_bytes: usize,
486    memory: &OffchipMemory,
487    caravel: &WpeCaravel,
488    pads: HarnessDelay,
489) -> String {
490    let p = &caravel.pins;
491    let board = emit_wprog_engine_soc_offchip_testbench_clocked(
492        name,
493        n_vectors,
494        caravel.clks_per_bit() as usize,
495        max_bytes,
496        memory,
497        caravel.clk_period_ps,
498    );
499    let dut = format!(
500        "  wpe_soc_{name} #(.CLKS_PER_BIT(CLKS)) dut(.clk(clk), .rst(rst), .uart_rx(uart_rx), .uart_tx(uart_tx),\n\
501         \x20   .psram_ce_n(psram_ce_n), .psram_sclk(psram_sclk), .psram_sio_out(psram_sio_out), .psram_sio_oe(psram_sio_oe),\n\
502         \x20   .psram_sio_in(psram_sio));\n"
503    );
504    assert_eq!(
505        board.matches(dut.as_str()).count(),
506        1,
507        "the board testbench instantiates the engine once"
508    );
509    let ns = |ps: u64| format!("{}.{:03}", ps / 1000, ps % 1000);
510    let (d_out, d_oeb, d_in) = (ns(pads.out_ps), ns(pads.oeb_ps), ns(pads.in_ps));
511    let sio_in = p
512        .psram_sio
513        .iter()
514        .enumerate()
515        .map(|(k, s)| format!("  assign #({d_in}) io_in[{s}] = psram_sio[{k}];\n"))
516        .collect::<String>();
517    let sio_out = p
518        .psram_sio
519        .iter()
520        .enumerate()
521        .map(|(k, s)| {
522            format!("  assign #({d_out}) psram_sio_out[{k}] = io_out[{s}]; assign #({d_oeb}) psram_sio_oe[{k}] = ~io_oeb[{s}];\n")
523        })
524        .collect::<String>();
525    let outputs = [p.uart_tx, p.psram_ce_n, p.psram_sclk];
526    let inputs: Vec<u8> = (0..CARAVEL_IO)
527        .filter(|pin| !outputs.contains(pin) && !p.psram_sio.contains(pin))
528        .collect();
529    let directions = format!(
530        "  // After reset every output pin drives and every other non-SIO pin is an input.\n\
531         \x20 always @(posedge clk) if (!rst && ({outs_oeb} || ~&{{{ins_oeb}}})) begin\n\
532         \x20   $display(\"PIN-DIRECTION io_oeb=%b\", io_oeb);\n\
533         \x20   $display(\"FAIL: a Caravel pin is driven the wrong way\");\n\
534         \x20   $finish;\n\
535         \x20 end\n",
536        outs_oeb = outputs
537            .iter()
538            .map(|o| format!("io_oeb[{o}]"))
539            .collect::<Vec<_>>()
540            .join(" || "),
541        ins_oeb = inputs
542            .iter()
543            .map(|i| format!("io_oeb[{i}]"))
544            .collect::<Vec<_>>()
545            .join(", "),
546    );
547    let user = format!(
548        "  wire [37:0] io_out, io_oeb; wire [37:0] io_in;\n\
549         \x20 genvar hk;\n\
550         \x20 generate for (hk = 0; hk < 38; hk = hk + 1) begin : idle_io\n\
551         \x20   if (hk != {rx} && hk != {s0} && hk != {s1} && hk != {s2} && hk != {s3}) begin : quiet\n\
552         \x20     assign io_in[hk] = 1'b0;\n\
553         \x20   end\n\
554         \x20 end endgenerate\n\
555         \x20 assign io_in[{rx}] = uart_rx;\n\
556         {sio_in}\
557         \x20 assign uart_tx = io_out[{tx}];\n\
558         \x20 assign #({d_out}) psram_ce_n = io_out[{ce}];\n\
559         \x20 assign #({d_out}) psram_sclk = io_out[{sclk}];\n\
560         {sio_out}\
561         \x20 wire wbs_ack_o; wire [31:0] wbs_dat_o; wire [127:0] la_data_out; wire [2:0] user_irq;\n\
562         \x20 // The management SoC's clock, at the period Caravel's chip signoff times it at; the engine's is clk.\n\
563         \x20 reg wb_clk = 0;\n\
564         \x20 always #{wb_half} wb_clk = ~wb_clk;\n\
565         \x20 wpe_user_project dut (\n\
566         \x20   .wb_clk_i(wb_clk), .wb_rst_i(rst), .wbs_stb_i(1'b0), .wbs_cyc_i(1'b0), .wbs_we_i(1'b0), .wbs_sel_i(4'd0),\n\
567         \x20   .wbs_dat_i(32'd0), .wbs_adr_i(32'd0), .wbs_ack_o(wbs_ack_o), .wbs_dat_o(wbs_dat_o),\n\
568         \x20   .la_data_in(128'd0), .la_data_out(la_data_out), .la_oenb({{128{{1'b1}}}}),\n\
569         \x20   .io_in(io_in), .io_out(io_out), .io_oeb(io_oeb), .{clk}(clk), .user_irq(user_irq));\n\
570         {directions}",
571        rx = p.uart_rx,
572        tx = p.uart_tx,
573        ce = p.psram_ce_n,
574        sclk = p.psram_sclk,
575        s0 = p.psram_sio[0],
576        s1 = p.psram_sio[1],
577        s2 = p.psram_sio[2],
578        s3 = p.psram_sio[3],
579        wb_half = core_clock_period_ns() / 2.0,
580        clk = WPE_CLOCK.port(),
581    );
582    board
583        .replace(&dut, &user)
584        .replace("dut.mstall", "dut.engine.mstall")
585        .replace("dut.rx_valid", "dut.engine.rx_valid")
586        .replace("dut.r_state", "dut.engine.r_state")
587        .replace("dut.r_cnt", "dut.engine.r_cnt")
588}
589
590/// [`emit_wpe_user_project_testbench`] driving the cell that is fabricated: Caravel's `user_project_wrapper`,
591/// the engine reached through its `mprj`.
592pub fn emit_wpe_user_project_wrapper_testbench(
593    name: &str,
594    n_vectors: usize,
595    max_bytes: usize,
596    memory: &OffchipMemory,
597    caravel: &WpeCaravel,
598    pads: HarnessDelay,
599) -> String {
600    let tb = emit_wpe_user_project_testbench(name, n_vectors, max_bytes, memory, caravel, pads);
601    assert_eq!(
602        tb.matches("  wpe_user_project dut (").count(),
603        1,
604        "the macro testbench instantiates the macro once"
605    );
606    tb.replace("  wpe_user_project dut (", "  user_project_wrapper dut (")
607        .replace("dut.engine.", "dut.mprj.engine.")
608}
609
610/// One program of the bring-up bench: the bytes the host sends and the result the model computes for them.
611#[derive(Clone, Debug, PartialEq, Eq)]
612pub struct BringupVector {
613    pub bytes: Vec<u8>,
614    pub result: i64,
615}
616
617/// Text built, compared and shown over the wire.
618pub const BRINGUP_TEXT: &str = "## Main\n    Let s be \"abc\".\n    Show item 2 of s.\n    Show s equals \"abc\".\n    Show \"n=\" + 42.\n";
619
620/// A weighted sum over a streamed preload of 40 weights: 41 heap words, more than a heap cache of eight two-word
621/// lines holds, so the weights come back from the part.
622pub const BRINGUP_PRELOAD: &str = "## To native loaded () -> Seq of Int\n## To sumw (n: Int) -> Int:\n    Let s be 0.\n    Let i be 0.\n    While i is less than n:\n        Set s to s + item (i + 1) of loaded().\n        Set i to i + 1.\n    Return s.\n";
623
624/// The bytes a host sends the Caravel chip to run `cfg` on `params` with `heap_init` streamed ahead of the run,
625/// or every table of the shipped profile the program overflows as (engine parameter, demand, cap). The chip
626/// traps a call deeper than its frame stack and a heap access past its region on its own; every other table is
627/// checked here, before anything is sent, because an overflowing table would be loaded into the wrong words.
628pub fn wpe_caravel_wire(
629    cfg: &WProgConfig,
630    params: &[i64],
631    heap_init: &[i64],
632) -> Result<Vec<u8>, Vec<(&'static str, usize, usize)>> {
633    let short = WpeProfile::from_caps(WPE_CARAVEL_CAPS).shortfalls(&WpeDemands::loaded(cfg));
634    if !short.is_empty() {
635        return Err(short);
636    }
637    Ok(wprog_config_image_unchecked(cfg).wire_bytes_preloaded(params, heap_init))
638}
639
640/// The bring-up bench's programs, in the order it sends them: the text program, then the preload summed over
641/// all 40 weights and over the first 17.
642pub fn wpe_caravel_bringup_vectors() -> Vec<BringupVector> {
643    let wire = |cfg: &WProgConfig, params: &[i64], heap_init: &[i64]| {
644        wpe_caravel_wire(cfg, params, heap_init)
645            .unwrap_or_else(|short| panic!("a bring-up program overflows {short:?}"))
646    };
647    let text = program_main_fabric(BRINGUP_TEXT);
648    let mut v = vec![BringupVector {
649        bytes: wire(&text, &[], &[]),
650        result: eval_wprog_config_run(&text, &[], &[]).result,
651    }];
652    let sumw = serialize_program(&program_cfg_fabric(BRINGUP_PRELOAD, "sumw"));
653    let blob: Vec<i64> = std::iter::once(40)
654        .chain((1..=40).map(|k| 7 * k - 100))
655        .collect();
656    for n in [40i64, 17] {
657        v.push(BringupVector {
658            bytes: wire(&sumw, &[n], &blob),
659            result: eval_wprog_config_counted_heap_preloaded(&sumw, &[n], &blob).0,
660        });
661    }
662    v
663}
664
665const SPDX_V: &str = "// SPDX-FileCopyrightText: 2026 DatacenterDotDev, Inc.\n// SPDX-License-Identifier: MIT\n";
666
667/// The scaffold's copyright line: `user_defines.v` carries the scaffold's GPIO mode table.
668const SCAFFOLD_COPYRIGHT_V: &str = "// SPDX-FileCopyrightText: 2022 Efabless Corporation\n";
669
670/// The emitted files of the WPE submission's overlay, by path under it: the GPIOs' power-on modes, and the
671/// bring-up directory — the bench driving the fabricated `user_project_wrapper` at the chip's clock with every
672/// PSRAM pin at the harness's slowest delay and the board's longest flight, and the program bytes,
673/// per-program lengths and model results it reads.
674pub fn wpe_caravel_overlay_files() -> Vec<(String, String)> {
675    let memory = wpe_caravel_memory();
676    let vectors = wpe_caravel_bringup_vectors();
677    let bytes: Vec<u8> = vectors
678        .iter()
679        .flat_map(|v| v.bytes.iter().copied())
680        .collect();
681    let tb = emit_wpe_user_project_wrapper_testbench(
682        WPE_CARAVEL_NAME,
683        vectors.len(),
684        bytes.len(),
685        &memory,
686        &WPE_CARAVEL,
687        CARAVEL_IO_MAX.with_board(BOARD_FLIGHT_MAX_PS),
688    );
689    let dv = "verilog/dv/logos_bringup";
690    let spdx = |body: String| format!("{SPDX_V}{body}");
691    vec![
692        (
693            "verilog/rtl/user_defines.v".to_string(),
694            format!("{SCAFFOLD_COPYRIGHT_V}{}", spdx(emit_wpe_user_defines(&WPE_CARAVEL.pins))),
695        ),
696        (format!("{dv}/tb_user_project_wrapper.v"), spdx(tb)),
697        (
698            format!("{dv}/wire_{WPE_CARAVEL_NAME}.hex"),
699            spdx(bytes.iter().map(|b| format!("{b:02x}\n")).collect()),
700        ),
701        (
702            format!("{dv}/lens_{WPE_CARAVEL_NAME}.hex"),
703            spdx(
704                vectors
705                    .iter()
706                    .map(|v| format!("{:08x}\n", v.bytes.len()))
707                    .collect(),
708            ),
709        ),
710        (
711            format!("{dv}/oracle_{WPE_CARAVEL_NAME}.hex"),
712            spdx(
713                vectors
714                    .iter()
715                    .map(|v| format!("{:016x}\n", v.result as u64))
716                    .collect(),
717            ),
718        ),
719    ]
720}
721
722/// The OpenSTA script that closes a PSRAM read through the hardened macro, at the corner `lib` characterizes and
723/// over the parasitics in `spef`: the SCLK edge's latest arrival at its pad-side port, the SIO data constrained to
724/// arrive the read's round trip outside the macro after it ([`WpeCaravel::read_round_trip_ps`]), and the worst
725/// slack of that data into its capture flops at the edge the half SCLK period puts it on. The first two terms are
726/// the macro's own share of the read window, which the harness check alone cannot see. Prints
727/// `SCLK_ARRIVAL_NS <ns>` and `SIO_CAPTURE_SLACK_NS <ns>`.
728pub fn wpe_psram_read_sta_tcl(
729    caravel: &WpeCaravel,
730    memory: &OffchipMemory,
731    lib: &str,
732    netlist: &str,
733    spef: &str,
734) -> String {
735    let p = &caravel.pins;
736    let sio = p
737        .psram_sio
738        .iter()
739        .map(|s| format!("io_in[{s}]"))
740        .collect::<Vec<_>>()
741        .join(" ");
742    let half = memory.edge.half;
743    let multicycle = if half > 1 {
744        format!("set_multicycle_path -setup {half} -from $sio\n")
745    } else {
746        String::new()
747    };
748    format!(
749        "read_liberty {lib}\n\
750         read_verilog {netlist}\n\
751         link_design wpe_user_project\n\
752         read_spef {spef}\n\
753         create_clock -name clk -period {period} [get_ports {clk}]\n\
754         set_propagated_clock [get_clocks clk]\n\
755         set_output_delay 0 -clock clk [get_ports {{io_out[{sclk}]}}]\n\
756         set sclk_arrival [get_property [lindex [get_property [find_timing_paths -to [get_ports {{io_out[{sclk}]}}] -path_delay max] points] end] arrival]\n\
757         set sio [get_ports {{{sio}}}]\n\
758         set_input_delay [expr {{$sclk_arrival + {outside}}}] -clock clk $sio\n\
759         {multicycle}\
760         set slack [get_property [find_timing_paths -from $sio -path_delay max] slack]\n\
761         puts \"SCLK_ARRIVAL_NS [format %.3f $sclk_arrival]\"\n\
762         puts \"SIO_CAPTURE_SLACK_NS [format %.3f $slack]\"\n",
763        period = caravel.clk_period_ps as f64 / 1000.0,
764        clk = WPE_CLOCK.port(),
765        sclk = p.psram_sclk,
766        outside = caravel.read_round_trip_ps(memory) as f64 / 1000.0,
767    )
768}