Skip to main content

logicaffeine_compile/codegen_sva/
psram.rs

1//! The off-chip memory edge for a QSPI PSRAM — the block behind the memory-edge descriptor contract
2//! when the far side is a pseudo-SRAM instead of a DRAM.
3//!
4//! The part is data ([`PsramPart`], its numbers quoted from the datasheet). A transfer descriptor is
5//! split at compile time into bursts the part can legally take ([`plan_stream`]): a burst never
6//! crosses a page, because the part wraps inside a page, and never holds CE# low past tCEM, because
7//! the part refreshes itself only while deselected. [`check_legal`] re-checks a plan independently.
8//! The controller ([`emit_psram_edge_verilog`]) runs the power-up sequence the part requires (tPU,
9//! Reset-Enable + Reset, tRST, enter QPI) and then one QPI burst per request; the pin-level model
10//! ([`emit_psram_model_verilog`]) implements the part's semantics and counts every timing and pin rule
11//! a host breaks, so a co-simulation reports violations rather than assuming there are none.
12
13use super::dram::StreamDesc;
14use std::fmt::Write as _;
15
16/// A QSPI PSRAM, as its datasheet specifies it. Times in picoseconds.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct PsramPart {
19    pub name: &'static str,
20    pub provenance: &'static str,
21    pub capacity_bytes: u32,
22    /// Bursts wrap to the start of the same page.
23    pub page_bytes: u32,
24    pub t_clk_min_ps: u32,
25    pub t_cph_min_ps: u32,
26    pub t_cem_max_ps: u32,
27    pub t_pu_min_ps: u32,
28    pub t_rst_min_ps: u32,
29    pub t_aclk_max_ps: u32,
30    /// QPI Fast Quad Read and its wait cycles.
31    pub read_cmd: u8,
32    pub read_wait: u32,
33    /// QPI Quad Write (no wait cycles).
34    pub write_cmd: u8,
35    /// SPI-mode command that switches the part into QPI.
36    pub enter_qpi: u8,
37    pub reset_enable: u8,
38    pub reset: u8,
39}
40
41/// AP Memory APS6404L-3SQN: 64 Mb (8M x 8) QSPI PSRAM, 3.3 V package, standard temperature grade.
42pub const APS6404L_3SQN: PsramPart = PsramPart {
43    name: "APS6404L-3SQN",
44    provenance: "AP Memory APS6404L-3SQN QSPI PSRAM datasheet, Rev. 2.7 (2024-01-05): §8 power-up \
45                 initialization, §9.1-9.5 address space, page size and command table, §16.7 Table 5 \
46                 READ/WRITE timing (3.3 V package, standard grade)",
47    capacity_bytes: 8 << 20,
48    page_bytes: 1024,
49    t_clk_min_ps: 9_170,
50    t_cph_min_ps: 18_000,
51    t_cem_max_ps: 8_000_000,
52    t_pu_min_ps: 150_000_000,
53    t_rst_min_ps: 50_000,
54    t_aclk_max_ps: 5_500,
55    read_cmd: 0xEB,
56    read_wait: 6,
57    write_cmd: 0x38,
58    enter_qpi: 0x35,
59    reset_enable: 0x66,
60    reset: 0x99,
61};
62
63/// Why an edge or a plan cannot be built for a part.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum PsramUnplaceable {
66    /// The SCLK the system clock divides down to is faster than the part's tCLK.
67    SclkTooFast { sclk_ps: u64, min_ps: u32 },
68    /// Read data is taken at the rising edge half a period after the part drives it at the falling
69    /// edge, so half a period must cover tACLK.
70    ReadWindowTooShort { half_ps: u64, t_aclk_ps: u32 },
71    /// A zero half-period divider.
72    ZeroDivider,
73    /// Only 8-byte words are planned; a stream of any other shape is refused.
74    NotWords {
75        elem_bytes: u64,
76        stride_bytes: u64,
77        base: u64,
78    },
79    /// A cache needs at least two lines to have an index and two words a line to have an offset.
80    CacheShape { index_bits: u32, offset_bits: u32 },
81    /// Index and offset must leave at least one tag bit of the edge's word address.
82    CacheWiderThanAddress {
83        index_bits: u32,
84        offset_bits: u32,
85        addr_bits: u32,
86    },
87    /// A line is filled and written back as one burst, so it must be a burst the part can take.
88    LineBurst { line_words: u32, max_words: u32 },
89    /// The slowest clock is faster than the clock the edge is timed for.
90    SlowestFasterThanFastest { slowest_ps: u32, fastest_ps: u32 },
91    /// At the slowest clock not even a one-word burst ends inside tCEM.
92    NoBurstAtSlowestClock { slowest_ps: u32 },
93}
94
95/// One burst: `words` consecutive 64-bit words from word address `word_addr`, one CE# low.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct PsramBurst {
98    pub write: bool,
99    pub word_addr: u32,
100    pub words: u32,
101}
102
103/// The edge: a part, the system clock that drives the controller, and the number of system cycles
104/// per SCLK half-period. The controller counts cycles, so the rules that set a minimum time — tPU, tRST,
105/// tCPH, tCLK, tACLK — are met for every clock no faster than `sys_period_ps`, while tCEM, which caps how
106/// long a burst may hold CE# low, must hold at the slowest clock the edge is run at, `slowest_period_ps`.
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub struct PsramEdge {
109    pub part: PsramPart,
110    pub sys_period_ps: u32,
111    pub half: u32,
112    pub slowest_period_ps: u32,
113}
114
115/// Command (2 nibbles) and 24-bit address (6 nibbles) edges of a QPI burst.
116const QPI_HEADER_EDGES: u32 = 8;
117/// SCLK edges per 64-bit word: 8 bytes, a nibble per edge.
118const EDGES_PER_WORD: u32 = 16;
119/// Width of the edge's word address (`req_addr`): the 24-bit QPI byte address of an 8 MiB part, less the
120/// three bits inside a word.
121pub const EDGE_WORD_ADDR_BITS: u32 = 20;
122
123impl PsramEdge {
124    pub fn new(part: PsramPart, sys_period_ps: u32, half: u32) -> Result<Self, PsramUnplaceable> {
125        if half == 0 {
126            return Err(PsramUnplaceable::ZeroDivider);
127        }
128        let e = PsramEdge {
129            part,
130            sys_period_ps,
131            half,
132            slowest_period_ps: sys_period_ps,
133        };
134        if e.sclk_period_ps() < part.t_clk_min_ps as u64 {
135            return Err(PsramUnplaceable::SclkTooFast {
136                sclk_ps: e.sclk_period_ps(),
137                min_ps: part.t_clk_min_ps,
138            });
139        }
140        let half_ps = half as u64 * sys_period_ps as u64;
141        if half_ps <= part.t_aclk_max_ps as u64 {
142            return Err(PsramUnplaceable::ReadWindowTooShort {
143                half_ps,
144                t_aclk_ps: part.t_aclk_max_ps,
145            });
146        }
147        Ok(e)
148    }
149
150    /// The same edge, also run at every clock down to `slowest_period_ps`: its bursts shrink to what
151    /// tCEM allows there.
152    pub fn clocked_down_to(self, slowest_period_ps: u32) -> Result<Self, PsramUnplaceable> {
153        if slowest_period_ps < self.sys_period_ps {
154            return Err(PsramUnplaceable::SlowestFasterThanFastest {
155                slowest_ps: slowest_period_ps,
156                fastest_ps: self.sys_period_ps,
157            });
158        }
159        let e = PsramEdge {
160            slowest_period_ps,
161            ..self
162        };
163        if e.max_words(false) == 0 || e.max_words(true) == 0 {
164            return Err(PsramUnplaceable::NoBurstAtSlowestClock {
165                slowest_ps: slowest_period_ps,
166            });
167        }
168        Ok(e)
169    }
170
171    pub fn sclk_period_ps(&self) -> u64 {
172        2 * self.half as u64 * self.sys_period_ps as u64
173    }
174
175    fn cycles_for(&self, ps: u32) -> u32 {
176        (ps as u64).div_ceil(self.sys_period_ps as u64) as u32
177    }
178
179    /// System cycles the controller waits after reset release before its first command (tPU).
180    pub fn powerup_cycles(&self) -> u32 {
181        self.cycles_for(self.part.t_pu_min_ps)
182    }
183
184    /// System cycles CE# stays high after a burst (tCPH).
185    pub fn cph_cycles(&self) -> u32 {
186        self.cycles_for(self.part.t_cph_min_ps)
187    }
188
189    /// System cycles CE# stays high after the Reset command (tRST).
190    pub fn rst_cycles(&self) -> u32 {
191        self.cycles_for(self.part.t_rst_min_ps)
192    }
193
194    /// Rising edges from reset release until `req_ready` first rises: tPU, then Reset-Enable, Reset and
195    /// Enter-QPI as eight-edge SPI bytes, with CE# high for tCPH, tRST and tCPH after them.
196    pub fn init_cycles(&self) -> u64 {
197        let gap = |c: u32| c.max(1) as u64;
198        self.powerup_cycles() as u64
199            + 3 * 8 * 2 * self.half as u64
200            + gap(self.cph_cycles())
201            + gap(self.rst_cycles())
202            + gap(self.cph_cycles())
203    }
204
205    /// SCLK edges of a burst of `words` words.
206    pub fn edges(&self, write: bool, words: u32) -> u32 {
207        QPI_HEADER_EDGES + if write { 0 } else { self.part.read_wait } + EDGES_PER_WORD * words
208    }
209
210    /// How long CE# is low for a burst at the slowest clock: every SCLK edge, from CE# falling with the
211    /// first output to CE# rising with the last falling edge.
212    pub fn ce_low_ps(&self, write: bool, words: u32) -> u64 {
213        self.edges(write, words) as u64 * 2 * self.half as u64 * self.slowest_period_ps as u64
214    }
215
216    /// The longest burst the part can take: inside tCEM, and no longer than a page.
217    pub fn max_words(&self, write: bool) -> u32 {
218        let page_words = self.part.page_bytes / 8;
219        (1..=page_words)
220            .take_while(|&n| self.ce_low_ps(write, n) <= self.part.t_cem_max_ps as u64)
221            .last()
222            .unwrap_or(0)
223    }
224
225    /// System cycles from the request being accepted to the controller accepting the next one: the
226    /// burst's SCLK edges, then CE# high for tCPH (at least one cycle).
227    pub fn burst_sys_cycles(&self, write: bool, words: u32) -> u64 {
228        self.edges(write, words) as u64 * 2 * self.half as u64 + self.cph_cycles().max(1) as u64
229    }
230}
231
232/// Split a stream of 8-byte words into bursts the part can take: at every page boundary and at the
233/// longest burst tCEM allows. A strided stream is one burst per word.
234pub fn plan_stream(edge: &PsramEdge, s: &StreamDesc) -> Result<Vec<PsramBurst>, PsramUnplaceable> {
235    if s.elem_bytes != 8 || s.base % 8 != 0 || s.stride_bytes % 8 != 0 || s.stride_bytes == 0 {
236        return Err(PsramUnplaceable::NotWords {
237            elem_bytes: s.elem_bytes,
238            stride_bytes: s.stride_bytes,
239            base: s.base,
240        });
241    }
242    let page_words = edge.part.page_bytes / 8;
243    let max = edge.max_words(s.write);
244    let mut out = Vec::new();
245    if s.stride_bytes != 8 {
246        for k in 0..s.count {
247            out.push(PsramBurst {
248                write: s.write,
249                word_addr: ((s.base + k * s.stride_bytes) / 8) as u32,
250                words: 1,
251            });
252        }
253        return Ok(out);
254    }
255    let mut w = (s.base / 8) as u32;
256    let mut left = s.count as u32;
257    while left > 0 {
258        let n = left.min(max).min(page_words - w % page_words);
259        out.push(PsramBurst {
260            write: s.write,
261            word_addr: w,
262            words: n,
263        });
264        w += n;
265        left -= n;
266    }
267    Ok(out)
268}
269
270/// Every rule a burst plan must satisfy, checked independently of the planner.
271pub fn check_legal(edge: &PsramEdge, plan: &[PsramBurst]) -> Result<(), String> {
272    let page_words = edge.part.page_bytes / 8;
273    let words_total = edge.part.capacity_bytes / 8;
274    for (i, b) in plan.iter().enumerate() {
275        if b.words == 0 {
276            return Err(format!("burst {i}: empty"));
277        }
278        if b.word_addr as u64 + b.words as u64 > words_total as u64 {
279            return Err(format!(
280                "burst {i}: words {}..{} past the part's {words_total}",
281                b.word_addr,
282                b.word_addr + b.words
283            ));
284        }
285        if b.word_addr % page_words + b.words > page_words {
286            return Err(format!(
287                "burst {i}: word {} + {} crosses a {}-byte page",
288                b.word_addr, b.words, edge.part.page_bytes
289            ));
290        }
291        let low = edge.ce_low_ps(b.write, b.words);
292        if low > edge.part.t_cem_max_ps as u64 {
293            return Err(format!(
294                "burst {i}: CE# low {low} ps exceeds tCEM {} ps",
295                edge.part.t_cem_max_ps
296            ));
297        }
298    }
299    Ok(())
300}
301
302/// The controller. One request is one burst: `req_len` words at word address `req_addr`. A write
303/// takes `wdata` at the start of each word (pulsing `wdata_take`); a read returns each word on
304/// `rdata` with a one-cycle `rdata_valid`. `req_ready` is high whenever a request can be taken.
305pub fn emit_psram_edge_verilog(edge: &PsramEdge) -> String {
306    let p = &edge.part;
307    let [c_hi, c_lo] = [p.read_cmd >> 4, p.read_cmd & 15];
308    let [w_hi, w_lo] = [p.write_cmd >> 4, p.write_cmd & 15];
309    let data_read = QPI_HEADER_EDGES + p.read_wait;
310    format!(
311        "// psram_edge — the off-chip memory edge for the {name} QSPI PSRAM (codegen_sva::psram).\n\
312         // Power-up: tPU with CLK low and CE# high, Reset-Enable 0x{rsten:02x} + Reset 0x{rst:02x} in SPI, tRST,\n\
313         // then 0x{qpi:02x} into QPI. A request is one QPI burst: 0x{rd:02x} (wait {wait}) to read, 0x{wr:02x} to write,\n\
314         // a 24-bit byte address, 16 nibbles per 64-bit word (byte 0 first, high nibble first). Outputs\n\
315         // change at SCLK's falling edge; the part samples at its rising edge, and read data is taken at\n\
316         // the rising edge. Emitted by `cargo run -p logicaffeine-compile --example emit_psram_edge`.\n\
317         module psram_edge #(\n\
318         \x20 parameter integer H = {half},\n\
319         \x20 parameter integer POWERUP = {pu},\n\
320         \x20 parameter integer CPH = {cph},\n\
321         \x20 parameter integer RSTGAP = {rstgap}\n\
322         ) (\n\
323         \x20 input clk, input rst,\n\
324         \x20 input req_valid, output req_ready, input req_we, input [{am}:0] req_addr, input [7:0] req_len,\n\
325         \x20 input [63:0] wdata, output reg wdata_take,\n\
326         \x20 output reg [63:0] rdata, output reg rdata_valid,\n\
327         \x20 output reg ce_n, output reg sclk, output reg [3:0] sio_out, output reg [3:0] sio_oe, input [3:0] sio_in\n\
328         );\n\
329         \x20 localparam S_PU = 2'd0, S_OP = 2'd1, S_GAP = 2'd2, S_IDLE = 2'd3;\n\
330         \x20 localparam O_SPI = 2'd0, O_READ = 2'd1, O_WRITE = 2'd2;\n\
331         \x20 reg [1:0] state, op, init;\n\
332         \x20 reg [7:0] spi_byte;\n\
333         \x20 reg [23:0] baddr;\n\
334         \x20 reg [11:0] ei, nei;\n\
335         \x20 reg [7:0] phase;\n\
336         \x20 reg [31:0] count;\n\
337         \x20 reg [63:0] wbuf, rbuf;\n\
338         \x20 assign req_ready = (state == S_IDLE);\n\
339         \n\
340         \x20 // The nibble to present on edge `e` of the current operation, and whether to drive it.\n\
341         \x20 function [4:0] nibble(input [1:0] o, input [11:0] e, input [7:0] sb, input [23:0] a, input [63:0] w);\n\
342         \x20   reg [11:0] d; reg [3:0] j;\n\
343         \x20   begin\n\
344         \x20     if (o == O_SPI) nibble = {{1'b1, 3'b000, sb[3'd7 - e[2:0]]}};\n\
345         \x20     else if (e == 12'd0) nibble = {{1'b1, (o == O_READ) ? 4'h{c_hi:x} : 4'h{w_hi:x}}};\n\
346         \x20     else if (e == 12'd1) nibble = {{1'b1, (o == O_READ) ? 4'h{c_lo:x} : 4'h{w_lo:x}}};\n\
347         \x20     else if (e < 12'd{hdr}) nibble = {{1'b1, a[5'd23 - 5'd4 * (e[4:0] - 5'd2) -: 4]}};\n\
348         \x20     else if (o == O_READ) nibble = 5'b0_0000;\n\
349         \x20     else begin\n\
350         \x20       d = e - 12'd{hdr}; j = d[3:0];\n\
351         \x20       nibble = {{1'b1, w[8 * j[3:1] + 7 - 4 * j[0] -: 4]}};\n\
352         \x20     end\n\
353         \x20   end\n\
354         \x20 endfunction\n\
355         \n\
356         \x20 // Begin operation `o`: CE# falls with the first edge's output.\n\
357         \x20 task start(input [1:0] o, input [7:0] sb);\n\
358         \x20   reg [4:0] n;\n\
359         \x20   begin\n\
360         \x20     op <= o; spi_byte <= sb; ei <= 12'd0; phase <= 8'd0; state <= S_OP;\n\
361         \x20     ce_n <= 1'b0; sclk <= 1'b0;\n\
362         \x20     nei <= (o == O_SPI) ? 12'd8 : (o == O_READ) ? 12'd{data_read} + {{req_len, 4'b0000}} : 12'd{hdr} + {{req_len, 4'b0000}};\n\
363         \x20     n = nibble(o, 12'd0, sb, (o == O_SPI) ? 24'd0 : {{1'b0, req_addr, 3'b000}}, wdata);\n\
364         \x20     sio_oe <= (o == O_SPI) ? 4'b0001 : {{4{{n[4]}}}}; sio_out <= n[3:0];\n\
365         \x20   end\n\
366         \x20 endtask\n\
367         \n\
368         \x20 reg [4:0] nx;\n\
369         \x20 reg [11:0] dn;\n\
370         \x20 reg [11:0] ds;\n\
371         \x20 always @(posedge clk) begin\n\
372         \x20   wdata_take <= 1'b0; rdata_valid <= 1'b0;\n\
373         \x20   if (rst) begin\n\
374         \x20     state <= S_PU; count <= 32'd0; init <= 2'd0;\n\
375         \x20     ce_n <= 1'b1; sclk <= 1'b0; sio_oe <= 4'b1111; sio_out <= 4'b0000;\n\
376         \x20   end else case (state)\n\
377         \x20     S_PU: if (count >= POWERUP - 1) start(O_SPI, 8'h{rsten:02x}); else count <= count + 32'd1;\n\
378         \x20     S_IDLE: if (req_valid) begin\n\
379         \x20       baddr <= {{1'b0, req_addr, 3'b000}};\n\
380         \x20       start(req_we ? O_WRITE : O_READ, 8'h00);\n\
381         \x20     end\n\
382         \x20     S_GAP: if (count <= 32'd1) begin\n\
383         \x20       case (init)\n\
384         \x20         2'd1: start(O_SPI, 8'h{rst:02x});\n\
385         \x20         2'd2: start(O_SPI, 8'h{qpi:02x});\n\
386         \x20         default: state <= S_IDLE;\n\
387         \x20       endcase\n\
388         \x20     end else count <= count - 32'd1;\n\
389         \x20     S_OP: begin\n\
390         \x20       if (phase == H - 1) begin\n\
391         \x20         sclk <= 1'b1;\n\
392         \x20         if (op == O_READ && ei >= 12'd{data_read}) begin\n\
393         \x20           ds = ei - 12'd{data_read};\n\
394         \x20           if (ds[3:0] == 4'd15) begin\n\
395         \x20             rdata <= {{rbuf[63:60], sio_in, rbuf[55:0]}}; rdata_valid <= 1'b1;\n\
396         \x20           end else rbuf[8 * ds[3:1] + 7 - 4 * ds[0] -: 4] <= sio_in;\n\
397         \x20         end\n\
398         \x20       end\n\
399         \x20       if (phase == 2 * H - 1) begin\n\
400         \x20         sclk <= 1'b0; phase <= 8'd0;\n\
401         \x20         if (ei == nei - 12'd1) begin\n\
402         \x20           ce_n <= 1'b1; sio_oe <= 4'b0000; state <= S_GAP;\n\
403         \x20           if (op == O_SPI) begin\n\
404         \x20             init <= init + 2'd1;\n\
405         \x20             count <= (init == 2'd1) ? RSTGAP : CPH;\n\
406         \x20           end else count <= CPH;\n\
407         \x20         end else begin\n\
408         \x20           ei <= ei + 12'd1;\n\
409         \x20           dn = ei + 12'd1 - 12'd{hdr};\n\
410         \x20           if (op == O_WRITE && ei + 12'd1 >= 12'd{hdr} && dn[3:0] == 4'd0) begin\n\
411         \x20             wbuf <= wdata; wdata_take <= 1'b1;\n\
412         \x20             nx = nibble(op, ei + 12'd1, spi_byte, baddr, wdata);\n\
413         \x20           end else nx = nibble(op, ei + 12'd1, spi_byte, baddr, wbuf);\n\
414         \x20           sio_oe <= (op == O_SPI) ? 4'b0001 : {{4{{nx[4]}}}}; sio_out <= nx[3:0];\n\
415         \x20         end\n\
416         \x20       end else phase <= phase + 8'd1;\n\
417         \x20     end\n\
418         \x20   endcase\n\
419         \x20 end\n\
420         endmodule\n",
421        name = p.name,
422        rsten = p.reset_enable,
423        rst = p.reset,
424        qpi = p.enter_qpi,
425        rd = p.read_cmd,
426        wr = p.write_cmd,
427        wait = p.read_wait,
428        half = edge.half,
429        pu = edge.powerup_cycles(),
430        cph = edge.cph_cycles(),
431        rstgap = edge.rst_cycles(),
432        hdr = QPI_HEADER_EDGES,
433        am = EDGE_WORD_ADDR_BITS - 1,
434    )
435}
436
437/// The heap cache in front of the edge: direct-mapped, write-back and write-allocate, `2^index_bits` lines
438/// of `2^offset_bits` words, each line filled and written back as one burst.
439#[derive(Clone, Copy, Debug, PartialEq, Eq)]
440pub struct PsramCache {
441    pub index_bits: u32,
442    pub offset_bits: u32,
443}
444
445impl PsramCache {
446    pub fn new(
447        edge: &PsramEdge,
448        index_bits: u32,
449        offset_bits: u32,
450    ) -> Result<Self, PsramUnplaceable> {
451        if index_bits == 0 || offset_bits == 0 {
452            return Err(PsramUnplaceable::CacheShape {
453                index_bits,
454                offset_bits,
455            });
456        }
457        if index_bits + offset_bits >= EDGE_WORD_ADDR_BITS {
458            return Err(PsramUnplaceable::CacheWiderThanAddress {
459                index_bits,
460                offset_bits,
461                addr_bits: EDGE_WORD_ADDR_BITS,
462            });
463        }
464        let line_words = 1u32 << offset_bits;
465        let max_words = edge
466            .max_words(true)
467            .min(edge.max_words(false))
468            .min(u8::MAX as u32);
469        if line_words > max_words {
470            return Err(PsramUnplaceable::LineBurst {
471                line_words,
472                max_words,
473            });
474        }
475        Ok(PsramCache {
476            index_bits,
477            offset_bits,
478        })
479    }
480
481    pub fn lines(&self) -> u32 {
482        1 << self.index_bits
483    }
484
485    pub fn line_words(&self) -> u32 {
486        1 << self.offset_bits
487    }
488
489    pub fn words(&self) -> u32 {
490        self.lines() * self.line_words()
491    }
492
493    /// Cycles `busy` holds for a miss whose victim is dirty: the request's own cycle, then for the
494    /// write-back and for the fill the cycle the edge accepts it, its burst, and the cycle that sees
495    /// `req_ready` again.
496    pub fn miss_stall_cycles(&self, edge: &PsramEdge) -> u64 {
497        1 + 2 * 2
498            + edge.burst_sys_cycles(true, self.line_words())
499            + edge.burst_sys_cycles(false, self.line_words())
500    }
501
502    /// The longest stall: a write and a read issued together, each missing with a dirty victim.
503    pub fn worst_stall_cycles(&self, edge: &PsramEdge) -> u64 {
504        2 * self.miss_stall_cycles(edge)
505    }
506}
507
508/// The cache. Its engine side is a registered-read memory port: `ra` holds the read address and `rd` is
509/// `data[ra]` one cycle later, refreshed every cycle, so a resident line reads exactly as a block RAM does;
510/// `req_r`/`req_w` pulse the cycle after the engine issues a read or a write, whose address, data and
511/// half-word enables `wm` hold until its next one. `busy` is high while a miss is served — a dirty victim
512/// written back, then the line filled, with a write miss merging its enabled halves into the fill — and
513/// while a read issued together with a write waits for it.
514///
515/// `busy` freezes the whole engine, so it takes no tag lookup of `ra` or `wa`: `rhit`/`whit` are their hits, which
516/// the engine registers beside each address as it sets it, looking the new address up in the lines as they will
517/// hold after that edge ([`emit_psram_cache_hit_function`]) — the same value, a cycle earlier, so the engine stalls in
518/// exactly the cycles it did. The cache exports what that lookup reads (`tags`, `vals`, and the line a fill installs at
519/// this edge: `filled`, `fidx`, `ftag`); in simulation it counts every cycle a registered hit disagrees with its lines
520/// (`hit_mismatches`, a `PSRAM-CACHE-HIT-MISMATCH` line each).
521pub fn emit_psram_cache_verilog(cache: &PsramCache) -> String {
522    let tw = EDGE_WORD_ADDR_BITS - cache.index_bits - cache.offset_bits;
523    format!(
524        "// psram_cache — {lines} lines of {words} 64-bit words, direct-mapped, write-back, in front of psram_edge\n\
525         // (codegen_sva::psram).\n\
526         module psram_cache #(parameter integer IW = {iw}, parameter integer OW = {ow}) (\n\
527         \x20 input clk, input rst,\n\
528         \x20 input req_r, input [{am}:0] ra, input rhit,\n\
529         \x20 input req_w, input [{am}:0] wa, input whit, input [63:0] wd, input [1:0] wm,\n\
530         \x20 output [63:0] rd,\n\
531         \x20 output busy,\n\
532         \x20 output [{tags_top}:0] tags, output [{lines_top}:0] vals, output filled, output [{iw_top}:0] fidx,\n\
533         \x20 output [{tw_top}:0] ftag,\n\
534         \x20 output e_valid, input e_ready, output e_we, output [{am}:0] e_addr, output [7:0] e_len,\n\
535         \x20 output [63:0] e_wdata, input e_wdata_take, input [63:0] e_rdata, input e_rdata_valid\n\
536         );\n\
537         \x20 localparam integer LINES = 1 << IW;\n\
538         \x20 localparam integer TW = {ab} - IW - OW;\n\
539         \x20 localparam [2:0] C_IDLE = 3'd0, C_PEND = 3'd1, C_WBQ = 3'd2, C_WB = 3'd3, C_FQ = 3'd4, C_FL = 3'd5;\n\
540         \x20 reg [2:0] cs;\n\
541         \x20 reg [TW-1:0] ctag [0:LINES-1];\n\
542         \x20 reg [LINES-1:0] cval, cdirty;\n\
543         \x20 reg [31:0] cd_lo [0:(LINES << OW)-1];\n\
544         \x20 reg [31:0] cd_hi [0:(LINES << OW)-1];\n\
545         \x20 reg [63:0] cr_q;\n\
546         \x20 reg mis_we, pr;\n\
547         \x20 reg [OW-1:0] wcnt, rcnt;\n\
548         \n\
549         \x20 wire [IW-1:0] ri = ra[OW +: IW];\n\
550         \x20 wire [IW-1:0] wi = wa[OW +: IW];\n\
551         \x20 wire hit_r = rhit;\n\
552         \x20 wire hit_w = whit;\n\
553         \x20 wire [{am}:0] ma = mis_we ? wa : ra;\n\
554         \x20 wire [IW-1:0] mi = ma[OW +: IW];\n\
555         \n\
556         \x20 genvar g;\n\
557         \x20 generate for (g = 0; g < LINES; g = g + 1) begin : tag_out\n\
558         \x20   assign tags[g * TW +: TW] = ctag[g];\n\
559         \x20 end endgenerate\n\
560         \x20 assign vals = cval;\n\
561         \x20 assign filled = (cs == C_FL) && e_ready;\n\
562         \x20 assign fidx = mi;\n\
563         \x20 assign ftag = ma[{am} -: TW];\n\
564         \n\
565         `ifndef SYNTHESIS\n\
566         \x20 integer hit_mismatches = 0;\n\
567         \x20 always @(posedge clk) if (!rst) begin\n\
568         \x20   if ((^ra) !== 1'bx && rhit !== (cval[ri] && ctag[ri] == ra[{am} -: TW])) begin\n\
569         \x20     hit_mismatches = hit_mismatches + 1; $display(\"PSRAM-CACHE-HIT-MISMATCH read %m %0t\", $time);\n\
570         \x20   end\n\
571         \x20   if ((^wa) !== 1'bx && whit !== (cval[wi] && ctag[wi] == wa[{am} -: TW])) begin\n\
572         \x20     hit_mismatches = hit_mismatches + 1; $display(\"PSRAM-CACHE-HIT-MISMATCH write %m %0t\", $time);\n\
573         \x20   end\n\
574         \x20 end\n\
575         `endif\n\
576         \n\
577         \x20 assign busy = (cs != C_IDLE) | (req_w & ~hit_w) | (req_r & ~hit_r) | (req_w & req_r);\n\
578         \x20 assign rd = cr_q;\n\
579         \x20 assign e_wdata = cr_q;\n\
580         \x20 assign e_valid = (cs == C_WBQ) | (cs == C_FQ);\n\
581         \x20 assign e_we = (cs == C_WBQ);\n\
582         \x20 assign e_addr = (cs == C_WBQ) ? {{ctag[mi], mi, {{OW{{1'b0}}}}}} : {{ma[{am}:OW], {{OW{{1'b0}}}}}};\n\
583         \x20 assign e_len = 8'd1 << OW;\n\
584         \n\
585         \x20 // One read port and one write port on the line store. The read address is the engine's, except\n\
586         \x20 // while a victim streams out; the write port takes an engine write hit or a filled word.\n\
587         \x20 wire [IW+OW-1:0] cr_a = (cs == C_WBQ || cs == C_WB) ? {{mi, wcnt}} : {{ri, ra[OW-1:0]}};\n\
588         \x20 wire hit_wr = cs == C_IDLE && req_w && hit_w;\n\
589         \x20 wire fill = cs == C_FL && e_rdata_valid;\n\
590         \x20 wire merge = fill && mis_we && rcnt == wa[OW-1:0];\n\
591         \x20 wire [IW+OW-1:0] dwa = (cs == C_FL) ? {{mi, rcnt}} : {{wi, wa[OW-1:0]}};\n\
592         \x20 wire [31:0] dwd_lo = (fill && !(merge && wm[0])) ? e_rdata[31:0] : wd[31:0];\n\
593         \x20 wire [31:0] dwd_hi = (fill && !(merge && wm[1])) ? e_rdata[63:32] : wd[63:32];\n\
594         \n\
595         \x20 always @(posedge clk) begin\n\
596         \x20   cr_q <= {{cd_hi[cr_a], cd_lo[cr_a]}};\n\
597         \x20   if (fill || (hit_wr && wm[0])) cd_lo[dwa] <= dwd_lo;\n\
598         \x20   if (fill || (hit_wr && wm[1])) cd_hi[dwa] <= dwd_hi;\n\
599         \x20 end\n\
600         \n\
601         \x20 task miss(input we, input [IW-1:0] idx);\n\
602         \x20   begin\n\
603         \x20     mis_we <= we; rcnt <= {{OW{{1'b0}}}}; wcnt <= {{OW{{1'b0}}}};\n\
604         \x20     cs <= (cval[idx] && cdirty[idx]) ? C_WBQ : C_FQ;\n\
605         \x20   end\n\
606         \x20 endtask\n\
607         \n\
608         \x20 always @(posedge clk) begin\n\
609         \x20   if (rst) begin\n\
610         \x20     cs <= C_IDLE; cval <= {{LINES{{1'b0}}}}; cdirty <= {{LINES{{1'b0}}}}; mis_we <= 1'b0; pr <= 1'b0;\n\
611         \x20   end else case (cs)\n\
612         \x20     C_IDLE: if (req_w) begin\n\
613         \x20               if (hit_w) begin cdirty[wi] <= 1'b1; if (req_r) cs <= C_PEND; end\n\
614         \x20               else begin pr <= req_r; miss(1'b1, wi); end\n\
615         \x20             end else if (req_r && !hit_r) miss(1'b0, ri);\n\
616         \x20     C_PEND: begin pr <= 1'b0; if (hit_r) cs <= C_IDLE; else miss(1'b0, ri); end\n\
617         \x20     C_WBQ:  if (e_ready) cs <= C_WB;\n\
618         \x20     C_WB:   begin if (e_wdata_take) wcnt <= wcnt + 1'b1; if (e_ready) cs <= C_FQ; end\n\
619         \x20     C_FQ:   if (e_ready) cs <= C_FL;\n\
620         \x20     C_FL:   begin\n\
621         \x20               if (e_rdata_valid) rcnt <= rcnt + 1'b1;\n\
622         \x20               if (e_ready) begin\n\
623         \x20                 ctag[mi] <= ma[{am} -: TW]; cval[mi] <= 1'b1; cdirty[mi] <= mis_we;\n\
624         \x20                 cs <= pr ? C_PEND : C_IDLE;\n\
625         \x20               end\n\
626         \x20             end\n\
627         \x20     default: cs <= C_IDLE;\n\
628         \x20   endcase\n\
629         \x20 end\n\
630         endmodule\n",
631        lines = cache.lines(),
632        words = cache.line_words(),
633        iw = cache.index_bits,
634        ow = cache.offset_bits,
635        ab = EDGE_WORD_ADDR_BITS,
636        am = EDGE_WORD_ADDR_BITS - 1,
637        tags_top = cache.lines() * tw - 1,
638        lines_top = cache.lines() - 1,
639        iw_top = cache.index_bits - 1,
640        tw_top = tw - 1,
641    )
642}
643
644/// The engine-side lookup of a cache's registered hits: `{inst}_hit(a)`, the hit of address `a` in the cache
645/// instance `inst`'s lines as they will hold after this edge — none after a reset, and a fill completing now holding the
646/// line it fills — read from the instance's exported `{inst}_tags`, `{inst}_vals`, `{inst}_filled`, `{inst}_fidx` and
647/// `{inst}_ftag`. Registering `{inst}_hit(a)` beside an address `a` the engine sets, and `{inst}_hit` of the held
648/// address every other cycle, gives the cache exactly the hit it would have looked up itself, a cycle earlier.
649pub fn emit_psram_cache_hit_function(inst: &str, cache: &PsramCache) -> String {
650    let am = EDGE_WORD_ADDR_BITS - 1;
651    let state = |s: &str| format!("{inst}_{s}");
652    psram_cache_hit_lookup(
653        &format!("{inst}_hit"),
654        &format!("input [{am}:0] a"),
655        cache,
656        [state("tags"), state("vals"), state("filled"), state("fidx"), state("ftag")],
657        "rst",
658    )
659}
660
661/// The lookup [`emit_psram_cache_hit_function`] names, as a function `name(args)` whose last argument is the address
662/// `a`, reading a cache's exported `[tags, vals, filled, fidx, ftag]` through the expressions given for them and its
663/// reset through `reset`.
664fn psram_cache_hit_lookup(name: &str, args: &str, cache: &PsramCache, state: [String; 5], reset: &str) -> String {
665    let (iw, ow) = (cache.index_bits, cache.offset_bits);
666    let tw = EDGE_WORD_ADDR_BITS - iw - ow;
667    let am = EDGE_WORD_ADDR_BITS - 1;
668    let [tags, vals, filled, fidx, ftag] = state;
669    format!(
670        "  function {name}({args});\n\
671         \x20   begin\n\
672         \x20     if ({reset}) {name} = 1'b0;\n\
673         \x20     else if ({filled} && {fidx} == a[{ow} +: {iw}]) {name} = {ftag} == a[{am} -: {tw}];\n\
674         \x20     else {name} = {vals}[a[{ow} +: {iw}]] && {tags}[a[{ow} +: {iw}] * {tw} +: {tw}] == a[{am} -: {tw}];\n\
675         \x20   end\n\
676         \x20 endfunction\n"
677    )
678}
679
680/// The wires a cache instance `inst` exports to its engine-side lookup ([`emit_psram_cache_hit_function`]), declared.
681pub fn emit_psram_cache_hit_wires(inst: &str, cache: &PsramCache) -> String {
682    let tw = EDGE_WORD_ADDR_BITS - cache.index_bits - cache.offset_bits;
683    format!(
684        "  wire [{}:0] {inst}_tags; wire [{}:0] {inst}_vals; wire {inst}_filled; wire [{}:0] {inst}_fidx; wire [{}:0] {inst}_ftag;\n",
685        cache.lines() * tw - 1,
686        cache.lines() - 1,
687        cache.index_bits - 1,
688        tw - 1
689    )
690}
691
692/// The port connections of a cache instance `inst` whose engine registers its hits in `rhit` and `whit`.
693pub fn psram_cache_hit_ports(inst: &str, rhit: &str, whit: &str) -> String {
694    format!(
695        ".rhit({rhit}), .whit({whit}), .tags({inst}_tags), .vals({inst}_vals), .filled({inst}_filled), \
696         .fidx({inst}_fidx), .ftag({inst}_ftag)"
697    )
698}
699
700/// A pin-level model of the part: its semantics (SPI until Enter-QPI, Reset-Enable + Reset before
701/// use, bursts wrapping inside a page) and every rule it checks — the timing rules, an SIO bit the part
702/// samples that nobody drives (`float`), and a host driving against the part's read data (`contend`) —
703/// each counted by name on a `PSRAM-VIOLATION <rule>` line. `violations` holds the total.
704pub fn emit_psram_model_verilog(p: &PsramPart) -> String {
705    let ns = |ps: u32| format!("{:.3}", ps as f64 / 1000.0);
706    let addr_bits = p.capacity_bytes.trailing_zeros();
707    let page_bits = p.page_bytes.trailing_zeros();
708    let data_read = QPI_HEADER_EDGES + p.read_wait;
709    format!(
710        "// psram_model — a pin-level model of the {name} ({prov}).\n\
711         // Emitted by `cargo run -p logicaffeine-compile --example emit_psram_edge` (codegen_sva::psram).\n\
712         `timescale 1ns/1ps\n\
713         module psram_model (input ce_n, input sclk, inout [3:0] sio);\n\
714         \x20 localparam real T_PU = {t_pu}, T_CEM = {t_cem}, T_CPH = {t_cph}, T_RST = {t_rst}, T_CLK = {t_clk}, T_ACLK = {t_aclk};\n\
715         \x20 reg [7:0] mem [0:{top}];\n\
716         \x20 reg qpi = 1'b0, armed = 1'b0, ready = 1'b0, wrapped = 1'b0, drive = 1'b0;\n\
717         \x20 reg [3:0] dout = 4'b0000;\n\
718         \x20 assign sio = drive ? dout : 4'bzzzz;\n\
719         \x20 integer violations = 0, nedge = 0;\n\
720         \x20 reg [7:0] cmd, wbyte;\n\
721         \x20 reg [23:0] addr;\n\
722         \x20 realtime t_fall = 0.0, t_rise = -1.0e9, t_rst_end = -1.0e9, t_last = 0.0;\n\
723         \n\
724         \x20 // An SIO bit nobody drives resolves to z; a driven unknown value is x, and only the first is a fault.\n\
725         \x20 function floating(input [3:0] v);\n\
726         \x20   floating = (v[0] === 1'bz) || (v[1] === 1'bz) || (v[2] === 1'bz) || (v[3] === 1'bz);\n\
727         \x20 endfunction\n\
728         \x20 task violation(input [8*8-1:0] rule);\n\
729         \x20   begin $display(\"PSRAM-VIOLATION %0s %0t\", rule, $realtime); violations = violations + 1; end\n\
730         \x20 endtask\n\
731         \n\
732         \x20 // The byte `k` bytes into the burst: a burst wraps to the start of its page.\n\
733         \x20 function [{abits_m1}:0] at(input [23:0] a, input integer k);\n\
734         \x20   reg [{pbits}:0] off;\n\
735         \x20   begin off = a[{pbits_m1}:0] + k; at = {{a[{abits_m1}:{pbits}], off[{pbits_m1}:0]}}; end\n\
736         \x20 endfunction\n\
737         \x20 task note_wrap(input [23:0] a, input integer k);\n\
738         \x20   begin if (!wrapped && a[{pbits_m1}:0] + k >= {page}) begin wrapped = 1'b1; violation(\"wrap\"); end end\n\
739         \x20 endtask\n\
740         \n\
741         \x20 always @(negedge ce_n) begin\n\
742         \x20   if ($realtime < T_PU) violation(\"tPU\");\n\
743         \x20   if ($realtime - t_rise < T_CPH) violation(\"tCPH\");\n\
744         \x20   if ($realtime - t_rst_end < T_RST) violation(\"tRST\");\n\
745         \x20   t_rst_end = -1.0e9;\n\
746         \x20   t_fall = $realtime; nedge = 0; cmd = 8'h00; addr = 24'h0; wrapped = 1'b0;\n\
747         \x20 end\n\
748         \n\
749         \x20 always @(posedge ce_n) begin\n\
750         \x20   if ($realtime - t_fall > T_CEM) violation(\"tCEM\");\n\
751         \x20   t_rise = $realtime; drive = 1'b0;\n\
752         \x20   if (!qpi && nedge == 8) begin\n\
753         \x20     if (cmd == 8'h{rst:02x} && armed) begin ready = 1'b1; qpi = 1'b0; t_rst_end = $realtime; end\n\
754         \x20     else if (cmd == 8'h{qpi:02x} && ready) qpi = 1'b1;\n\
755         \x20     armed = (cmd == 8'h{rsten:02x});\n\
756         \x20   end else if (qpi && nedge == 2) begin\n\
757         \x20     if (cmd == 8'h{rst:02x} && armed) begin ready = 1'b1; qpi = 1'b0; t_rst_end = $realtime; end\n\
758         \x20     armed = (cmd == 8'h{rsten:02x});\n\
759         \x20   end else armed = 1'b0;\n\
760         \x20 end\n\
761         \n\
762         \x20 always @(posedge sclk) begin\n\
763         \x20   if ($realtime < T_PU) violation(\"tPU\");\n\
764         \x20   if (!ce_n) begin\n\
765         \x20     if (nedge > 0 && $realtime - t_last < T_CLK) violation(\"tCLK\");\n\
766         \x20     t_last = $realtime;\n\
767         \x20     if (!qpi && nedge < 8) begin\n\
768         \x20       if (sio[0] === 1'bz) violation(\"float\");\n\
769         \x20       cmd = {{cmd[6:0], sio[0]}};\n\
770         \x20       if (nedge == 7 && !(cmd == 8'h{rsten:02x} || cmd == 8'h{rst:02x} || (cmd == 8'h{qpi:02x} && ready)))\n\
771         \x20         violation(ready ? \"cmd\" : \"init\");\n\
772         \x20     end else if (qpi && nedge < 2) begin\n\
773         \x20       if (floating(sio)) violation(\"float\");\n\
774         \x20       cmd = {{cmd[3:0], sio}};\n\
775         \x20       if (nedge == 1 && cmd != 8'h{rd:02x} && cmd != 8'h{wr:02x} && cmd != 8'h02 && cmd != 8'h{rsten:02x} && cmd != 8'h{rst:02x})\n\
776         \x20         violation(\"cmd\");\n\
777         \x20     end else if (qpi) begin\n\
778         \x20       if (nedge < {hdr}) begin\n\
779         \x20         if (floating(sio)) violation(\"float\");\n\
780         \x20         addr = {{addr[19:0], sio}};\n\
781         \x20       end else if (cmd == 8'h{wr:02x} || cmd == 8'h02) begin\n\
782         \x20         if (floating(sio)) violation(\"float\");\n\
783         \x20         if ((nedge - {hdr}) % 2 == 0) wbyte[7:4] = sio;\n\
784         \x20         else begin\n\
785         \x20           wbyte[3:0] = sio;\n\
786         \x20           note_wrap(addr, (nedge - {hdr}) / 2);\n\
787         \x20           mem[at(addr, (nedge - {hdr}) / 2)] = wbyte;\n\
788         \x20         end\n\
789         \x20       end else if (drive && sio !== dout) violation(\"contend\");\n\
790         \x20     end\n\
791         \x20     nedge = nedge + 1;\n\
792         \x20   end\n\
793         \x20 end\n\
794         \n\
795         \x20 // Read data: nibble n follows the falling edge after rising edge {data_read} + n, valid tACLK later.\n\
796         \x20 always @(negedge sclk) begin\n\
797         \x20   if (!ce_n && qpi && cmd == 8'h{rd:02x} && nedge >= {data_read}) begin : drive_nibble\n\
798         \x20     reg [7:0] b; integer n;\n\
799         \x20     n = nedge - {data_read};\n\
800         \x20     note_wrap(addr, n / 2);\n\
801         \x20     b = mem[at(addr, n / 2)];\n\
802         \x20     #(T_ACLK);\n\
803         \x20     if (!ce_n) begin dout = (n % 2 == 0) ? b[7:4] : b[3:0]; drive = 1'b1; end\n\
804         \x20   end\n\
805         \x20 end\n\
806         endmodule\n",
807        name = p.name,
808        prov = p.provenance,
809        t_pu = ns(p.t_pu_min_ps),
810        t_cem = ns(p.t_cem_max_ps),
811        t_cph = ns(p.t_cph_min_ps),
812        t_rst = ns(p.t_rst_min_ps),
813        t_clk = ns(p.t_clk_min_ps),
814        t_aclk = ns(p.t_aclk_max_ps),
815        top = p.capacity_bytes - 1,
816        abits_m1 = addr_bits - 1,
817        pbits = page_bits,
818        pbits_m1 = page_bits - 1,
819        page = p.page_bytes,
820        rsten = p.reset_enable,
821        rst = p.reset,
822        qpi = p.enter_qpi,
823        rd = p.read_cmd,
824        wr = p.write_cmd,
825        hdr = QPI_HEADER_EDGES,
826    )
827}
828
829/// Several caches share one edge. When the edge is idle the lowest-numbered requester is granted and its
830/// request goes to the edge; that cache owns the edge until the burst completes and sees `c_ready` exactly
831/// once more when it does, in a cycle in which no other cache is granted. The edge's write-data takes and
832/// read strobes go to every cache: only the owner is ever in a burst state, the only states that listen.
833pub fn emit_psram_arbiter_verilog() -> String {
834    format!(
835        "// psram_arbiter — N psram_cache instances share one psram_edge (codegen_sva::psram).\n\
836         module psram_arbiter #(parameter integer N = 1, parameter integer AW = {ab}) (\n\
837         \x20 input clk, input rst,\n\
838         \x20 input  [N-1:0] c_valid, output [N-1:0] c_ready, input [N-1:0] c_we,\n\
839         \x20 input  [N*AW-1:0] c_addr, input [N*8-1:0] c_len, input [N*64-1:0] c_wdata,\n\
840         \x20 output e_valid, input e_ready, output e_we, output [AW-1:0] e_addr, output [7:0] e_len, output [63:0] e_wdata\n\
841         );\n\
842         \x20 reg pend;\n\
843         \x20 reg [7:0] owner;\n\
844         \x20 reg [7:0] pick;\n\
845         \x20 reg any;\n\
846         \x20 integer i;\n\
847         \x20 always @* begin\n\
848         \x20   any = 1'b0; pick = 8'd0;\n\
849         \x20   for (i = N - 1; i >= 0; i = i - 1) if (c_valid[i]) begin any = 1'b1; pick = i; end\n\
850         \x20 end\n\
851         \x20 wire [7:0] sel = pend ? owner : pick;\n\
852         \x20 assign e_valid = !pend && any;\n\
853         \x20 assign e_we = c_we[sel];\n\
854         \x20 assign e_addr = c_addr[sel*AW +: AW];\n\
855         \x20 assign e_len = c_len[sel*8 +: 8];\n\
856         \x20 assign e_wdata = c_wdata[sel*64 +: 64];\n\
857         \x20 genvar g;\n\
858         \x20 generate for (g = 0; g < N; g = g + 1) begin : grant\n\
859         \x20   assign c_ready[g] = e_ready && (pend ? owner == g : (any && pick == g));\n\
860         \x20 end endgenerate\n\
861         \x20 always @(posedge clk) begin\n\
862         \x20   if (rst) begin pend <= 1'b0; owner <= 8'd0; end\n\
863         \x20   else if (pend) begin if (e_ready) pend <= 1'b0; end\n\
864         \x20   else if (e_valid && e_ready) begin pend <= 1'b1; owner <= pick; end\n\
865         \x20 end\n\
866         endmodule\n",
867        ab = EDGE_WORD_ADDR_BITS,
868    )
869}
870
871/// A randomized testbench for the cache: an engine-shaped driver that issues nothing while `busy` and
872/// otherwise a read (then waits a cycle and checks `rd` against a reference memory), a write of one or
873/// both halves of a word, or a write and a read together. Once the edge has initialized the part it writes every word it will use — `words`
874/// consecutive from 0 and `words` scattered over the whole part — then runs `ops` random operations.
875/// Prints `CACHE-ERRORS`, `PSRAM-VIOLATIONS`, the longest `busy` run as `CACHE-MAX-STALL`, the rising edge
876/// after reset at which `req_ready` was first seen as `EDGE-READY-AT`, then `CACHE-DONE`. The clock runs at
877/// `run_period_ps`, anywhere in the edge's range.
878pub fn emit_psram_cache_testbench(
879    edge: &PsramEdge,
880    cache: &PsramCache,
881    run_period_ps: u32,
882    seed: u32,
883    words: u32,
884    ops: u32,
885) -> String {
886    let period_ns = run_period_ps as f64 / 1000.0;
887    let budget_ns = (edge.init_cycles() as f64
888        + (2 * words + ops) as f64 * (cache.worst_stall_cycles(edge) as f64 + 4.0))
889        * period_ns
890        * 2.0;
891    format!(
892        "`timescale 1ns/1ps\n\
893         module tb;\n\
894         \x20 localparam integer NADDR = {words};\n\
895         \x20 localparam integer M = {ops};\n\
896         \x20 reg clk = 1'b0; always #{half_ns} clk = ~clk;\n\
897         \x20 reg rst = 1'b1;\n\
898         \x20 reg req_r = 1'b0, req_w = 1'b0; reg [{am}:0] ra = 0, wa = 0; reg [63:0] wd = 64'd0; reg [1:0] wm = 2'b11;\n\
899         \x20 // The engine's half of the hits: each registered beside the address it is the hit of.\n\
900         \x20 reg rhit = 1'b0, whit = 1'b0;\n\
901         \x20 // The cache's own reset: the bench's, and one cycle once the traffic is done, with lines valid.\n\
902         \x20 reg rst_pulse = 1'b0;\n\
903         \x20 wire crst = rst | rst_pulse;\n\
904         {hit_wires}\
905         \x20 wire [63:0] rd; wire busy;\n\
906         \x20 wire e_valid, e_ready, e_we, e_take, e_rv; wire [{am}:0] e_addr; wire [7:0] e_len; wire [63:0] e_wdata, e_rdata;\n\
907         \x20 wire ce_n, sclk; wire [3:0] sio_out, sio_oe; wire [3:0] sio;\n\
908         \x20 genvar gi;\n\
909         \x20 generate for (gi = 0; gi < 4; gi = gi + 1) begin : pads\n\
910         \x20   assign sio[gi] = sio_oe[gi] ? sio_out[gi] : 1'bz;\n\
911         \x20 end endgenerate\n\
912         \x20 psram_cache cache(.clk(clk), .rst(crst), .req_r(req_r), .ra(ra), .req_w(req_w), .wa(wa), .wd(wd), .wm(wm),\n\
913         \x20   {hit_ports},\n\
914         \x20   .rd(rd), .busy(busy), .e_valid(e_valid), .e_ready(e_ready), .e_we(e_we), .e_addr(e_addr), .e_len(e_len),\n\
915         \x20   .e_wdata(e_wdata), .e_wdata_take(e_take), .e_rdata(e_rdata), .e_rdata_valid(e_rv));\n\
916         \x20 psram_edge edge_u(.clk(clk), .rst(rst), .req_valid(e_valid), .req_ready(e_ready), .req_we(e_we),\n\
917         \x20   .req_addr(e_addr), .req_len(e_len), .wdata(e_wdata), .wdata_take(e_take), .rdata(e_rdata),\n\
918         \x20   .rdata_valid(e_rv), .ce_n(ce_n), .sclk(sclk), .sio_out(sio_out), .sio_oe(sio_oe), .sio_in(sio));\n\
919         \x20 psram_model part(.ce_n(ce_n), .sclk(sclk), .sio(sio));\n\
920         {hit_fn}\
921         \n\
922         \x20 // Word k < NADDR is address k; k >= NADDR is scattered over the whole part.\n\
923         \x20 function [{am}:0] addr_of(input integer k);\n\
924         \x20   reg [31:0] s;\n\
925         \x20   begin\n\
926         \x20     if (k < NADDR) addr_of = k;\n\
927         \x20     else begin s = (k - NADDR) * 40503 + (32'd1 << {am}); addr_of = s[{am}:0]; end\n\
928         \x20   end\n\
929         \x20 endfunction\n\
930         \x20 // Both halves of a written word vary with the draw, so a half that should have kept its old value\n\
931         \x20 // cannot pass by coinciding with the new one.\n\
932         \x20 function [63:0] mix(input [31:0] a, input [31:0] b);\n\
933         \x20   mix = {{a ^ b ^ 32'h9E3779B9, b * 32'd2654435761}};\n\
934         \x20 endfunction\n\
935         \n\
936         \x20 reg [63:0] refm [0:2*NADDR-1];\n\
937         \x20 reg [1:0] m;\n\
938         \x20 // A write of the enabled halves of `v`, to the part through the cache and to the reference memory.\n\
939         \x20 task put(input integer kk, input [63:0] v, input [1:0] mask);\n\
940         \x20   begin\n\
941         \x20     if (mask[0]) refm[kk][31:0] = v[31:0];\n\
942         \x20     if (mask[1]) refm[kk][63:32] = v[63:32];\n\
943         \x20     wa <= addr_of(kk); whit <= cache_hit(addr_of(kk)); wd <= v; wm <= mask; req_w <= 1'b1;\n\
944         \x20   end\n\
945         \x20 endtask\n\
946         \x20 reg [31:0] lcg;\n\
947         \x20 reg [1:0] tst;\n\
948         \x20 reg [63:0] expv;\n\
949         \x20 integer k, k2, op, init, done_ops, errors, run, max_run, edges, ready_at, fin;\n\
950         \x20 always @(posedge clk) if (!rst) begin\n\
951         \x20   edges = edges + 1;\n\
952         \x20   if (e_ready && ready_at < 0) ready_at = edges;\n\
953         \x20   if (busy) begin run = run + 1; if (run > max_run) max_run = run; end else run = 0;\n\
954         \x20 end\n\
955         \x20 always @(posedge clk) begin\n\
956         \x20   req_r <= 1'b0; req_w <= 1'b0;\n\
957         \x20   rhit <= cache_hit(ra); whit <= cache_hit(wa);\n\
958         \x20   if (!rst && !busy && ready_at >= 0) begin\n\
959         \x20     case (tst)\n\
960         \x20       2'd0: begin\n\
961         \x20         lcg = lcg * 32'd1664525 + 32'd1013904223;\n\
962         \x20         if (init < 2 * NADDR) begin\n\
963         \x20           put(init, mix(init, lcg), 2'b11); init = init + 1;\n\
964         \x20         end else if (done_ops < M) begin\n\
965         \x20           done_ops = done_ops + 1;\n\
966         \x20           k = (lcg >> 8) % (2 * NADDR); k2 = (lcg >> 17) % (2 * NADDR); op = lcg[31:28];\n\
967         \x20           m = lcg[27:26] == 2'b00 ? 2'b11 : lcg[27:26];\n\
968         \x20           if (op < 7) begin\n\
969         \x20             put(k, mix(k, lcg), m);\n\
970         \x20           end else if (op < 14) begin\n\
971         \x20             ra <= addr_of(k); rhit <= cache_hit(addr_of(k)); expv <= refm[k]; req_r <= 1'b1; tst <= 2'd1;\n\
972         \x20           end else begin\n\
973         \x20             put(k, mix(k, lcg ^ 32'hA5A5), m);\n\
974         \x20             if (op == 15) k2 = k;\n\
975         \x20             ra <= addr_of(k2); rhit <= cache_hit(addr_of(k2)); expv <= refm[k2]; req_r <= 1'b1; tst <= 2'd1;\n\
976         \x20           end\n\
977         \x20         end else if (fin < 3) begin\n\
978         \x20           rst_pulse <= (fin == 0); fin = fin + 1;\n\
979         \x20         end else begin\n\
980         \x20           $display(\"CACHE-ERRORS %0d\", errors);\n\
981         \x20           $display(\"PSRAM-VIOLATIONS %0d\", part.violations);\n\
982         \x20           $display(\"CACHE-MAX-STALL %0d\", max_run);\n\
983         \x20           $display(\"EDGE-READY-AT %0d\", ready_at);\n\
984         \x20           $display(\"CACHE-HIT-MISMATCHES %0d\", cache.hit_mismatches);\n\
985         \x20           $display(\"CACHE-DONE\");\n\
986         \x20           $finish;\n\
987         \x20         end\n\
988         \x20       end\n\
989         \x20       2'd1: tst <= 2'd2;\n\
990         \x20       2'd2: begin\n\
991         \x20         if (rd !== expv) begin\n\
992         \x20           errors = errors + 1;\n\
993         \x20           if (errors < 10) $display(\"MISMATCH addr=%h got=%016h exp=%016h\", ra, rd, expv);\n\
994         \x20         end\n\
995         \x20         tst <= 2'd0;\n\
996         \x20       end\n\
997         \x20       default: tst <= 2'd0;\n\
998         \x20     endcase\n\
999         \x20   end\n\
1000         \x20 end\n\
1001         \x20 initial begin\n\
1002         \x20   lcg = 32'd{seed}; tst = 2'd0; fin = 0; init = 0; done_ops = 0; errors = 0; run = 0; max_run = 0; edges = 0; ready_at = -1;\n\
1003         \x20   repeat (4) @(posedge clk); rst = 1'b0;\n\
1004         \x20 end\n\
1005         \x20 initial begin #{budget:.0}; $display(\"CACHE-TIMEOUT\"); $display(\"CACHE-ERRORS %0d\", errors); $finish; end\n\
1006         endmodule\n",
1007        half_ns = period_ns / 2.0,
1008        am = EDGE_WORD_ADDR_BITS - 1,
1009        budget = budget_ns + 10_000.0,
1010        hit_wires = emit_psram_cache_hit_wires("cache", cache),
1011        hit_ports = psram_cache_hit_ports("cache", "rhit", "whit"),
1012        hit_fn = psram_cache_hit_lookup(
1013            "cache_hit",
1014            &format!("input [{}:0] a", EDGE_WORD_ADDR_BITS - 1),
1015            cache,
1016            ["cache_tags", "cache_vals", "cache_filled", "cache_fidx", "cache_ftag"].map(String::from),
1017            "crst",
1018        ),
1019    )
1020}
1021
1022/// A randomized testbench for caches sharing the edge through the arbiter: `n` caches, each over its own
1023/// region of the part, driven by one engine-shaped driver that issues to any subset of them in the same
1024/// cycle — a read, a write, or a write and a read each — and nothing while any is `busy`. Prints
1025/// `MULTI-ERRORS`, `PSRAM-VIOLATIONS`, the longest stall as `MULTI-MAX-STALL`, then `MULTI-DONE`.
1026pub fn emit_psram_arbiter_testbench(
1027    edge: &PsramEdge,
1028    cache: &PsramCache,
1029    n: u32,
1030    seed: u32,
1031    words: u32,
1032    ops: u32,
1033) -> String {
1034    let period_ns = edge.sys_period_ps as f64 / 1000.0;
1035    let budget_ns = (edge.init_cycles() as f64
1036        + (words + ops) as f64 * n as f64 * (cache.worst_stall_cycles(edge) as f64 + 8.0))
1037        * period_ns
1038        * 2.0;
1039    let region_bits = EDGE_WORD_ADDR_BITS - (32 - (n.max(2) - 1).leading_zeros());
1040    let tw = EDGE_WORD_ADDR_BITS - cache.index_bits - cache.offset_bits;
1041    format!(
1042        "`timescale 1ns/1ps\n\
1043         module tb;\n\
1044         \x20 localparam integer N = {n};\n\
1045         \x20 localparam integer NADDR = {words};\n\
1046         \x20 localparam integer M = {ops};\n\
1047         \x20 reg clk = 1'b0; always #{half_ns} clk = ~clk;\n\
1048         \x20 reg rst = 1'b1;\n\
1049         \x20 reg [N-1:0] req_r = 0, req_w = 0;\n\
1050         \x20 reg [{am}:0] ra [0:N-1]; reg [{am}:0] wa [0:N-1]; reg [63:0] wd [0:N-1];\n\
1051         \x20 wire [63:0] rd [0:N-1];\n\
1052         \x20 wire [N-1:0] busy;\n\
1053         \x20 // The engine's half of each cache's hits, registered beside the address each is the hit of.\n\
1054         \x20 reg [N-1:0] rhit = 0, whit = 0;\n\
1055         \x20 wire [{tags_top}:0] c_tags [0:N-1]; wire [{lines_top}:0] c_vals [0:N-1]; wire [N-1:0] c_filled;\n\
1056         \x20 wire [{iw_top}:0] c_fidx [0:N-1]; wire [{tw_top}:0] c_ftag [0:N-1];\n\
1057         \x20 wire [N-1:0] c_valid, c_ready, c_we;\n\
1058         \x20 wire [N*{aw}-1:0] c_addr; wire [N*8-1:0] c_len; wire [N*64-1:0] c_wdata;\n\
1059         \x20 wire e_valid, e_ready, e_we, e_take, e_rv; wire [{am}:0] e_addr; wire [7:0] e_len; wire [63:0] e_wdata, e_rdata;\n\
1060         \x20 wire ce_n, sclk; wire [3:0] sio_out, sio_oe; wire [3:0] sio;\n\
1061         \x20 genvar gi;\n\
1062         \x20 generate for (gi = 0; gi < 4; gi = gi + 1) begin : pads\n\
1063         \x20   assign sio[gi] = sio_oe[gi] ? sio_out[gi] : 1'bz;\n\
1064         \x20 end endgenerate\n\
1065         \x20 // Cache g owns the region whose high bits are g.\n\
1066         \x20 generate for (gi = 0; gi < N; gi = gi + 1) begin : caches\n\
1067         \x20   wire [{am}:0] a_r = ra[gi] | (gi << {region_bits});\n\
1068         \x20   wire [{am}:0] a_w = wa[gi] | (gi << {region_bits});\n\
1069         \x20   psram_cache c(.clk(clk), .rst(rst), .req_r(req_r[gi]), .ra(a_r), .req_w(req_w[gi]), .wa(a_w), .wd(wd[gi]),\n\
1070         \x20     .rhit(rhit[gi]), .whit(whit[gi]), .tags(c_tags[gi]), .vals(c_vals[gi]), .filled(c_filled[gi]),\n\
1071         \x20     .fidx(c_fidx[gi]), .ftag(c_ftag[gi]),\n\
1072         \x20     .wm(2'b11), .rd(rd[gi]), .busy(busy[gi]), .e_valid(c_valid[gi]), .e_ready(c_ready[gi]), .e_we(c_we[gi]),\n\
1073         \x20     .e_addr(c_addr[gi*{aw} +: {aw}]), .e_len(c_len[gi*8 +: 8]), .e_wdata(c_wdata[gi*64 +: 64]),\n\
1074         \x20     .e_wdata_take(e_take), .e_rdata(e_rdata), .e_rdata_valid(e_rv));\n\
1075         \x20 end endgenerate\n\
1076         \x20 psram_arbiter #(.N(N)) arb(.clk(clk), .rst(rst), .c_valid(c_valid), .c_ready(c_ready), .c_we(c_we),\n\
1077         \x20   .c_addr(c_addr), .c_len(c_len), .c_wdata(c_wdata),\n\
1078         \x20   .e_valid(e_valid), .e_ready(e_ready), .e_we(e_we), .e_addr(e_addr), .e_len(e_len), .e_wdata(e_wdata));\n\
1079         \x20 psram_edge edge_u(.clk(clk), .rst(rst), .req_valid(e_valid), .req_ready(e_ready), .req_we(e_we),\n\
1080         \x20   .req_addr(e_addr), .req_len(e_len), .wdata(e_wdata), .wdata_take(e_take), .rdata(e_rdata),\n\
1081         \x20   .rdata_valid(e_rv), .ce_n(ce_n), .sclk(sclk), .sio_out(sio_out), .sio_oe(sio_oe), .sio_in(sio));\n\
1082         \x20 psram_model part(.ce_n(ce_n), .sclk(sclk), .sio(sio));\n\
1083         {hit_fn}\
1084         \n\
1085         \x20 reg [63:0] refm [0:N*NADDR-1];\n\
1086         \x20 reg [63:0] expv [0:N-1];\n\
1087         \x20 reg [N-1:0] chk;\n\
1088         \x20 reg [31:0] lcg;\n\
1089         \x20 reg [1:0] tst;\n\
1090         \x20 integer c, k, op, init, done_ops, errors, run, max_run, ready_at, edges;\n\
1091         \x20 wire any_busy = |busy;\n\
1092         \x20 always @(posedge clk) if (!rst) begin\n\
1093         \x20   edges = edges + 1;\n\
1094         \x20   if (e_ready && ready_at < 0) ready_at = edges;\n\
1095         \x20   if (any_busy) begin run = run + 1; if (run > max_run) max_run = run; end else run = 0;\n\
1096         \x20 end\n\
1097         \x20 always @(posedge clk) begin\n\
1098         \x20   req_r <= 0; req_w <= 0;\n\
1099         \x20   for (c = 0; c < N; c = c + 1) begin\n\
1100         \x20     rhit[c] <= hit_of(c, ra[c] | (c << {region_bits})); whit[c] <= hit_of(c, wa[c] | (c << {region_bits}));\n\
1101         \x20   end\n\
1102         \x20   if (!rst && !any_busy && ready_at >= 0) begin\n\
1103         \x20     case (tst)\n\
1104         \x20       2'd0: begin\n\
1105         \x20         if (init < NADDR) begin\n\
1106         \x20           for (c = 0; c < N; c = c + 1) begin\n\
1107         \x20             lcg = lcg * 32'd1664525 + 32'd1013904223;\n\
1108         \x20             refm[c*NADDR + init] = {{lcg, lcg ^ 32'h5555AAAA}};\n\
1109         \x20             wa[c] <= init; whit[c] <= hit_of(c, init | (c << {region_bits})); wd[c] <= refm[c*NADDR + init]; req_w[c] <= 1'b1;\n\
1110         \x20           end\n\
1111         \x20           init = init + 1;\n\
1112         \x20         end else if (done_ops < M) begin\n\
1113         \x20           done_ops = done_ops + 1; chk = 0;\n\
1114         \x20           for (c = 0; c < N; c = c + 1) begin\n\
1115         \x20             lcg = lcg * 32'd1664525 + 32'd1013904223;\n\
1116         \x20             op = lcg[31:29]; k = (lcg >> 8) % NADDR;\n\
1117         \x20             if (op < 3) begin\n\
1118         \x20               refm[c*NADDR + k] = {{lcg ^ c[31:0], lcg * 32'd3}}; wa[c] <= k; whit[c] <= hit_of(c, k | (c << {region_bits})); wd[c] <= refm[c*NADDR + k]; req_w[c] <= 1'b1;\n\
1119         \x20             end else if (op < 6) begin\n\
1120         \x20               ra[c] <= k; rhit[c] <= hit_of(c, k | (c << {region_bits})); expv[c] <= refm[c*NADDR + k]; req_r[c] <= 1'b1; chk[c] = 1'b1;\n\
1121         \x20             end else if (op == 6) begin\n\
1122         \x20               refm[c*NADDR + k] = {{lcg, ~lcg}}; wa[c] <= k; whit[c] <= hit_of(c, k | (c << {region_bits})); wd[c] <= refm[c*NADDR + k]; req_w[c] <= 1'b1;\n\
1123         \x20               k = (lcg >> 20) % NADDR;\n\
1124         \x20               ra[c] <= k; rhit[c] <= hit_of(c, k | (c << {region_bits})); expv[c] <= refm[c*NADDR + k]; req_r[c] <= 1'b1; chk[c] = 1'b1;\n\
1125         \x20             end\n\
1126         \x20           end\n\
1127         \x20           if (chk != 0) tst <= 2'd1;\n\
1128         \x20         end else begin\n\
1129         \x20           $display(\"MULTI-ERRORS %0d\", errors);\n\
1130         \x20           $display(\"PSRAM-VIOLATIONS %0d\", part.violations);\n\
1131         \x20           $display(\"MULTI-MAX-STALL %0d\", max_run);\n\
1132         \x20           $display(\"MULTI-DONE\");\n\
1133         \x20           $finish;\n\
1134         \x20         end\n\
1135         \x20       end\n\
1136         \x20       2'd1: tst <= 2'd2;\n\
1137         \x20       2'd2: begin\n\
1138         \x20         for (c = 0; c < N; c = c + 1) if (chk[c] && rd[c] !== expv[c]) begin\n\
1139         \x20           errors = errors + 1;\n\
1140         \x20           if (errors < 10) $display(\"MISMATCH cache=%0d addr=%h got=%016h exp=%016h\", c, ra[c], rd[c], expv[c]);\n\
1141         \x20         end\n\
1142         \x20         tst <= 2'd0;\n\
1143         \x20       end\n\
1144         \x20       default: tst <= 2'd0;\n\
1145         \x20     endcase\n\
1146         \x20   end\n\
1147         \x20 end\n\
1148         \x20 initial begin\n\
1149         \x20   lcg = 32'd{seed}; tst = 2'd0; init = 0; done_ops = 0; errors = 0; run = 0; max_run = 0; ready_at = -1; edges = 0;\n\
1150         \x20   for (c = 0; c < N; c = c + 1) begin ra[c] = 0; wa[c] = 0; wd[c] = 0; end\n\
1151         \x20   repeat (4) @(posedge clk); rst = 1'b0;\n\
1152         \x20 end\n\
1153         \x20 initial begin #{budget:.0}; $display(\"MULTI-TIMEOUT\"); $display(\"MULTI-ERRORS %0d\", errors); $finish; end\n\
1154         endmodule\n",
1155        half_ns = period_ns / 2.0,
1156        am = EDGE_WORD_ADDR_BITS - 1,
1157        aw = EDGE_WORD_ADDR_BITS,
1158        budget = budget_ns + 10_000.0,
1159        tags_top = cache.lines() * tw - 1,
1160        lines_top = cache.lines() - 1,
1161        iw_top = cache.index_bits - 1,
1162        tw_top = tw - 1,
1163        hit_fn = psram_cache_hit_lookup(
1164            "hit_of",
1165            &format!("input integer g, input [{}:0] a", EDGE_WORD_ADDR_BITS - 1),
1166            cache,
1167            ["c_tags[g]", "c_vals[g]", "c_filled[g]", "c_fidx[g]", "c_ftag[g]"].map(String::from),
1168            "rst",
1169        ),
1170    )
1171}
1172
1173/// Controller parameters a test may override, to drive the part out of its rules on purpose.
1174#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1175pub struct EdgeOverrides {
1176    pub powerup_cycles: Option<u32>,
1177    pub cph_cycles: Option<u32>,
1178    pub rst_cycles: Option<u32>,
1179}
1180
1181/// A testbench: the controller and the model, the bursts issued in order (write data taken from
1182/// `wdata` in order), every word read printed as `RD <hex>`, every burst's cycles from acceptance to
1183/// the next `req_ready` as `BURST <n>`, then the model's total and `PSRAM-DONE`.
1184pub fn emit_psram_edge_testbench(
1185    edge: &PsramEdge,
1186    over: &EdgeOverrides,
1187    bursts: &[PsramBurst],
1188    wdata: &[u64],
1189) -> String {
1190    let period_ns = edge.sys_period_ps as f64 / 1000.0;
1191    let mut words = String::new();
1192    for (i, w) in wdata.iter().enumerate() {
1193        let _ = writeln!(words, "    wmem[{i}] = 64'h{w:016x};");
1194    }
1195    let mut calls = String::new();
1196    for b in bursts {
1197        let _ = writeln!(
1198            calls,
1199            "    burst({}, 20'h{:05x}, 8'd{});",
1200            u8::from(b.write),
1201            b.word_addr,
1202            b.words
1203        );
1204    }
1205    let sim_budget_ns: f64 = (edge.powerup_cycles() as f64 + 100.0) * period_ns
1206        + bursts
1207            .iter()
1208            .map(|b| edge.burst_sys_cycles(b.write, b.words) as f64 + 8.0)
1209            .sum::<f64>()
1210            * period_ns
1211            * 2.0;
1212    format!(
1213        "`timescale 1ns/1ps\n\
1214         module tb;\n\
1215         \x20 reg clk = 1'b0; always #{half_ns} clk = ~clk;\n\
1216         \x20 reg rst = 1'b1;\n\
1217         \x20 reg req_valid = 1'b0, req_we = 1'b0; reg [19:0] req_addr = 20'h0; reg [7:0] req_len = 8'd0;\n\
1218         \x20 reg [63:0] wdata = 64'h0; wire wdata_take, req_ready, rdata_valid; wire [63:0] rdata;\n\
1219         \x20 wire ce_n, sclk; wire [3:0] sio_out, sio_oe; wire [3:0] sio;\n\
1220         \x20 genvar gi;\n\
1221         \x20 generate for (gi = 0; gi < 4; gi = gi + 1) begin : pads\n\
1222         \x20   assign sio[gi] = sio_oe[gi] ? sio_out[gi] : 1'bz;\n\
1223         \x20 end endgenerate\n\
1224         \x20 psram_edge #(.H({half}), .POWERUP({pu}), .CPH({cph}), .RSTGAP({rstgap})) dut(\n\
1225         \x20   .clk(clk), .rst(rst), .req_valid(req_valid), .req_ready(req_ready), .req_we(req_we), .req_addr(req_addr),\n\
1226         \x20   .req_len(req_len), .wdata(wdata), .wdata_take(wdata_take), .rdata(rdata), .rdata_valid(rdata_valid),\n\
1227         \x20   .ce_n(ce_n), .sclk(sclk), .sio_out(sio_out), .sio_oe(sio_oe), .sio_in(sio));\n\
1228         \x20 psram_model part(.ce_n(ce_n), .sclk(sclk), .sio(sio));\n\
1229         \x20 reg [63:0] wmem [0:{wlast}];\n\
1230         \x20 integer wi = 0, n;\n\
1231         \x20 always @(negedge clk) begin\n\
1232         \x20   if (rdata_valid) $display(\"RD %016h\", rdata);\n\
1233         \x20   if (wdata_take) begin wi = wi + 1; wdata = wmem[wi]; end\n\
1234         \x20 end\n\
1235         \x20 task burst(input we, input [19:0] a, input [7:0] len);\n\
1236         \x20   begin\n\
1237         \x20     @(negedge clk); while (!req_ready) @(negedge clk);\n\
1238         \x20     req_valid = 1'b1; req_we = we; req_addr = a; req_len = len;\n\
1239         \x20     @(posedge clk); n = 0;\n\
1240         \x20     @(negedge clk); req_valid = 1'b0;\n\
1241         \x20     while (!req_ready) begin @(posedge clk); n = n + 1; @(negedge clk); end\n\
1242         \x20     $display(\"BURST %0d\", n);\n\
1243         \x20   end\n\
1244         \x20 endtask\n\
1245         \x20 initial begin\n\
1246         {words}\
1247         \x20   wdata = wmem[0];\n\
1248         \x20   repeat (4) @(posedge clk); rst = 1'b0;\n\
1249         {calls}\
1250         \x20   repeat (4) @(posedge clk);\n\
1251         \x20   $display(\"PSRAM-VIOLATIONS %0d\", part.violations);\n\
1252         \x20   $display(\"PSRAM-DONE\");\n\
1253         \x20   $finish;\n\
1254         \x20 end\n\
1255         \x20 initial begin #{budget:.0}; $display(\"PSRAM-TIMEOUT\"); $display(\"PSRAM-VIOLATIONS %0d\", part.violations); $finish; end\n\
1256         endmodule\n",
1257        half_ns = period_ns / 2.0,
1258        half = edge.half,
1259        pu = over.powerup_cycles.unwrap_or(edge.powerup_cycles()),
1260        cph = over.cph_cycles.unwrap_or(edge.cph_cycles()),
1261        rstgap = over.rst_cycles.unwrap_or(edge.rst_cycles()),
1262        wlast = wdata.len().max(1) - 1,
1263        budget = sim_budget_ns + 10_000.0,
1264    )
1265}