1use super::dram::StreamDesc;
14use std::fmt::Write as _;
15
16#[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 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 pub read_cmd: u8,
32 pub read_wait: u32,
33 pub write_cmd: u8,
35 pub enter_qpi: u8,
37 pub reset_enable: u8,
38 pub reset: u8,
39}
40
41pub 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#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum PsramUnplaceable {
66 SclkTooFast { sclk_ps: u64, min_ps: u32 },
68 ReadWindowTooShort { half_ps: u64, t_aclk_ps: u32 },
71 ZeroDivider,
73 NotWords {
75 elem_bytes: u64,
76 stride_bytes: u64,
77 base: u64,
78 },
79 CacheShape { index_bits: u32, offset_bits: u32 },
81 CacheWiderThanAddress {
83 index_bits: u32,
84 offset_bits: u32,
85 addr_bits: u32,
86 },
87 LineBurst { line_words: u32, max_words: u32 },
89 SlowestFasterThanFastest { slowest_ps: u32, fastest_ps: u32 },
91 NoBurstAtSlowestClock { slowest_ps: u32 },
93}
94
95#[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#[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
115const QPI_HEADER_EDGES: u32 = 8;
117const EDGES_PER_WORD: u32 = 16;
119pub 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 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 pub fn powerup_cycles(&self) -> u32 {
181 self.cycles_for(self.part.t_pu_min_ps)
182 }
183
184 pub fn cph_cycles(&self) -> u32 {
186 self.cycles_for(self.part.t_cph_min_ps)
187 }
188
189 pub fn rst_cycles(&self) -> u32 {
191 self.cycles_for(self.part.t_rst_min_ps)
192 }
193
194 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 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 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 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 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
232pub 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
270pub 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
302pub 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#[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 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 pub fn worst_stall_cycles(&self, edge: &PsramEdge) -> u64 {
504 2 * self.miss_stall_cycles(edge)
505 }
506}
507
508pub 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
644pub 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
661fn 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
680pub 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
692pub 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
700pub 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
829pub 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
871pub 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
1022pub 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#[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
1181pub 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}