1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
38pub enum FlashOp {
39 Read,
41 Program,
43 Erase,
45}
46
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub struct FlashCmd {
51 pub op: FlashOp,
52 pub chan: u32,
53 pub die: u32,
54}
55
56#[derive(Clone, Copy, Debug)]
59pub struct TimedCmd {
60 pub at: u64,
61 pub cmd: FlashCmd,
62}
63
64#[derive(Clone, Debug)]
68pub struct FlashTimings {
69 pub channels: u32,
71 pub dies_per_channel: u32,
73 pub planes_per_die: u32,
75 pub pages_per_block: u32,
77 pub zone_pages: u64,
79 pub t_r: u64,
81 pub t_prog: u64,
83 pub t_bers: u64,
85 pub data_cycles: u64,
87 pub suspend_window: u64,
89 pub ldpc_retry_worst: u64,
91}
92
93impl FlashTimings {
94 pub fn zns_tlc() -> Self {
97 FlashTimings {
98 channels: 8,
99 dies_per_channel: 8,
100 planes_per_die: 2,
101 pages_per_block: 384,
102 zone_pages: 12288,
103 t_r: 64,
104 t_prog: 640,
105 t_bers: 6400,
106 data_cycles: 8,
107 suspend_window: 96,
108 ldpc_retry_worst: 32,
109 }
110 }
111
112 pub fn pslc_fast() -> Self {
115 FlashTimings {
116 channels: 8,
117 dies_per_channel: 8,
118 planes_per_die: 2,
119 pages_per_block: 256,
120 zone_pages: 8192,
121 t_r: 16,
122 t_prog: 160,
123 t_bers: 1600,
124 data_cycles: 8,
125 suspend_window: 32,
126 ldpc_retry_worst: 8,
127 }
128 }
129
130 pub fn n_dies(&self) -> u32 {
132 self.channels * self.dies_per_channel
133 }
134
135 pub fn read_latency_bound(&self) -> u64 {
138 self.t_r + self.data_cycles + self.suspend_window + self.ldpc_retry_worst
139 }
140}
141
142pub fn map_page(t: &FlashTimings, page: u64) -> (u32, u32) {
146 let g = (page % t.n_dies() as u64) as u32;
147 let chan = g % t.channels;
148 (chan, g)
149}
150
151#[derive(Clone, Debug)]
154pub struct ZoneDesc {
155 pub zone: u64,
156 pub base_page: u64,
157 pub count: u64,
158 pub write: bool,
159}
160
161#[derive(Clone, Debug)]
163pub struct Gather {
164 pub pages: Vec<u64>,
165}
166
167#[derive(Clone, Copy, Debug)]
170pub struct ZoneReset {
171 pub zone: u64,
172}
173
174#[derive(Clone, Copy, PartialEq, Eq, Debug)]
176pub enum VisitOrder {
177 ProgramOrder,
179 Interleaved,
181}
182
183fn access_list(zones: &[ZoneDesc], gathers: &[Gather], order: VisitOrder) -> Vec<(FlashOp, u64)> {
184 let mut out: Vec<(FlashOp, u64)> = Vec::new();
185 let per_zone: Vec<Vec<(FlashOp, u64)>> = zones
186 .iter()
187 .map(|z| {
188 let op = if z.write {
189 FlashOp::Program
190 } else {
191 FlashOp::Read
192 };
193 (0..z.count).map(|i| (op, z.base_page + i)).collect()
194 })
195 .collect();
196 match order {
197 VisitOrder::ProgramOrder => {
198 for z in &per_zone {
199 out.extend(z.iter().copied());
200 }
201 }
202 VisitOrder::Interleaved => {
203 let mut idx = vec![0usize; per_zone.len()];
204 loop {
205 let mut any = false;
206 for (s, z) in per_zone.iter().enumerate() {
207 if idx[s] < z.len() {
208 out.push(z[idx[s]]);
209 idx[s] += 1;
210 any = true;
211 }
212 }
213 if !any {
214 break;
215 }
216 }
217 }
218 }
219 for g in gathers {
220 for &p in &g.pages {
221 out.push((FlashOp::Read, p));
222 }
223 }
224 out
225}
226
227#[derive(Clone, Copy, Debug)]
231pub struct Placement {
232 pub die_base: u32,
233 pub die_count: u32,
234}
235
236impl Placement {
237 pub fn full(t: &FlashTimings) -> Self {
238 Placement {
239 die_base: 0,
240 die_count: t.n_dies(),
241 }
242 }
243 fn map(&self, t: &FlashTimings, local: u64) -> (u32, u32) {
245 let die = self.die_base + (local % self.die_count.max(1) as u64) as u32;
246 (die % t.channels, die)
247 }
248 fn dies(&self) -> std::ops::Range<u32> {
249 self.die_base..self.die_base + self.die_count
250 }
251}
252
253fn schedule_ops(
257 t: &FlashTimings,
258 ops: &[(FlashOp, u32, u32)],
259 erase_dies: &[u32],
260) -> Vec<TimedCmd> {
261 let nd = t.n_dies() as usize;
262 let nc = t.channels as usize;
263 let mut die_free = vec![0u64; nd];
264 let mut chan_free = vec![0u64; nc];
265 let mut out: Vec<TimedCmd> = Vec::new();
266
267 for &(op, chan, die) in ops {
268 let (ci, gi) = (chan as usize, die as usize);
269 match op {
270 FlashOp::Read => {
271 let mut at = die_free[gi];
273 if chan_free[ci] > at + t.t_r {
274 at = chan_free[ci] - t.t_r;
275 }
276 at = at.max(die_free[gi]);
277 out.push(TimedCmd {
278 at,
279 cmd: FlashCmd { op, chan, die },
280 });
281 die_free[gi] = at + t.t_r;
282 chan_free[ci] = at + t.t_r + t.data_cycles;
283 }
284 FlashOp::Program => {
285 let at = die_free[gi].max(chan_free[ci]);
287 out.push(TimedCmd {
288 at,
289 cmd: FlashCmd { op, chan, die },
290 });
291 chan_free[ci] = at + t.data_cycles;
292 die_free[gi] = at + t.data_cycles + t.t_prog;
293 }
294 FlashOp::Erase => unreachable!("erases come from erase_dies"),
295 }
296 }
297
298 for &die in erase_dies {
302 let gi = die as usize;
303 let chan = die % t.channels;
304 let at = die_free[gi];
305 out.push(TimedCmd {
306 at,
307 cmd: FlashCmd {
308 op: FlashOp::Erase,
309 chan,
310 die,
311 },
312 });
313 die_free[gi] = at + t.t_bers;
314 }
315
316 out.sort_by(|a, b| a.at.cmp(&b.at));
317 out
318}
319
320pub fn schedule_zoned(
324 t: &FlashTimings,
325 zones: &[ZoneDesc],
326 gathers: &[Gather],
327 resets: &[ZoneReset],
328 order: VisitOrder,
329) -> Vec<TimedCmd> {
330 let nd = t.n_dies();
331 let ops: Vec<(FlashOp, u32, u32)> = access_list(zones, gathers, order)
332 .into_iter()
333 .map(|(op, page)| {
334 let (chan, die) = map_page(t, page);
335 (op, chan, die)
336 })
337 .collect();
338 let mut erase_dies: Vec<u32> = Vec::new();
339 for _ in resets {
340 erase_dies.extend(0..nd);
341 }
342 schedule_ops(t, &ops, &erase_dies)
343}
344
345pub fn schedule_placed(
349 t: &FlashTimings,
350 reads: &[(ZoneDesc, Placement)],
351 resets: &[(ZoneReset, Placement)],
352 order: VisitOrder,
353) -> Vec<TimedCmd> {
354 let per_stream: Vec<Vec<(FlashOp, u32, u32)>> = reads
355 .iter()
356 .map(|(z, p)| {
357 let op = if z.write {
358 FlashOp::Program
359 } else {
360 FlashOp::Read
361 };
362 (0..z.count)
363 .map(|i| {
364 let (chan, die) = p.map(t, z.base_page + i);
365 (op, chan, die)
366 })
367 .collect()
368 })
369 .collect();
370 let mut ops: Vec<(FlashOp, u32, u32)> = Vec::new();
371 match order {
372 VisitOrder::ProgramOrder => {
373 for s in &per_stream {
374 ops.extend(s.iter().copied());
375 }
376 }
377 VisitOrder::Interleaved => {
378 let mut idx = vec![0usize; per_stream.len()];
379 loop {
380 let mut any = false;
381 for (s, stream) in per_stream.iter().enumerate() {
382 if idx[s] < stream.len() {
383 ops.push(stream[idx[s]]);
384 idx[s] += 1;
385 any = true;
386 }
387 }
388 if !any {
389 break;
390 }
391 }
392 }
393 }
394 let mut erase_dies: Vec<u32> = Vec::new();
395 for (_r, p) in resets {
396 erase_dies.extend(p.dies());
397 }
398 schedule_ops(t, &ops, &erase_dies)
399}
400
401pub fn check_legal_flash(t: &FlashTimings, plan: &[TimedCmd]) -> Result<(), String> {
409 let nd = t.n_dies() as usize;
410 let nc = t.channels as usize;
411 let mut die_busy = vec![0u64; nd]; let mut chan_bus: Vec<Vec<(u64, u64, usize)>> = vec![Vec::new(); nc]; let mut prev_at = 0u64;
414
415 for (i, c) in plan.iter().enumerate() {
416 if c.at < prev_at {
417 return Err(format!("cmd {i}: plan not time-sorted"));
418 }
419 prev_at = c.at;
420 let (di, ci) = (c.cmd.die as usize, c.cmd.chan as usize);
421 if di >= nd {
422 return Err(format!("cmd {i}: die {} out of range", c.cmd.die));
423 }
424 if ci >= nc {
425 return Err(format!("cmd {i}: chan {} out of range", c.cmd.chan));
426 }
427 if c.at < die_busy[di] {
428 return Err(format!(
429 "cmd {i}: {:?} on die {} at {} while die busy until {} (op collision)",
430 c.cmd.op, c.cmd.die, c.at, die_busy[di]
431 ));
432 }
433 match c.cmd.op {
434 FlashOp::Read => {
435 let bus_start = c.at + t.t_r;
438 chan_bus[ci].push((bus_start, bus_start + t.data_cycles, i));
439 die_busy[di] = c.at + t.t_r;
440 }
441 FlashOp::Program => {
442 chan_bus[ci].push((c.at, c.at + t.data_cycles, i));
444 die_busy[di] = c.at + t.data_cycles + t.t_prog;
445 }
446 FlashOp::Erase => {
447 die_busy[di] = c.at + t.t_bers;
448 }
449 }
450 }
451
452 for (ci, windows) in chan_bus.iter().enumerate() {
454 let mut sorted = windows.clone();
455 sorted.sort_by_key(|w| w.0);
456 for pair in sorted.windows(2) {
457 if pair[1].0 < pair[0].1 {
458 return Err(format!(
459 "cmd {}: data bus on chan {ci} at {} overlaps a window busy until {}",
460 pair[1].2, pair[1].0, pair[0].1
461 ));
462 }
463 }
464 }
465 Ok(())
466}
467
468pub fn gather_delivery_order(t: &FlashTimings, pages: &[u64]) -> Vec<usize> {
474 let nd = t.n_dies() as usize;
475 let nc = t.channels as usize;
476 let mut die_free = vec![0u64; nd];
477 let mut chan_free = vec![0u64; nc];
478 let mut beats: Vec<(u64, usize)> = Vec::with_capacity(pages.len());
479 for (i, &page) in pages.iter().enumerate() {
480 let (chan, g) = map_page(t, page);
481 let (ci, gi) = (chan as usize, g as usize);
482 let mut at = die_free[gi];
483 if chan_free[ci] > at + t.t_r {
484 at = chan_free[ci] - t.t_r;
485 }
486 at = at.max(die_free[gi]);
487 beats.push((at + t.t_r + t.data_cycles, i));
488 die_free[gi] = at + t.t_r;
489 chan_free[ci] = at + t.t_r + t.data_cycles;
490 }
491 beats.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
492 beats.into_iter().map(|(_, i)| i).collect()
493}
494
495pub fn planned_read_beats(t: &FlashTimings, plan: &[TimedCmd]) -> Vec<u64> {
497 plan.iter()
498 .filter(|c| c.cmd.op == FlashOp::Read)
499 .map(|c| c.at + t.t_r + t.data_cycles)
500 .collect()
501}
502
503#[derive(Clone, Copy, Debug, PartialEq)]
505pub struct Utilization {
506 pub read_data_cycles: u64,
508 pub program_data_cycles: u64,
510 pub span_cycles: u64,
511 pub reads: u64,
512 pub programs: u64,
513 pub erases: u64,
514 pub worst_read_latency: u64,
516 pub channels: u32,
518}
519
520impl Utilization {
521 pub fn value(&self) -> f64 {
526 let cap = self.span_cycles * self.channels as u64;
527 if cap == 0 {
528 1.0
529 } else {
530 (self.read_data_cycles + self.program_data_cycles) as f64 / cap as f64
531 }
532 }
533}
534
535pub fn measure(t: &FlashTimings, plan: &[TimedCmd]) -> Utilization {
536 let mut u = Utilization {
537 read_data_cycles: 0,
538 program_data_cycles: 0,
539 span_cycles: 0,
540 reads: 0,
541 programs: 0,
542 erases: 0,
543 worst_read_latency: 0,
544 channels: t.channels,
545 };
546 let mut last_end = 0u64;
547 for c in plan {
548 match c.cmd.op {
549 FlashOp::Read => {
550 u.reads += 1;
551 u.read_data_cycles += t.data_cycles;
552 let end = c.at + t.t_r + t.data_cycles;
553 last_end = last_end.max(end);
554 u.worst_read_latency = u.worst_read_latency.max(t.t_r + t.data_cycles);
555 }
556 FlashOp::Program => {
557 u.programs += 1;
558 u.program_data_cycles += t.data_cycles;
559 last_end = last_end.max(c.at + t.data_cycles + t.t_prog);
560 }
561 FlashOp::Erase => {
562 u.erases += 1;
563 last_end = last_end.max(c.at + t.t_bers);
564 }
565 }
566 }
567 u.span_cycles = last_end;
568 u
569}
570
571pub struct FlashDeviceModel;
578
579impl FlashDeviceModel {
580 pub fn execute(t: &FlashTimings, plan: &[TimedCmd]) -> Vec<u64> {
581 let nd = t.n_dies() as usize;
582 let mut die_free = vec![0u64; nd];
583 let mut beats = Vec::new();
584 for c in plan {
585 let di = c.cmd.die as usize;
586 if di >= nd {
587 continue;
588 }
589 let issue = c.at.max(die_free[di]); match c.cmd.op {
591 FlashOp::Read => {
592 beats.push(issue + t.t_r + t.data_cycles);
593 die_free[di] = issue + t.t_r;
594 }
595 FlashOp::Program => die_free[di] = issue + t.data_cycles + t.t_prog,
596 FlashOp::Erase => die_free[di] = issue + t.t_bers,
597 }
598 }
599 beats
600 }
601}
602
603pub fn execute_with_suspend(t: &FlashTimings, plan: &[TimedCmd]) -> Vec<u64> {
607 let nd = t.n_dies() as usize;
608 let mut die_until = vec![0u64; nd];
610 let mut die_erasing = vec![false; nd];
611 let mut beats = Vec::new();
612 for c in plan {
613 let di = c.cmd.die as usize;
614 match c.cmd.op {
615 FlashOp::Read => {
616 let mut at = c.at;
617 if at < die_until[di] {
618 at = if die_erasing[di] {
620 c.at + t.suspend_window
621 } else {
622 die_until[di]
623 };
624 }
625 beats.push(at + t.t_r + t.data_cycles);
626 die_until[di] = at + t.t_r;
627 die_erasing[di] = false;
628 }
629 FlashOp::Program => {
630 let at = c.at.max(die_until[di]);
631 die_until[di] = at + t.data_cycles + t.t_prog;
632 die_erasing[di] = false;
633 }
634 FlashOp::Erase => {
635 let at = c.at.max(die_until[di]);
636 die_until[di] = at + t.t_bers;
637 die_erasing[di] = true;
638 }
639 }
640 }
641 beats
642}
643
644pub fn black_box_execute(
650 t: &FlashTimings,
651 plan: &[TimedCmd],
652 victim_die: u32,
653 victim_read: usize,
654) -> Vec<u64> {
655 let nd = t.n_dies() as usize;
656 let mut die_until = vec![0u64; nd];
657 let mut beats = Vec::new();
658 let mut read_ix = 0usize;
659 for c in plan {
660 let di = c.cmd.die as usize;
661 match c.cmd.op {
662 FlashOp::Read => {
663 if read_ix == victim_read && c.cmd.die == victim_die {
665 die_until[di] = die_until[di].max(c.at) + t.t_bers;
666 }
667 let at = c.at.max(die_until[di]);
668 beats.push(at + t.t_r + t.data_cycles);
669 die_until[di] = at + t.t_r;
670 read_ix += 1;
671 }
672 FlashOp::Program => {
673 let at = c.at.max(die_until[di]);
674 die_until[di] = at + t.data_cycles + t.t_prog;
675 }
676 FlashOp::Erase => {
677 let at = c.at.max(die_until[di]);
678 die_until[di] = at + t.t_bers;
679 }
680 }
681 }
682 beats
683}
684
685pub fn delta_encode(plan: &[TimedCmd]) -> Vec<u64> {
696 let mut out = Vec::with_capacity(plan.len());
697 let mut prev = 0u64;
698 for c in plan {
699 let delta = c.at - prev;
700 assert!(
701 delta < (1 << 24),
702 "command delta {delta} exceeds 24-bit executor field"
703 );
704 prev = c.at;
705 let op = match c.cmd.op {
706 FlashOp::Read => 0u64,
707 FlashOp::Program => 1,
708 FlashOp::Erase => 2,
709 };
710 out.push(
711 (delta << 40) | (op << 37) | ((c.cmd.chan as u64) << 32) | ((c.cmd.die as u64) << 24),
712 );
713 }
714 out
715}
716
717pub fn emit_storage_controller_verilog() -> String {
723 r#"// storage_controller — the in-order NAND command-stream executor (storage analog of
724// dram_executor). The compile-time plan is delta-encoded ([delta:24][op:3][chan:5][die:8]);
725// this FSM waits each delta and fires the command pins. Commands can only issue in strict
726// word order — there is no queue or scheduling logic in the netlist to do otherwise. The
727// countdown arithmetic is proven exact by our certified SAT (see codegen_sva::storage_controller_rtl).
728module storage_controller(
729 input clk,
730 input rst,
731 input [63:0] cmd_word,
732 input cmd_valid,
733 output reg cmd_ack,
734 output reg issue,
735 output reg [2:0] op,
736 output reg [4:0] chan,
737 output reg [7:0] die
738);
739 reg loaded;
740 reg [23:0] wait_left;
741 reg [2:0] op_l;
742 reg [4:0] chan_l;
743 reg [7:0] die_l;
744 always @(posedge clk) begin
745 if (rst) begin
746 loaded <= 1'b0; wait_left <= 24'd0; issue <= 1'b0; cmd_ack <= 1'b0;
747 op <= 3'd0; chan <= 5'd0; die <= 8'd0; op_l <= 3'd0; chan_l <= 5'd0; die_l <= 8'd0;
748 end else begin
749 issue <= 1'b0;
750 cmd_ack <= 1'b0;
751 if (!loaded) begin
752 if (cmd_valid) begin
753 wait_left <= cmd_word[63:40];
754 op_l <= cmd_word[39:37];
755 chan_l <= cmd_word[36:32];
756 die_l <= cmd_word[31:24];
757 loaded <= 1'b1;
758 cmd_ack <= 1'b1;
759 end
760 end else if (wait_left == 24'd0) begin
761 issue <= 1'b1;
762 op <= op_l; chan <= chan_l; die <= die_l;
763 loaded <= 1'b0;
764 end else begin
765 wait_left <= wait_left - 24'd1;
766 end
767 end
768 end
769endmodule
770"#
771 .to_string()
772}
773
774pub fn emit_storage_controller_testbench(nwords: usize) -> String {
780 format!(
781 r#"// tb_storage_controller — replays a REAL compiled NAND stream (storage_plan.hex) and
782// checks order + payload + TIMING FAITHFULNESS (gap - delta is a fixed pipeline overhead).
783`timescale 1ns/1ps
784module tb_storage_controller;
785 reg clk = 0; always #5 clk = ~clk;
786 reg rst = 1;
787 reg [63:0] rom [0:{last}];
788 integer idx = 0;
789 integer issues = 0;
790 integer errors = 0;
791 integer cyc = 0;
792 integer prev_issue = -1;
793 integer overhead = -1;
794 integer dlt = 0;
795 wire [63:0] cmd_word = rom[idx];
796 wire cmd_valid = (idx <= {last});
797 wire cmd_ack, issue;
798 wire [2:0] op; wire [4:0] chan; wire [7:0] die;
799 storage_controller dut(.clk(clk), .rst(rst), .cmd_word(cmd_word), .cmd_valid(cmd_valid),
800 .cmd_ack(cmd_ack), .issue(issue), .op(op), .chan(chan), .die(die));
801 reg [63:0] expect_word;
802 always @(posedge clk) begin
803 if (!rst) cyc = cyc + 1; // global cycle counter (origin irrelevant; gaps are what matter)
804 if (cmd_ack) idx <= idx + 1; // pop on CONSUME (load), not issue
805 if (issue) begin
806 expect_word = rom[issues]; // issues arrive strictly in word order
807 dlt = expect_word[63:40]; // this command's compiled delta
808 if (op !== expect_word[39:37] || chan !== expect_word[36:32] || die !== expect_word[31:24]) begin
809 errors = errors + 1;
810 $display("FAIL: issue %0d payload mismatch", issues);
811 end
812 // TIMING FAITHFULNESS: gap since the previous issue = delta + a constant pipeline overhead.
813 if (prev_issue >= 0) begin
814 if (overhead == -1) overhead = (cyc - prev_issue) - dlt;
815 else if (((cyc - prev_issue) - dlt) != overhead) begin
816 errors = errors + 1;
817 $display("FAIL: issue %0d timing skew (gap %0d, delta %0d, overhead %0d != %0d)",
818 issues, cyc - prev_issue, dlt, (cyc - prev_issue) - dlt, overhead);
819 end
820 end
821 prev_issue = cyc;
822 issues = issues + 1;
823 end
824 end
825 initial begin
826 $readmemh("storage_plan.hex", rom);
827 repeat (4) @(posedge clk);
828 rst = 0;
829 repeat (2000000) @(posedge clk);
830 if (issues !== {n}) begin
831 $display("FAIL: issued %0d of {n} commands", issues);
832 end else if (errors == 0) begin
833 $display("PASS: %0d commands issued in order, exact payloads, timing faithful (overhead %0d cycles)", issues, overhead);
834 end
835 $finish;
836 end
837endmodule
838"#,
839 last = nwords - 1,
840 n = nwords
841 )
842}
843
844pub fn standard_report() -> String {
848 let t = FlashTimings::zns_tlc();
849 let read_zone = |base: u64, count: u64| ZoneDesc {
850 zone: 0,
851 base_page: base,
852 count,
853 write: false,
854 };
855
856 let mut scenarios: Vec<(&str, Vec<TimedCmd>)> = Vec::new();
857 scenarios.push((
858 "sequential_read_8k_pages",
859 schedule_zoned(
860 &t,
861 &[read_zone(0, 8192)],
862 &[],
863 &[],
864 VisitOrder::ProgramOrder,
865 ),
866 ));
867
868 let append = ZoneDesc {
870 zone: 1,
871 base_page: 0,
872 count: 4096,
873 write: true,
874 };
875 scenarios.push((
876 "zns_append_4k_pages",
877 schedule_zoned(&t, &[append], &[], &[], VisitOrder::ProgramOrder),
878 ));
879
880 let read_win = Placement {
883 die_base: 0,
884 die_count: 56,
885 };
886 let gc_win = Placement {
887 die_base: 56,
888 die_count: 8,
889 };
890 scenarios.push((
891 "reads_with_concurrent_gc",
892 schedule_placed(
893 &t,
894 &[(
895 ZoneDesc {
896 zone: 0,
897 base_page: 0,
898 count: 6144,
899 write: false,
900 },
901 read_win,
902 )],
903 &[(ZoneReset { zone: 1 }, gc_win)],
904 VisitOrder::ProgramOrder,
905 ),
906 ));
907
908 let mut x = 0x9E3779B97F4A7C15u64;
910 let pages: Vec<u64> = (0..4096)
911 .map(|_| {
912 x ^= x << 13;
913 x ^= x >> 7;
914 x ^= x << 17;
915 x % (1 << 24)
916 })
917 .collect();
918 scenarios.push((
919 "random_gather_4096",
920 schedule_zoned(&t, &[], &[Gather { pages }], &[], VisitOrder::ProgramOrder),
921 ));
922
923 let mut body = String::new();
924 for (name, plan) in &scenarios {
925 check_legal_flash(&t, plan).unwrap_or_else(|e| panic!("{name}: illegal plan: {e}"));
926 let u = measure(&t, plan);
927 body.push_str(&format!(
928 " \"{name}\": {{ \"utilization\": {:.4}, \"reads\": {}, \"programs\": {}, \
929 \"erases\": {}, \"worst_read_latency\": {}, \"span_cycles\": {} }},\n",
930 u.value(),
931 u.reads,
932 u.programs,
933 u.erases,
934 u.worst_read_latency,
935 u.span_cycles
936 ));
937 }
938 let body = body.trim_end_matches(",\n").to_string();
939
940 format!(
941 "{{\n \"meta\": {{\n \"model\": \"host-managed ZNS-TLC, {ch}ch × {dc} dies, \
942 die-interleaved / channel-striped, erases scheduled into idle windows\",\n \
943 \"timings\": {{ \"t_r\": {}, \"t_prog\": {}, \"t_bers\": {}, \"data_cycles\": {}, \
944 \"suspend_window\": {}, \"ldpc_retry_worst\": {}, \"channels\": {}, \"dies_per_channel\": {} }},\n \
945 \"read_latency_bound\": {},\n \"note\": \"pure function of the timing table — \
946 regenerate with STORAGE_RECORD=1\"\n }},\n \"scenarios\": {{\n{body}\n }}\n}}\n",
947 t.t_r,
948 t.t_prog,
949 t.t_bers,
950 t.data_cycles,
951 t.suspend_window,
952 t.ldpc_retry_worst,
953 t.channels,
954 t.dies_per_channel,
955 t.read_latency_bound(),
956 ch = t.channels,
957 dc = t.dies_per_channel,
958 )
959}
960
961#[cfg(test)]
962#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/storage_controller_rtl/tests.rs"]
963mod tests;