1use std::collections::BTreeMap;
26
27#[derive(Clone, Debug)]
33pub struct DramTimings {
34 pub banks: u32,
35 pub row_bytes: u64,
37 pub burst_bytes: u64,
39 pub data_cycles: u64,
41 pub trcd: u64,
43 pub trp: u64,
45 pub tras: u64,
47 pub trc: u64,
49 pub trrd: u64,
51 pub tfaw: u64,
53 pub tccd: u64,
55 pub trfc_pb: u64,
57 pub trefi_pb: u64,
59 pub twr: u64,
61 pub tturn: u64,
63 pub hot: bool,
65}
66
67impl DramTimings {
68 pub fn lpddr5x() -> Self {
73 DramTimings {
74 banks: 16,
75 row_bytes: 2048,
76 burst_bytes: 32,
77 data_cycles: 8,
78 trcd: 15,
79 trp: 15,
80 tras: 34,
81 trc: 49,
82 trrd: 8,
83 tfaw: 40,
84 tccd: 8,
85 trfc_pb: 112,
86 trefi_pb: 3906,
87 twr: 18,
88 tturn: 6,
89 hot: false,
90 }
91 }
92
93 pub fn ddr2_micron() -> Self {
100 DramTimings {
101 banks: 8,
102 row_bytes: 2048, burst_bytes: 16, data_cycles: 4, trcd: 5, trp: 5, tras: 18, trc: 23, trrd: 4, tfaw: 18, tccd: 2, trfc_pb: 51, trefi_pb: 3120, twr: 6, tturn: 2,
116 hot: false,
117 }
118 }
119
120 pub fn trefi_effective(&self) -> u64 {
121 if self.hot {
122 self.trefi_pb / 2
123 } else {
124 self.trefi_pb
125 }
126 }
127}
128
129#[derive(Clone, Debug)]
134pub struct StreamDesc {
135 pub base: u64,
136 pub elem_bytes: u64,
137 pub stride_bytes: u64,
138 pub count: u64,
139 pub write: bool,
140}
141
142#[derive(Clone, Debug)]
144pub struct Gather {
145 pub addrs: Vec<u64>,
146 pub elem_bytes: u64,
147 pub write: bool,
148}
149
150#[derive(Clone, Copy, PartialEq, Eq, Debug)]
152pub enum DramCmd {
153 Act { bank: u32, row: u64 },
154 Rd { bank: u32 },
155 Wr { bank: u32 },
156 Pre { bank: u32 },
157 RefPb { bank: u32 },
158}
159
160#[derive(Clone, Copy, Debug)]
164pub struct TimedCmd {
165 pub at: u64,
166 pub cmd: DramCmd,
167}
168
169pub fn map_addr(t: &DramTimings, addr: u64) -> (u32, u64) {
173 let row_index = addr / t.row_bytes;
174 let bank = (row_index % t.banks as u64) as u32;
175 (bank, row_index / t.banks as u64)
176}
177
178#[derive(Clone, Copy, PartialEq, Eq, Debug)]
180pub enum VisitOrder {
181 ProgramOrder,
183 RowTiled,
188}
189
190fn one_stream_bursts(t: &DramTimings, s: &StreamDesc) -> Vec<(u32, u64, bool)> {
191 let mut out = Vec::new();
192 let mut last_burst = u64::MAX;
193 for i in 0..s.count {
194 let addr = s.base + i * s.stride_bytes;
195 for b in 0..s.elem_bytes.div_ceil(t.burst_bytes).max(1) {
196 let burst = (addr + b * t.burst_bytes) / t.burst_bytes;
197 if burst != last_burst {
198 last_burst = burst;
199 let (bank, row) = map_addr(t, burst * t.burst_bytes);
200 out.push((bank, row, s.write));
201 }
202 }
203 }
204 out
205}
206
207fn burst_list(
210 t: &DramTimings,
211 streams: &[StreamDesc],
212 gathers: &[Gather],
213 order: VisitOrder,
214) -> Vec<(u32, u64, bool)> {
215 let mut out = Vec::new();
216 match order {
217 VisitOrder::ProgramOrder => {
218 for s in streams {
219 out.extend(one_stream_bursts(t, s));
220 }
221 }
222 VisitOrder::RowTiled => {
223 let mut runs: Vec<Vec<Vec<(u32, u64, bool)>>> = streams
225 .iter()
226 .map(|s| {
227 let bursts = one_stream_bursts(t, s);
228 let mut rows: Vec<Vec<(u32, u64, bool)>> = Vec::new();
229 for b in bursts {
230 match rows.last_mut() {
231 Some(run) if run[0].0 == b.0 && run[0].1 == b.1 => run.push(b),
232 _ => rows.push(vec![b]),
233 }
234 }
235 rows
236 })
237 .collect();
238 let mut idx = vec![0usize; runs.len()];
239 loop {
240 let mut any = false;
241 for (s, run_list) in runs.iter_mut().enumerate() {
242 if idx[s] < run_list.len() {
243 out.append(&mut run_list[idx[s]]);
244 idx[s] += 1;
245 any = true;
246 }
247 }
248 if !any {
249 break;
250 }
251 }
252 }
253 }
254 for g in gathers {
255 for &addr in &g.addrs {
256 let (bank, row) = map_addr(t, addr);
257 out.push((bank, row, g.write));
258 }
259 }
260 out
261}
262
263pub fn schedule(
271 t: &DramTimings,
272 streams: &[StreamDesc],
273 gathers: &[Gather],
274 bad_rows: &[(u32, u64)],
275) -> Vec<TimedCmd> {
276 schedule_ordered(t, streams, gathers, bad_rows, VisitOrder::ProgramOrder)
277}
278
279pub fn schedule_ordered(
281 t: &DramTimings,
282 streams: &[StreamDesc],
283 gathers: &[Gather],
284 bad_rows: &[(u32, u64)],
285 order: VisitOrder,
286) -> Vec<TimedCmd> {
287 let bursts = burst_list(t, streams, gathers, order);
288
289 let mut bad: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
292 for &(b, r) in bad_rows {
293 bad.entry(b).or_default().push(r);
294 }
295 for v in bad.values_mut() {
296 v.sort_unstable();
297 v.dedup();
298 }
299 let remap = |bank: u32, row: u64| -> u64 {
300 match bad.get(&bank) {
301 None => row,
302 Some(list) => {
303 let mut r = row;
305 for &b in list {
306 if b <= r {
307 r += 1;
308 } else {
309 break;
310 }
311 }
312 while list.binary_search(&r).is_ok() {
314 r += 1;
315 }
316 r
317 }
318 }
319 };
320
321 struct Visit {
323 bank: u32,
324 row: u64,
325 bursts: Vec<bool>, }
327 let mut visits: Vec<Visit> = Vec::new();
328 for (bank, row, write) in bursts {
329 let row = remap(bank, row);
330 match visits.last_mut() {
331 Some(v) if v.bank == bank && v.row == row => v.bursts.push(write),
332 _ => visits.push(Visit {
333 bank,
334 row,
335 bursts: vec![write],
336 }),
337 }
338 }
339
340 let nb = t.banks as usize;
349 let trefi = t.trefi_effective();
350 let mut idle_ready: Vec<u64> = vec![0; nb]; let mut last_act_bank: Vec<u64> = vec![u64::MAX; nb]; let mut next_ref: Vec<u64> = (0..nb)
353 .map(|i| (i as u64 + 1) * trefi / nb as u64)
354 .collect();
355 let mut acts: Vec<u64> = Vec::new(); let mut bus_free: u64 = 0;
357 let mut last_dir: Option<bool> = None; let mut out: Vec<TimedCmd> = Vec::new();
359
360 fn place_act(t: &DramTimings, acts: &mut Vec<u64>, mut at: u64) -> u64 {
361 if let Some(&last) = acts.last() {
362 at = at.max(last + t.trrd);
363 }
364 loop {
365 let win_start = at.saturating_sub(t.tfaw - 1);
366 let in_window = acts.iter().filter(|&&a| a >= win_start).count();
367 if in_window < 4 {
368 break;
369 }
370 let oldest = acts
371 .iter()
372 .copied()
373 .filter(|&a| a >= win_start)
374 .min()
375 .unwrap();
376 at = oldest + t.tfaw;
377 }
378 acts.push(at);
379 let horizon = at.saturating_sub(2 * t.tfaw);
380 acts.retain(|&a| a >= horizon);
381 at
382 }
383
384 let drain_refreshes = |bank: usize,
385 before: u64,
386 idle_ready: &mut Vec<u64>,
387 next_ref: &mut Vec<u64>,
388 out: &mut Vec<TimedCmd>| {
389 while next_ref[bank] <= before {
390 let r_at = next_ref[bank].max(idle_ready[bank]);
391 out.push(TimedCmd {
392 at: r_at,
393 cmd: DramCmd::RefPb { bank: bank as u32 },
394 });
395 idle_ready[bank] = r_at + t.trfc_pb;
396 next_ref[bank] += trefi;
397 }
398 };
399
400 for v in visits.iter() {
401 let bank = v.bank as usize;
402
403 drain_refreshes(bank, bus_free, &mut idle_ready, &mut next_ref, &mut out);
405
406 let mut act_lb = idle_ready[bank];
409 if last_act_bank[bank] != u64::MAX {
410 act_lb = act_lb.max(last_act_bank[bank] + t.trc);
411 }
412 let want = bus_free.saturating_sub(t.trcd);
413 let act_at = place_act(t, &mut acts, act_lb.max(want.min(u64::MAX)));
414 out.push(TimedCmd {
415 at: act_at,
416 cmd: DramCmd::Act {
417 bank: v.bank,
418 row: v.row,
419 },
420 });
421 last_act_bank[bank] = act_at;
422
423 let mut cas_at = (act_at + t.trcd).max(bus_free);
426 let mut last_cas_end = 0u64;
427 let mut last_write_end = 0u64;
428 for &write in &v.bursts {
429 let turn = if last_dir.is_some_and(|d| d != write) {
430 t.tturn
431 } else {
432 0
433 };
434 cas_at = cas_at.max(bus_free + turn);
435 let cmd = if write {
436 DramCmd::Wr { bank: v.bank }
437 } else {
438 DramCmd::Rd { bank: v.bank }
439 };
440 out.push(TimedCmd { at: cas_at, cmd });
441 bus_free = cas_at + t.tccd.max(t.data_cycles);
442 last_cas_end = cas_at + t.data_cycles;
443 if write {
444 last_write_end = last_cas_end;
445 }
446 last_dir = Some(write);
447 cas_at += t.tccd.max(t.data_cycles);
448 }
449
450 let pre_at = (act_at + t.tras)
452 .max(last_cas_end)
453 .max(if last_write_end > 0 {
454 last_write_end + t.twr
455 } else {
456 0
457 });
458 out.push(TimedCmd {
459 at: pre_at,
460 cmd: DramCmd::Pre { bank: v.bank },
461 });
462 idle_ready[bank] = pre_at + t.trp;
463 }
464
465 let span = out.iter().map(|c| c.at).max().unwrap_or(0);
467 for bank in 0..nb {
468 drain_refreshes(bank, span, &mut idle_ready, &mut next_ref, &mut out);
469 }
470
471 out.sort_by(|a, b| a.at.cmp(&b.at));
472 out
473}
474
475#[derive(Clone, Copy, Debug, PartialEq)]
477pub struct Efficiency {
478 pub data_bus_cycles: u64,
479 pub span_cycles: u64,
480 pub bursts: u64,
481 pub row_activates: u64,
482 pub refreshes: u64,
483}
484
485impl Efficiency {
486 pub fn value(&self) -> f64 {
487 if self.span_cycles == 0 {
488 1.0
489 } else {
490 self.data_bus_cycles as f64 / self.span_cycles as f64
491 }
492 }
493}
494
495pub fn measure(t: &DramTimings, plan: &[TimedCmd]) -> Efficiency {
496 let mut e = Efficiency {
497 data_bus_cycles: 0,
498 span_cycles: 0,
499 bursts: 0,
500 row_activates: 0,
501 refreshes: 0,
502 };
503 let mut last_end = 0u64;
504 for c in plan {
505 match c.cmd {
506 DramCmd::Rd { .. } | DramCmd::Wr { .. } => {
507 e.bursts += 1;
508 e.data_bus_cycles += t.data_cycles;
509 last_end = last_end.max(c.at + t.data_cycles);
510 }
511 DramCmd::Act { .. } => {
512 e.row_activates += 1;
513 last_end = last_end.max(c.at + 1);
514 }
515 DramCmd::RefPb { .. } => {
516 e.refreshes += 1;
517 last_end = last_end.max(c.at + t.trfc_pb);
518 }
519 DramCmd::Pre { .. } => last_end = last_end.max(c.at + 1),
520 }
521 }
522 e.span_cycles = last_end;
523 e
524}
525
526pub fn check_legal(t: &DramTimings, plan: &[TimedCmd]) -> Result<(), String> {
529 let nb = t.banks as usize;
530 #[derive(Clone, Copy, PartialEq)]
531 enum BankState {
532 Idle,
533 Active,
534 }
535 let mut state = vec![BankState::Idle; nb];
536 let mut act_at = vec![0u64; nb];
537 let mut act_ready = vec![0u64; nb];
539 let mut idle_ready = vec![0u64; nb];
541 let mut last_cas_end = vec![0u64; nb];
542 let mut acts: Vec<u64> = Vec::new();
543 let mut last_act: Option<u64> = None;
544 let mut bus_busy_until = 0u64;
545 let mut bus_last_dir: Option<bool> = None;
546 let mut last_write_end = vec![0u64; nb];
547 let mut refs: Vec<Vec<u64>> = vec![Vec::new(); nb];
548
549 let mut prev_at = 0u64;
550 for (i, c) in plan.iter().enumerate() {
551 if c.at < prev_at {
552 return Err(format!("cmd {i}: plan not time-sorted"));
553 }
554 prev_at = c.at;
555 match c.cmd {
556 DramCmd::Act { bank, .. } => {
557 let b = bank as usize;
558 if state[b] != BankState::Idle {
559 return Err(format!("cmd {i}: ACT on open bank {bank}"));
560 }
561 if c.at < idle_ready[b] {
562 return Err(format!(
563 "cmd {i}: ACT bank {bank} at {} violates tRP/tRFC (idle at {})",
564 c.at, idle_ready[b]
565 ));
566 }
567 if c.at < act_ready[b] {
568 return Err(format!("cmd {i}: ACT bank {bank} violates tRC"));
569 }
570 if let Some(la) = last_act {
571 if c.at < la + t.trrd {
572 return Err(format!("cmd {i}: ACT violates tRRD"));
573 }
574 }
575 let win = c.at.saturating_sub(t.tfaw - 1);
576 if acts.iter().filter(|&&a| a >= win).count() >= 4 {
577 return Err(format!("cmd {i}: 5th ACT inside tFAW window"));
578 }
579 acts.push(c.at);
580 let horizon = c.at.saturating_sub(4 * t.tfaw);
581 acts.retain(|&a| a >= horizon);
582 last_act = Some(c.at);
583 state[b] = BankState::Active;
584 act_at[b] = c.at;
585 act_ready[b] = c.at + t.trc;
586 }
587 DramCmd::Rd { bank } | DramCmd::Wr { bank } => {
588 let b = bank as usize;
589 let write = matches!(c.cmd, DramCmd::Wr { .. });
590 if state[b] != BankState::Active {
591 return Err(format!("cmd {i}: CAS on idle bank {bank}"));
592 }
593 if c.at < act_at[b] + t.trcd {
594 return Err(format!("cmd {i}: CAS violates tRCD on bank {bank}"));
595 }
596 let turn = if bus_last_dir.is_some_and(|d| d != write) {
597 t.tturn
598 } else {
599 0
600 };
601 if c.at < bus_busy_until + turn {
602 return Err(format!(
603 "cmd {i}: data-bus overlap/turnaround (free at {}, CAS at {})",
604 bus_busy_until + turn,
605 c.at
606 ));
607 }
608 bus_busy_until = c.at + t.data_cycles.max(t.tccd);
609 bus_last_dir = Some(write);
610 last_cas_end[b] = c.at + t.data_cycles;
611 if write {
612 last_write_end[b] = c.at + t.data_cycles;
613 }
614 }
615 DramCmd::Pre { bank } => {
616 let b = bank as usize;
617 if state[b] != BankState::Active {
618 return Err(format!("cmd {i}: PRE on idle bank {bank}"));
619 }
620 if c.at < act_at[b] + t.tras {
621 return Err(format!("cmd {i}: PRE violates tRAS on bank {bank}"));
622 }
623 if c.at < last_cas_end[b] {
624 return Err(format!(
625 "cmd {i}: PRE before burst completes on bank {bank}"
626 ));
627 }
628 if last_write_end[b] > 0 && c.at < last_write_end[b] + t.twr {
629 return Err(format!("cmd {i}: PRE violates tWR on bank {bank}"));
630 }
631 state[b] = BankState::Idle;
632 idle_ready[b] = idle_ready[b].max(c.at + t.trp);
633 }
634 DramCmd::RefPb { bank } => {
635 let b = bank as usize;
636 if state[b] != BankState::Idle {
637 return Err(format!("cmd {i}: REFpb on open bank {bank}"));
638 }
639 if c.at < idle_ready[b] {
640 return Err(format!(
641 "cmd {i}: REFpb bank {bank} at {} before idle-ready {}",
642 c.at, idle_ready[b]
643 ));
644 }
645 refs[b].push(c.at);
646 idle_ready[b] = c.at + t.trfc_pb;
647 }
648 }
649 }
650
651 let span = plan.iter().map(|c| c.at).max().unwrap_or(0);
655 let trefi = t.trefi_effective();
656 if span > 2 * trefi {
657 for (b, bank_refs) in refs.iter().enumerate() {
658 if bank_refs.is_empty() {
659 return Err(format!("bank {b}: no REFpb inside a {span}-cycle plan"));
660 }
661 let mut prev = 0u64;
662 for &r in bank_refs {
663 if r.saturating_sub(prev) > trefi + trefi / 2 {
664 return Err(format!(
665 "bank {b}: refresh gap {} exceeds 1.5×tREFI ({})",
666 r - prev,
667 trefi + trefi / 2
668 ));
669 }
670 prev = r;
671 }
672 if span.saturating_sub(prev) > trefi + trefi / 2 {
673 return Err(format!("bank {b}: trailing refresh gap exceeds 1.5×tREFI"));
674 }
675 }
676 }
677 Ok(())
678}
679
680pub struct DeviceModel;
684
685impl DeviceModel {
686 pub fn execute(t: &DramTimings, plan: &[TimedCmd]) -> Result<Vec<u64>, String> {
687 check_legal(t, plan)?;
688 Ok(plan
689 .iter()
690 .filter_map(|c| match c.cmd {
691 DramCmd::Rd { .. } | DramCmd::Wr { .. } => Some(c.at + t.data_cycles),
692 _ => None,
693 })
694 .collect())
695 }
696}
697
698pub fn planned_beats(t: &DramTimings, plan: &[TimedCmd]) -> Vec<u64> {
701 plan.iter()
702 .filter_map(|c| match c.cmd {
703 DramCmd::Rd { .. } | DramCmd::Wr { .. } => Some(c.at + t.data_cycles),
704 _ => None,
705 })
706 .collect()
707}
708
709pub fn standard_report() -> String {
713 let t = DramTimings::lpddr5x();
714 let seq = |base: u64, bytes: u64, write: bool| StreamDesc {
715 base,
716 elem_bytes: 8,
717 stride_bytes: 8,
718 count: bytes / 8,
719 write,
720 };
721
722 let mut scenarios: Vec<(&str, Vec<TimedCmd>, &DramTimings)> = Vec::new();
723 let seq_plan = schedule(&t, &[seq(0, 1 << 20, false)], &[], &[]);
724 scenarios.push(("sequential_read_1mb", seq_plan, &t));
725
726 let soa: Vec<StreamDesc> = (0..7)
728 .map(|i| seq(i * (1 << 22), 1 << 17, i == 6))
729 .collect();
730 let soa_plan = schedule_ordered(&t, &soa, &[], &[], VisitOrder::RowTiled);
731 scenarios.push(("soa7_rowtiled_nbody", soa_plan, &t));
732
733 let merge = vec![
735 seq(0, 1 << 18, false),
736 seq(1 << 22, 1 << 18, false),
737 seq(1 << 23, 1 << 19, true),
738 ];
739 let merge_plan = schedule_ordered(&t, &merge, &[], &[], VisitOrder::RowTiled);
740 scenarios.push(("merge3_rowtiled", merge_plan, &t));
741
742 let mut x = 0x9E3779B97F4A7C15u64;
744 let addrs: Vec<u64> = (0..4096)
745 .map(|_| {
746 x ^= x << 13;
747 x ^= x >> 7;
748 x ^= x << 17;
749 (x % (1 << 26)) & !7
750 })
751 .collect();
752 let gather_plan = schedule(
753 &t,
754 &[],
755 &[Gather {
756 addrs,
757 elem_bytes: 8,
758 write: false,
759 }],
760 &[],
761 );
762 scenarios.push(("gather_4096_random", gather_plan, &t));
763
764 let hot = DramTimings {
765 hot: true,
766 ..DramTimings::lpddr5x()
767 };
768 let hot_plan = schedule(&hot, &[seq(0, 1 << 20, false)], &[], &[]);
769 let hot_owned = hot.clone();
770
771 let mut body = String::new();
772 let emit = |name: &str, plan: &[TimedCmd], tt: &DramTimings, body: &mut String| {
773 check_legal(tt, plan).unwrap_or_else(|e| panic!("{name}: illegal plan: {e}"));
774 let e = measure(tt, plan);
775 body.push_str(&format!(
776 " \"{name}\": {{ \"efficiency\": {:.4}, \"bursts\": {}, \"activates\": {}, \
777 \"refreshes\": {}, \"span_cycles\": {} }},\n",
778 e.value(),
779 e.bursts,
780 e.row_activates,
781 e.refreshes,
782 e.span_cycles
783 ));
784 };
785 for (name, plan, tt) in &scenarios {
786 emit(name, plan, tt, &mut body);
787 }
788 emit("sequential_read_1mb_hot", &hot_plan, &hot_owned, &mut body);
789 let body = body.trim_end_matches(",\n").to_string();
790
791 format!(
792 "{{\n \"meta\": {{\n \"model\": \"LPDDR5X-class 16-bank closed-page, \
793 bank-rotated, background per-bank refresh\",\n \"timings\": {{ \"trcd\": {}, \
794 \"trp\": {}, \"tras\": {}, \"trc\": {}, \"trrd\": {}, \"tfaw\": {}, \"tccd\": {}, \
795 \"trfc_pb\": {}, \"trefi_pb\": {}, \"twr\": {}, \"tturn\": {}, \"banks\": {}, \"row_bytes\": {}, \
796 \"burst_bytes\": {} }},\n \"note\": \"pure function of the timing table — \
797 regenerate with DRAM_RECORD=1 cargo test dram_sim_artifact\", \"unmodeled\": \"bank-group tCCD_L/S, ZQ calibration, command-bus contention — flagged for the memory-architect review\"\n }},\n \
798 \"scenarios\": {{\n{body}\n }}\n}}\n",
799 t.trcd,
800 t.trp,
801 t.tras,
802 t.trc,
803 t.trrd,
804 t.tfaw,
805 t.tccd,
806 t.trfc_pb,
807 t.trefi_pb,
808 t.twr,
809 t.tturn,
810 t.banks,
811 t.row_bytes,
812 t.burst_bytes
813 )
814}
815
816pub fn delta_encode(plan: &[TimedCmd]) -> Vec<u64> {
828 let mut out = Vec::with_capacity(plan.len());
829 let mut prev = 0u64;
830 for c in plan {
831 let delta = c.at - prev;
832 assert!(
833 delta < (1 << 24),
834 "command delta {delta} exceeds 24-bit executor field"
835 );
836 prev = c.at;
837 let (op, bank, row) = match c.cmd {
838 DramCmd::Act { bank, row } => (0u64, bank as u64, row),
839 DramCmd::Rd { bank } => (1, bank as u64, 0),
840 DramCmd::Wr { bank } => (2, bank as u64, 0),
841 DramCmd::Pre { bank } => (3, bank as u64, 0),
842 DramCmd::RefPb { bank } => (4, bank as u64, 0),
843 };
844 assert!(row < (1 << 32), "row exceeds 32-bit executor field");
845 out.push((delta << 40) | (op << 37) | (bank << 32) | row);
846 }
847 out
848}
849
850pub fn emit_dram_executor_verilog() -> String {
854 r#"// dram_executor — the rung-3 in-order command-stream executor (COLD_HARD_FACTS Wave 3).
855// The compile-time plan is delta-encoded ([delta:24][op:3][bank:5][row:32]); this FSM
856// waits each delta and fires the command pins. Commands can only issue in strict word
857// order — there is no queue or scheduling logic in the netlist to do otherwise. The
858// countdown arithmetic is proven exact by our certified SAT (see codegen_sva::dram).
859// cmd_ack is a FIFO-POP pulse asserted the cycle a word is CONSUMED (loaded), not the
860// cycle it later issues — so the producer's read index advances immediately and a
861// zero-delay command can never reload itself (the classic handshake-latency trap).
862module dram_executor(
863 input clk,
864 input rst,
865 input [63:0] cmd_word,
866 input cmd_valid,
867 output reg cmd_ack,
868 output reg issue,
869 output reg [2:0] op,
870 output reg [4:0] bank,
871 output reg [31:0] row
872);
873 reg loaded;
874 reg [23:0] wait_left;
875 reg [2:0] op_l;
876 reg [4:0] bank_l;
877 reg [31:0] row_l;
878 always @(posedge clk) begin
879 if (rst) begin
880 loaded <= 1'b0; wait_left <= 24'd0; issue <= 1'b0; cmd_ack <= 1'b0;
881 op <= 3'd0; bank <= 5'd0; row <= 32'd0; op_l <= 3'd0; bank_l <= 5'd0; row_l <= 32'd0;
882 end else begin
883 issue <= 1'b0;
884 cmd_ack <= 1'b0;
885 if (!loaded) begin
886 if (cmd_valid) begin
887 wait_left <= cmd_word[63:40];
888 op_l <= cmd_word[39:37];
889 bank_l <= cmd_word[36:32];
890 row_l <= cmd_word[31:0];
891 loaded <= 1'b1;
892 cmd_ack <= 1'b1;
893 end
894 end else if (wait_left == 24'd0) begin
895 issue <= 1'b1;
896 op <= op_l; bank <= bank_l; row <= row_l;
897 loaded <= 1'b0;
898 end else begin
899 wait_left <= wait_left - 24'd1;
900 end
901 end
902 end
903endmodule
904"#
905 .to_string()
906}
907
908pub fn emit_dram_executor_testbench(nwords: usize) -> String {
913 format!(
914 r#"// tb_dram_executor — replays a REAL Wave-2 command stream (dram_plan.hex).
915`timescale 1ns/1ps
916module tb_dram_executor;
917 reg clk = 0; always #5 clk = ~clk;
918 reg rst = 1;
919 reg [63:0] rom [0:{last}];
920 integer idx = 0;
921 integer issues = 0;
922 integer errors = 0;
923 wire [63:0] cmd_word = rom[idx];
924 wire cmd_valid = (idx <= {last});
925 wire cmd_ack, issue;
926 wire [2:0] op; wire [4:0] bank; wire [31:0] row;
927 dram_executor dut(.clk(clk), .rst(rst), .cmd_word(cmd_word), .cmd_valid(cmd_valid),
928 .cmd_ack(cmd_ack), .issue(issue), .op(op), .bank(bank), .row(row));
929 reg [63:0] expect_word;
930 always @(posedge clk) begin
931 if (cmd_ack) idx <= idx + 1; // pop on CONSUME (load), not issue
932 if (issue) begin
933 expect_word = rom[issues]; // issues arrive strictly in word order
934 if (op !== expect_word[39:37] || bank !== expect_word[36:32] || row !== expect_word[31:0]) begin
935 errors = errors + 1;
936 $display("FAIL: issue %0d payload mismatch", issues);
937 end
938 issues = issues + 1;
939 end
940 end
941 initial begin
942 $readmemh("dram_plan.hex", rom);
943 repeat (4) @(posedge clk);
944 rst = 0;
945 // Run long enough for every delta in the plan slice.
946 repeat (200000) @(posedge clk);
947 if (issues !== {n}) begin
948 $display("FAIL: issued %0d of {n} commands", issues);
949 end else if (errors == 0) begin
950 $display("PASS: %0d commands issued strictly in order with exact payloads", issues);
951 end
952 $finish;
953 end
954endmodule
955"#,
956 last = nwords - 1,
957 n = nwords
958 )
959}
960
961#[derive(Clone, Copy, Debug, PartialEq, Eq)]
978pub struct ComputeDramTiming {
979 pub t1: u64,
981 pub t2: u64,
983}
984
985impl ComputeDramTiming {
986 pub fn nominal() -> Self {
988 ComputeDramTiming { t1: 2, t2: 1 }
989 }
990}
991
992#[derive(Clone, Copy, Debug, PartialEq, Eq)]
996pub enum ComputeDramOp {
997 RowCopy {
998 bank: u32,
999 src: u64,
1000 dst: u64,
1001 },
1002 And {
1003 bank: u32,
1004 a: u64,
1005 b: u64,
1006 dst: u64,
1007 },
1008 Or {
1009 bank: u32,
1010 a: u64,
1011 b: u64,
1012 dst: u64,
1013 },
1014 Maj {
1015 bank: u32,
1016 a: u64,
1017 b: u64,
1018 c: u64,
1019 dst: u64,
1020 },
1021}
1022
1023impl ComputeDramOp {
1024 pub fn bank(&self) -> u32 {
1025 match *self {
1026 ComputeDramOp::RowCopy { bank, .. }
1027 | ComputeDramOp::And { bank, .. }
1028 | ComputeDramOp::Or { bank, .. }
1029 | ComputeDramOp::Maj { bank, .. } => bank,
1030 }
1031 }
1032
1033 fn activation_rows(&self) -> Vec<u64> {
1035 match *self {
1036 ComputeDramOp::RowCopy { src, dst, .. } => vec![src, dst],
1037 ComputeDramOp::And { a, b, .. } | ComputeDramOp::Or { a, b, .. } => vec![a, b],
1038 ComputeDramOp::Maj { a, b, c, .. } => vec![a, b, c],
1039 }
1040 }
1041
1042 fn is_reduction(&self) -> bool {
1043 !matches!(self, ComputeDramOp::RowCopy { .. })
1044 }
1045}
1046
1047pub fn emit_computedram_stream(op: &ComputeDramOp, tv: &ComputeDramTiming) -> Vec<TimedCmd> {
1051 let bank = op.bank();
1052 let rows = op.activation_rows();
1053 let mut out = Vec::new();
1054 let mut at = 0u64;
1055 for (i, row) in rows.iter().enumerate() {
1058 out.push(TimedCmd {
1059 at,
1060 cmd: DramCmd::Act { bank, row: *row },
1061 });
1062 at += tv.t1;
1063 if i + 1 < rows.len() {
1064 out.push(TimedCmd {
1065 at,
1066 cmd: DramCmd::Pre { bank },
1067 });
1068 at += tv.t2;
1069 }
1070 }
1071 if op.is_reduction() {
1073 out.push(TimedCmd {
1074 at,
1075 cmd: DramCmd::Rd { bank },
1076 });
1077 at += 1;
1078 }
1079 out.push(TimedCmd {
1080 at,
1081 cmd: DramCmd::Pre { bank },
1082 });
1083 out
1084}
1085
1086pub fn word_majority(a: u64, b: u64, c: u64) -> u64 {
1088 (a & b) | (a & c) | (b & c)
1089}
1090
1091#[derive(Clone, Debug, Default)]
1094pub struct RowStore {
1095 words: usize,
1096 rows: BTreeMap<(u32, u64), Vec<u64>>,
1097}
1098
1099impl RowStore {
1100 pub fn new(words: usize) -> Self {
1101 RowStore {
1102 words,
1103 rows: BTreeMap::new(),
1104 }
1105 }
1106
1107 pub fn write(&mut self, bank: u32, row: u64, data: &[u64]) {
1108 let mut v = vec![0u64; self.words];
1109 for (slot, w) in v.iter_mut().zip(data.iter()) {
1110 *slot = *w;
1111 }
1112 self.rows.insert((bank, row), v);
1113 }
1114
1115 pub fn read(&self, bank: u32, row: u64) -> Vec<u64> {
1116 self.rows
1117 .get(&(bank, row))
1118 .cloned()
1119 .unwrap_or_else(|| vec![0u64; self.words])
1120 }
1121
1122 pub fn apply(&mut self, op: &ComputeDramOp) {
1125 let (bank, dst, result): (u32, u64, Vec<u64>) = match *op {
1126 ComputeDramOp::RowCopy { bank, src, dst } => (bank, dst, self.read(bank, src)),
1127 ComputeDramOp::And { bank, a, b, dst } => {
1128 let (ra, rb) = (self.read(bank, a), self.read(bank, b));
1129 (
1130 bank,
1131 dst,
1132 ra.iter()
1133 .zip(&rb)
1134 .map(|(x, y)| word_majority(*x, *y, 0))
1135 .collect(),
1136 )
1137 }
1138 ComputeDramOp::Or { bank, a, b, dst } => {
1139 let (ra, rb) = (self.read(bank, a), self.read(bank, b));
1140 (
1141 bank,
1142 dst,
1143 ra.iter()
1144 .zip(&rb)
1145 .map(|(x, y)| word_majority(*x, *y, u64::MAX))
1146 .collect(),
1147 )
1148 }
1149 ComputeDramOp::Maj { bank, a, b, c, dst } => {
1150 let (ra, rb, rc) = (self.read(bank, a), self.read(bank, b), self.read(bank, c));
1151 let r = ra
1152 .iter()
1153 .zip(&rb)
1154 .zip(&rc)
1155 .map(|((x, y), z)| word_majority(*x, *y, *z))
1156 .collect();
1157 (bank, dst, r)
1158 }
1159 };
1160 self.write(bank, dst, &result);
1161 }
1162}
1163
1164#[cfg(test)]
1165#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/dram/tests.rs"]
1166mod tests;