Skip to main content

logicaffeine_compile/codegen_sva/
ddr2_phy.rs

1//! ddr2_phy — a full custom hand-rolled DDR2 controller + PHY for the Nexys A7-100T's on-board
2//! Micron MT47H64M16 (1 Gb, x16, 8 banks, DDR2-800). This is Phase 1 of the DRAM-into-Compute
3//! campaign: total ownership in the openXC7 flow (no vendor MIG), tied directly to the
4//! `dram_executor` command engine (`super::dram`), so the SAME command stream that drives the
5//! JEDEC-legal schedule also drives the deliberately-illegal ComputeDRAM cadences.
6//!
7//! Built in gated, iverilog-verified sub-steps (each proven against a behavioral DDR2 model
8//! before the board):
9//!   • 1a — command layer (THIS brick first): op field → DDR2 pins, single source of truth;
10//!          clocking + I/O primitives (MMCM/PLL, SSTL18, ODDR/IDDR, IDELAY);
11//!   • 1b — init / mode-register sequence;
12//!   • 1c — closed-loop per-byte-lane read calibration;
13//!   • 1d — full controller (ODT, refresh, power-down, bank-interleave, status);
14//!   • 1e — memtest over the full 128 MiB (the Phase-1 gate).
15//!
16//! The command layer maps the `dram_executor` op field (0=ACT 1=RD 2=WR 3=PRE 4=REFpb — see
17//! `super::dram::delta_encode`) plus the init commands (NOP / DESELECT / LOAD-MODE) to the JEDEC
18//! DDR2 command truth table on {CS#, RAS#, CAS#, WE#}. The Verilog `ddr2_cmd_encoder` is GENERATED
19//! from `ddr2_pins`, so the netlist and the model can never drift.
20
21use super::dram::{DramCmd, DramTimings, TimedCmd};
22
23/// A DDR2 command (JEDEC truth table over {CS#, RAS#, CAS#, WE#}, with CKE held high).
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Ddr2Cmd {
26    /// CS# high — the chip ignores the bus this cycle.
27    Deselect,
28    /// CS# low, RAS#/CAS#/WE# high — an idle active cycle.
29    Nop,
30    /// Open a row (RAS#).
31    Active,
32    /// Column read (CAS#).
33    Read,
34    /// Column write (CAS# + WE#).
35    Write,
36    /// Close a row (RAS# + WE#); A10 selects single-bank vs all-banks.
37    Precharge,
38    /// Auto-refresh (RAS# + CAS#), all banks precharged.
39    Refresh,
40    /// Mode/extended-mode register set (RAS# + CAS# + WE#); BA selects the register.
41    LoadMode,
42}
43
44/// The `(cs_n, ras_n, cas_n, we_n)` pin levels for a DDR2 command — the JEDEC DDR2 command truth
45/// table. This is the SINGLE SOURCE OF TRUTH the Verilog encoder is generated from.
46pub fn ddr2_pins(cmd: Ddr2Cmd) -> (u8, u8, u8, u8) {
47    match cmd {
48        // CS# high: bus ignored (RAS#/CAS#/WE# held high, don't-care).
49        Ddr2Cmd::Deselect => (1, 1, 1, 1),
50        Ddr2Cmd::Nop => (0, 1, 1, 1),
51        Ddr2Cmd::Active => (0, 0, 1, 1),
52        Ddr2Cmd::Read => (0, 1, 0, 1),
53        Ddr2Cmd::Write => (0, 1, 0, 0),
54        Ddr2Cmd::Precharge => (0, 0, 1, 0),
55        Ddr2Cmd::Refresh => (0, 0, 0, 1),
56        Ddr2Cmd::LoadMode => (0, 0, 0, 0),
57    }
58}
59
60/// Map a `dram_executor` op field (0=ACT 1=RD 2=WR 3=PRE 4=REFpb) to its DDR2 command.
61/// Returns `None` for op codes outside the executor's issued set.
62pub fn executor_op_to_cmd(op: u8) -> Option<Ddr2Cmd> {
63    match op {
64        0 => Some(Ddr2Cmd::Active),
65        1 => Some(Ddr2Cmd::Read),
66        2 => Some(Ddr2Cmd::Write),
67        3 => Some(Ddr2Cmd::Precharge),
68        4 => Some(Ddr2Cmd::Refresh),
69        _ => None,
70    }
71}
72
73/// Emit the combinational `ddr2_cmd_encoder` — op field + `issue` → DDR2 command pins. When
74/// `issue` is low the bus holds NOP (CS# low, RAS#/CAS#/WE# high). The `case` arms are GENERATED
75/// from [`ddr2_pins`], so the netlist is by-construction consistent with the model/oracle.
76pub fn emit_ddr2_command_encoder_verilog() -> String {
77    let (ncs, nras, ncas, nwe) = ddr2_pins(Ddr2Cmd::Nop);
78    let mut arms = String::new();
79    for op in 0u8..5 {
80        let cmd = executor_op_to_cmd(op).expect("0..5 are executor ops");
81        let (cs, ras, cas, we) = ddr2_pins(cmd);
82        arms.push_str(&format!(
83            "        3'd{op}: begin cs_n=1'b{cs}; ras_n=1'b{ras}; cas_n=1'b{cas}; we_n=1'b{we}; end // {cmd:?}\n"
84        ));
85    }
86    format!(
87        r#"// ddr2_cmd_encoder — op field (0=ACT 1=RD 2=WR 3=PRE 4=REFpb) → DDR2 command pins.
88// GENERATED from codegen_sva::ddr2_phy::ddr2_pins (the JEDEC DDR2 command truth table); the
89// netlist and the Rust model share one source of truth. `issue` low holds NOP.
90module ddr2_cmd_encoder(
91  input      [2:0] op,
92  input            issue,
93  output reg       cs_n,
94  output reg       ras_n,
95  output reg       cas_n,
96  output reg       we_n
97);
98  always @(*) begin
99    if (!issue) begin
100      cs_n=1'b{ncs}; ras_n=1'b{nras}; cas_n=1'b{ncas}; we_n=1'b{nwe}; // NOP
101    end else begin
102      case (op)
103{arms}        default: begin cs_n=1'b{ncs}; ras_n=1'b{nras}; cas_n=1'b{ncas}; we_n=1'b{nwe}; end // NOP
104      endcase
105    end
106  end
107endmodule
108"#
109    )
110}
111
112// ── Phase 1b: the DDR2 power-up / mode-register init sequence ─────────────────────────
113//
114// Before any read/write, DDR2 requires a fixed JEDEC power-up sequence (JESD79-2 / the Micron
115// MT47H64M16 datasheet): hold CKE low ≥200 µs, PRECHARGE ALL, then program the mode registers in
116// order — EMR2, EMR3, EMR1 (enable DLL), MR (with DLL reset) — PRECHARGE ALL, ≥2 AUTO-REFRESH, MR
117// (DLL not reset, normal operation), wait for DLL lock, then EMR1 OCD default → OCD exit. The
118// sequence is modeled as DATA (`ddr2_init_sequence`) with the JEDEC ordering/content pinned by
119// invariant locks, and the `ddr2_init_fsm` ROM is GENERATED from it (single source of truth).
120
121/// A DDR2 mode register, identified by its `BA[1:0]` selector.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum ModeReg {
124    /// Mode Register (BA = 00): burst length, CAS latency, burst type, DLL reset, write recovery.
125    Mr,
126    /// Extended Mode Register 1 (BA = 01): DLL enable, drive strength, ODT, AL, OCD.
127    Emr1,
128    /// Extended Mode Register 2 (BA = 10): high-temperature self-refresh, partial-array SR.
129    Emr2,
130    /// Extended Mode Register 3 (BA = 11): reserved.
131    Emr3,
132}
133
134impl ModeReg {
135    /// The `BA[1:0]` selector that addresses this register.
136    pub fn ba(self) -> u8 {
137        match self {
138            ModeReg::Mr => 0,
139            ModeReg::Emr1 => 1,
140            ModeReg::Emr2 => 2,
141            ModeReg::Emr3 => 3,
142        }
143    }
144}
145
146/// One step of the DDR2 power-up init sequence.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum InitStep {
149    /// Hold CKE low (DESELECT) for `cycles` — the ≥200 µs power-up wait — then raise CKE.
150    CkeLowWait { cycles: u64 },
151    /// A NOP idle wait of `cycles` (satisfies tRP / tMRD / tRFC / tDLLK between commands).
152    Wait { cycles: u64 },
153    /// PRECHARGE ALL banks (A10 = 1).
154    PrechargeAll,
155    /// AUTO-REFRESH.
156    Refresh,
157    /// Load a mode register with the 13-bit address payload (opcode).
158    LoadMode { reg: ModeReg, a: u16 },
159}
160
161/// EMR1 with the DLL ENABLED (A0 = 0), full drive, ODT off, AL = 0, OCD exit — the first EMR1.
162pub const EMR1_DLL_ENABLE: u16 = 0x0000;
163/// EMR1 OCD calibration DEFAULT (A9:A7 = 111).
164pub const EMR1_OCD_DEFAULT: u16 = 0b111 << 7;
165/// EMR1 OCD calibration EXIT (A9:A7 = 000), DLL still enabled.
166pub const EMR1_OCD_EXIT: u16 = 0x0000;
167
168/// Encode the Mode Register (BA = 00) opcode from the timing table: burst length (A2:A0),
169/// sequential burst type (A3 = 0), CAS latency (A6:A4, == tRCD on the 5-5-5 grade), DLL reset
170/// (A8), and write recovery WR-1 (A11:A9).
171pub fn mr_opcode(t: &DramTimings, dll_reset: bool) -> u16 {
172    let bl: u16 = if t.burst_bytes / 2 == 4 { 0b010 } else { 0b011 }; // x16: BL8 default
173    let cl: u16 = ((t.trcd as u16) & 0b111) << 4; // CAS latency A6:A4 (CL == tRCD, 5-5-5 grade)
174    let wr: u16 = ((t.twr as u16).saturating_sub(1) & 0b111) << 9; // write recovery WR-1 in A11:A9
175    let dll: u16 = if dll_reset { 1 << 8 } else { 0 };
176    bl | cl | wr | dll
177}
178
179/// The JEDEC DDR2 power-up init sequence for this part, as data. `cke_low_cycles` is the ≥200 µs
180/// power-up hold expressed in controller cycles (a deployment parameter of the chosen clock).
181pub fn ddr2_init_sequence(t: &DramTimings, cke_low_cycles: u64) -> Vec<InitStep> {
182    use InitStep::*;
183    use ModeReg::*;
184    let tmrd = 2; // MRS → next command
185    let tdllk = 200; // DLL lock after the normal-operation MR
186    vec![
187        CkeLowWait {
188            cycles: cke_low_cycles,
189        }, // ≥200 µs, CKE low, then raise CKE
190        Wait { cycles: 400 }, // settle after CKE high
191        PrechargeAll,
192        Wait { cycles: t.trp },
193        LoadMode { reg: Emr2, a: 0 },
194        Wait { cycles: tmrd },
195        LoadMode { reg: Emr3, a: 0 },
196        Wait { cycles: tmrd },
197        LoadMode {
198            reg: Emr1,
199            a: EMR1_DLL_ENABLE,
200        }, // enable DLL
201        Wait { cycles: tmrd },
202        LoadMode {
203            reg: Mr,
204            a: mr_opcode(t, true),
205        }, // program MR + reset DLL
206        Wait { cycles: tmrd },
207        PrechargeAll,
208        Wait { cycles: t.trp },
209        Refresh,
210        Wait { cycles: t.trfc_pb },
211        Refresh,
212        Wait { cycles: t.trfc_pb },
213        LoadMode {
214            reg: Mr,
215            a: mr_opcode(t, false),
216        }, // MR, DLL not reset — normal operation
217        Wait { cycles: tdllk }, // wait for DLL lock
218        LoadMode {
219            reg: Emr1,
220            a: EMR1_OCD_DEFAULT,
221        }, // OCD calibration default
222        Wait { cycles: tmrd },
223        LoadMode {
224            reg: Emr1,
225            a: EMR1_OCD_EXIT,
226        }, // OCD calibration exit — device ready
227        Wait { cycles: tmrd },
228    ]
229}
230
231/// The pin/field tuple `(cke, cmd, ba, a, wait)` an init step drives on the DDR2 bus.
232fn init_step_fields(step: &InitStep) -> (u8, Ddr2Cmd, u8, u16, u64) {
233    match *step {
234        InitStep::CkeLowWait { cycles } => (0, Ddr2Cmd::Deselect, 0, 0, cycles),
235        InitStep::Wait { cycles } => (1, Ddr2Cmd::Nop, 0, 0, cycles),
236        InitStep::PrechargeAll => (1, Ddr2Cmd::Precharge, 0, 1 << 10, 0), // A10 = 1 (all banks)
237        InitStep::Refresh => (1, Ddr2Cmd::Refresh, 0, 0, 0),
238        InitStep::LoadMode { reg, a } => (1, Ddr2Cmd::LoadMode, reg.ba(), a, 0),
239    }
240}
241
242/// Pack an init step into the 44-bit `ddr2_init_fsm` ROM word
243/// `{cke, cs_n, ras_n, cas_n, we_n, ba[1:0], a[12:0], wait[23:0]}` — the SINGLE encoding shared by
244/// the emitter and the drift lock, with the command pins taken from [`ddr2_pins`].
245pub fn init_rom_word(step: &InitStep) -> u64 {
246    let (cke, cmd, ba, a, wait) = init_step_fields(step);
247    let (cs, ras, cas, we) = ddr2_pins(cmd);
248    ((cke as u64) << 43)
249        | ((cs as u64) << 42)
250        | ((ras as u64) << 41)
251        | ((cas as u64) << 40)
252        | ((we as u64) << 39)
253        | ((ba as u64) << 37)
254        | ((a as u64) << 24)
255        | (wait & 0xff_ffff)
256}
257
258/// Emit `ddr2_init_fsm` — a ROM-driven power-up sequencer. The ROM is GENERATED from
259/// [`ddr2_init_sequence`]; the FSM drives `{cke, cs_n, ras_n, cas_n, we_n, ba, a}` per entry, waits
260/// its cycle count, advances, and latches `init_done`. (Behavioral iverilog validation lands with
261/// the controller-integration brick; this brick pins the ROM to the JEDEC spec.)
262pub fn emit_ddr2_init_fsm_verilog(t: &DramTimings, cke_low_cycles: u64) -> String {
263    let seq = ddr2_init_sequence(t, cke_low_cycles);
264    let n = seq.len().max(1);
265    let mut rom = String::new();
266    for (i, step) in seq.iter().enumerate() {
267        let word = init_rom_word(step);
268        let (_, cmd, ..) = init_step_fields(step);
269        rom.push_str(&format!("    rom[{i}] = 44'h{word:011x}; // {cmd:?}\n"));
270    }
271    format!(
272        r#"// ddr2_init_fsm — DDR2 power-up / mode-register sequencer. The step ROM is GENERATED from
273// codegen_sva::ddr2_phy::ddr2_init_sequence (the JEDEC init spec); each entry drives
274// {{cke, cs_n, ras_n, cas_n, we_n, ba, a}} for `wait` cycles, then advances. init_done latches high.
275module ddr2_init_fsm(
276  input             clk,
277  input             rst,
278  output reg        cke,
279  output reg        cs_n,
280  output reg        ras_n,
281  output reg        cas_n,
282  output reg        we_n,
283  output reg [1:0]  ba,
284  output reg [12:0] a,
285  output reg        init_done
286);
287  localparam NSTEPS = {n};
288  reg [43:0] rom [0:NSTEPS-1];
289  initial begin
290{rom}  end
291  integer step;
292  reg [43:0] cur;
293  reg [23:0] wait_left;
294  always @(posedge clk) begin
295    if (rst) begin
296      step <= 0; cur <= rom[0]; wait_left <= rom[0][23:0]; init_done <= 1'b0;
297    end else if (!init_done) begin
298      if (wait_left == 24'd0) begin
299        if (step == NSTEPS-1) init_done <= 1'b1;
300        else begin step <= step + 1; cur <= rom[step+1]; wait_left <= rom[step+1][23:0]; end
301      end else begin
302        wait_left <= wait_left - 24'd1;
303      end
304    end
305  end
306  always @(*) begin
307    if (init_done) begin
308      cke=1'b1; cs_n=1'b0; ras_n=1'b1; cas_n=1'b1; we_n=1'b1; ba=2'b0; a=13'b0; // NOP idle
309    end else begin
310      cke=cur[43]; cs_n=cur[42]; ras_n=cur[41]; cas_n=cur[40]; we_n=cur[39];
311      ba=cur[38:37]; a=cur[36:24];
312    end
313  end
314endmodule
315"#
316    )
317}
318
319/// A self-checking testbench for `ddr2_cmd_encoder`: drives every op with issue = 0 and issue = 1 and
320/// asserts the pins equal [`ddr2_pins`] (expectations GENERATED from the same truth table). PASS/FAIL.
321pub fn emit_ddr2_cmd_encoder_testbench() -> String {
322    let (ncs, nras, ncas, nwe) = ddr2_pins(Ddr2Cmd::Nop);
323    let mut checks = String::new();
324    for op in 0u8..8 {
325        checks.push_str(&format!(
326            "    chk(3'd{op}, 1'b0, 1'b{ncs}, 1'b{nras}, 1'b{ncas}, 1'b{nwe});\n"
327        ));
328    }
329    for op in 0u8..8 {
330        let (cs, ras, cas, we) = match executor_op_to_cmd(op) {
331            Some(cmd) => ddr2_pins(cmd),
332            None => (ncs, nras, ncas, nwe), // out-of-range ⇒ default arm holds NOP
333        };
334        checks.push_str(&format!(
335            "    chk(3'd{op}, 1'b1, 1'b{cs}, 1'b{ras}, 1'b{cas}, 1'b{we});\n"
336        ));
337    }
338    format!(
339        r#"`timescale 1ns/1ps
340module tb_ddr2_cmd_encoder;
341  reg [2:0] op; reg issue;
342  wire cs_n, ras_n, cas_n, we_n;
343  integer errors = 0;
344  ddr2_cmd_encoder dut(.op(op), .issue(issue), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n), .we_n(we_n));
345  task chk(input [2:0] o, input i, input ecs, input eras, input ecas, input ewe);
346    begin
347      op=o; issue=i; #1;
348      if (cs_n!==ecs || ras_n!==eras || cas_n!==ecas || we_n!==ewe) begin
349        errors=errors+1;
350        $display("FAIL op=%0d issue=%0d got=%b%b%b%b exp=%b%b%b%b", o, i, cs_n, ras_n, cas_n, we_n, ecs, eras, ecas, ewe);
351      end
352    end
353  endtask
354  initial begin
355{checks}    if (errors==0) $display("PASS: ddr2_cmd_encoder matches the JEDEC truth table for all ops");
356    $finish;
357  end
358endmodule
359"#
360    )
361}
362
363/// A behavioral testbench for `ddr2_init_fsm`: a bus monitor records each isolated real command
364/// (NOP/DESELECT skipped) the FSM issues during power-up, and asserts the recorded log equals the
365/// JEDEC command sequence (generated from [`ddr2_init_sequence`]) and that `init_done` asserts.
366pub fn emit_ddr2_init_testbench(t: &DramTimings, cke_low_cycles: u64) -> String {
367    let seq = ddr2_init_sequence(t, cke_low_cycles);
368    let mut exp: Vec<(u8, u8)> = Vec::new();
369    for s in &seq {
370        match s {
371            InitStep::PrechargeAll => exp.push((4, 0)),
372            InitStep::Refresh => exp.push((5, 0)),
373            InitStep::LoadMode { reg, .. } => exp.push((6, reg.ba())),
374            _ => {}
375        }
376    }
377    let expn = exp.len();
378    let mut expinit = String::new();
379    for (i, (c, b)) in exp.iter().enumerate() {
380        expinit.push_str(&format!("    exp_code[{i}]=4'd{c}; exp_ba[{i}]=2'd{b};\n"));
381    }
382    format!(
383        r#"`timescale 1ns/1ps
384module tb_ddr2_init_fsm;
385  reg clk=0; always #5 clk=~clk;
386  reg rst=1;
387  wire cke, cs_n, ras_n, cas_n, we_n; wire [1:0] ba; wire [12:0] a; wire init_done;
388  ddr2_init_fsm dut(.clk(clk), .rst(rst), .cke(cke), .cs_n(cs_n), .ras_n(ras_n),
389                    .cas_n(cas_n), .we_n(we_n), .ba(ba), .a(a), .init_done(init_done));
390  localparam EXP_N = {expn};
391  reg [3:0] exp_code [0:EXP_N-1];
392  reg [1:0] exp_ba [0:EXP_N-1];
393  reg [3:0] got_code [0:255];
394  reg [1:0] got_ba [0:255];
395  integer ngot; reg was_real; reg [3:0] cc; integer i; integer errors;
396  // Record each isolated real command (real commands are separated by NOPs in the sequence).
397  always @(posedge clk) begin
398    if (rst) begin ngot=0; was_real=0; end
399    else if (!init_done) begin
400      if (cs_n===1'b0 && {{ras_n,cas_n,we_n}} !== 3'b111) begin
401        case ({{ras_n,cas_n,we_n}})
402          3'b011: cc=4'd1; 3'b101: cc=4'd2; 3'b100: cc=4'd3;
403          3'b010: cc=4'd4; 3'b001: cc=4'd5; 3'b000: cc=4'd6; default: cc=4'd0;
404        endcase
405        if (!was_real) begin got_code[ngot]=cc; got_ba[ngot]=ba; ngot=ngot+1; end
406        was_real=1;
407      end else was_real=0;
408    end
409  end
410  initial begin
411{expinit}    errors=0;
412    repeat (3) @(posedge clk); rst=0;
413    i=0;
414    while (i<20000 && !init_done) begin @(posedge clk); i=i+1; end
415    if (!init_done) begin errors=errors+1; $display("FAIL: init_done never asserted"); end
416    if (ngot !== EXP_N) begin errors=errors+1; $display("FAIL: recorded %0d cmds, expected %0d", ngot, EXP_N); end
417    for (i=0; i<EXP_N; i=i+1) begin
418      if (i<ngot) begin
419        if (got_code[i]!==exp_code[i] || (exp_code[i]==4'd6 && got_ba[i]!==exp_ba[i])) begin
420          errors=errors+1;
421          $display("FAIL: cmd %0d got code=%0d ba=%0d exp code=%0d ba=%0d", i, got_code[i], got_ba[i], exp_code[i], exp_ba[i]);
422        end
423      end
424    end
425    if (errors==0) $display("PASS: ddr2_init_fsm walks the JEDEC init sequence (%0d cmds) and asserts init_done", ngot);
426    $finish;
427  end
428endmodule
429"#
430    )
431}
432
433// ── Phase 1d: read/write transaction command sequencing ───────────────────────────────
434//
435// The controller turns each read/write request into a single-transaction, closed-page command
436// sequence — ACT(row) → RD/WR → PRE — honoring tRCD (ACT→CAS), tRAS (ACT→PRE), the burst duration,
437// and (writes) tWR (write recovery before PRE). This is the LEGAL counterpart to the ComputeDRAM
438// streams: the SAME ACT/CAS/PRE vocabulary, proven `super::dram::check_legal`-LEGAL here and
439// `check_legal`-ILLEGAL there. One command bus, two planes — normal memory vs in-DRAM compute.
440
441/// Emit the controller's per-request command sequence for one closed-page DDR2 access.
442pub fn emit_transaction(t: &DramTimings, bank: u32, row: u64, write: bool) -> Vec<TimedCmd> {
443    let cas = t.trcd;
444    let cas_end = cas + t.data_cycles;
445    let cas_cmd = if write {
446        DramCmd::Wr { bank }
447    } else {
448        DramCmd::Rd { bank }
449    };
450    let mut pre = t.tras.max(cas_end); // tRAS from ACT, and after the burst completes
451    if write {
452        pre = pre.max(cas_end + t.twr); // write recovery before the page closes
453    }
454    vec![
455        TimedCmd {
456            at: 0,
457            cmd: DramCmd::Act { bank, row },
458        },
459        TimedCmd {
460            at: cas,
461            cmd: cas_cmd,
462        },
463        TimedCmd {
464            at: pre,
465            cmd: DramCmd::Pre { bank },
466        },
467    ]
468}
469
470// ── Phase 1d: the controller FSM + behavioral DDR2 model ───────────────────────────────
471//
472// The controller turns a read/write request into an ACT → RD/WR → PRE transaction with a
473// counter-driven schedule that mirrors `emit_transaction` (CAS at tRCD, PRE at max(tRAS, burst,
474// +tWR for writes), tRP before the next ACT), plus periodic all-bank auto-refresh. It is proven
475// under iverilog against `ddr2_bank_model` — a behavioral DDR2 array that stores/returns data AND
476// enforces tRCD/tRAS/tRP/tWR (a buggy controller trips `model_error`, so the round-trip proof can
477// never pass on illegal timing). The analog DQ/DQS SERDES + SSTL18 I/O + IDELAY calibration are the
478// board-gated layer BELOW this; here the request/response and DDR command bus are digital.
479
480fn ddr2_bake(tpl: &str, t: &DramTimings) -> String {
481    tpl.replace("%TRCD%", &t.trcd.to_string())
482        .replace("%TRAS%", &t.tras.to_string())
483        .replace("%TRP%", &t.trp.to_string())
484        .replace("%TWR%", &t.twr.to_string())
485        .replace("%CL%", &t.trcd.to_string())
486        .replace("%BURST%", &t.data_cycles.to_string())
487        .replace("%TREFI%", &t.trefi_pb.to_string())
488        .replace("%TRFC%", &t.trfc_pb.to_string())
489}
490
491/// Emit `ddr2_ctrl` — the request-driven DDR2 controller FSM (timings baked from the table).
492pub fn emit_ddr2_controller_verilog(t: &DramTimings) -> String {
493    ddr2_bake(
494        r#"module ddr2_ctrl(
495  input clk, input rst,
496  input req_valid, input req_we, input [25:0] req_addr, input [15:0] req_wdata,
497  output req_ready,
498  output reg resp_valid, output reg [15:0] resp_rdata,
499  output reg cke,
500  output cs_n, output ras_n, output cas_n, output we_n,
501  output [2:0] ba, output [12:0] a, output odt,
502  output [15:0] wr_data, output wr_en,
503  input [15:0] rd_data, input rd_valid
504);
505  localparam TRCD=%TRCD%, TRAS=%TRAS%, TRP=%TRP%, TWR=%TWR%, BURST=%BURST%;
506  localparam PRE_RD = (TRAS > (TRCD+BURST)) ? TRAS : (TRCD+BURST);
507  localparam PRE_WR = (TRAS > (TRCD+BURST+TWR)) ? TRAS : (TRCD+BURST+TWR);
508  localparam integer TREFI=%TREFI%;
509  localparam TRFC=%TRFC%;
510  localparam S_IDLE=2'd0, S_BUSY=2'd1, S_PREW=2'd2, S_REF=2'd3;
511
512  reg [1:0] state;
513  reg cur_we; reg [2:0] cur_bank; reg [12:0] cur_row; reg [9:0] cur_col; reg [15:0] cur_wd;
514  reg [15:0] t; reg [31:0] refi; reg refresh_pending; reg [15:0] reft;
515  wire [15:0] pre_t = cur_we ? PRE_WR : PRE_RD;
516
517  reg r_cs, r_ras, r_cas, r_we, r_wren, r_odt;
518  reg [2:0] r_ba; reg [12:0] r_a;
519  assign cs_n=r_cs; assign ras_n=r_ras; assign cas_n=r_cas; assign we_n=r_we;
520  assign ba=r_ba; assign a=r_a; assign odt=r_odt; assign wr_en=r_wren; assign wr_data=cur_wd;
521  assign req_ready = (state==S_IDLE) && !refresh_pending;
522
523  // Commands are a pure function of the REGISTERED state + counter (no req_valid dependence),
524  // so a missed accept can never leave a stray ACT on the bus.
525  always @(*) begin
526    r_cs=1'b0; r_ras=1'b1; r_cas=1'b1; r_we=1'b1; r_wren=1'b0; r_odt=1'b0; r_ba=cur_bank; r_a=13'b0;
527    case (state)
528      S_BUSY: begin
529        if (t==0) begin r_ras=1'b0; r_cas=1'b1; r_we=1'b1; r_a=cur_row; end // ACT
530        else if (t==TRCD) begin
531          r_ras=1'b1; r_cas=1'b0; r_we=cur_we?1'b0:1'b1; r_a={3'b000, cur_col};
532          if (cur_we) begin r_wren=1'b1; r_odt=1'b1; end
533        end else if (t==pre_t) begin
534          r_ras=1'b0; r_cas=1'b1; r_we=1'b0; // PRE
535        end
536      end
537      S_REF: if (reft==0) begin r_ras=1'b0; r_cas=1'b0; r_we=1'b1; end // REF (banks idle)
538      default: ;
539    endcase
540  end
541
542  always @(posedge clk) begin
543    if (rst) begin
544      state<=S_IDLE; t<=0; refi<=0; refresh_pending<=1'b0; reft<=0; cke<=1'b1;
545      resp_valid<=1'b0; resp_rdata<=0; cur_we<=0; cur_bank<=0; cur_row<=0; cur_col<=0; cur_wd<=0;
546    end else begin
547      cke<=1'b1; resp_valid<=1'b0;
548      if (refi >= TREFI-1) begin refresh_pending<=1'b1; refi<=0; end else refi<=refi+1;
549      case (state)
550        S_IDLE: begin
551          if (refresh_pending) begin reft<=0; refresh_pending<=1'b0; state<=S_REF; end
552          else if (req_valid) begin
553            cur_we<=req_we; cur_bank<=req_addr[12:10]; cur_row<=req_addr[25:13];
554            cur_col<=req_addr[9:0]; cur_wd<=req_wdata; t<=0; state<=S_BUSY;
555          end
556        end
557        S_BUSY: begin
558          if (!cur_we && rd_valid) begin resp_valid<=1'b1; resp_rdata<=rd_data; end
559          if (t==pre_t) state<=S_PREW;
560          t<=t+1;
561        end
562        S_PREW: begin
563          if (t >= pre_t + TRP) begin state<=S_IDLE; t<=0; end else t<=t+1;
564        end
565        S_REF: begin
566          if (reft >= TRFC-1) state<=S_IDLE; else reft<=reft+1;
567        end
568      endcase
569    end
570  end
571endmodule
572"#,
573        t,
574    )
575}
576
577/// Emit `ddr2_bank_model` — a behavioral DDR2 array that stores/returns data (CL read latency) AND
578/// enforces tRCD/tRAS/tRP/tWR, raising `model_error` on any violation (the round-trip checker).
579pub fn emit_ddr2_bank_model_verilog(t: &DramTimings) -> String {
580    ddr2_bake(
581        r#"module ddr2_bank_model(
582  input clk, input rst,
583  input cke, input cs_n, input ras_n, input cas_n, input we_n,
584  input [2:0] ba, input [12:0] a, input odt,
585  input [15:0] wr_data, input wr_en,
586  output reg [15:0] rd_data, output reg rd_valid,
587  output reg model_error
588);
589  localparam TRCD=%TRCD%, TRAS=%TRAS%, TRP=%TRP%, TWR=%TWR%, CL=%CL%, BURST=%BURST%;
590  reg openb [0:7];
591  reg [12:0] open_row [0:7];
592  reg [31:0] last_act [0:7];
593  reg [31:0] last_pre [0:7];
594  reg had_pre [0:7];
595  reg [31:0] wr_end [0:7];
596  reg had_wr [0:7];
597  reg [31:0] mc;
598  reg rd_pending; reg [15:0] rd_cnt; reg [15:0] rd_val;
599  reg [15:0] mem [0:2047];
600  integer bk;
601
602  always @(posedge clk) begin
603    if (rst) begin
604      mc<=0; model_error<=1'b0; rd_valid<=1'b0; rd_data<=0; rd_pending<=1'b0; rd_cnt<=0; rd_val<=0;
605      for (bk=0;bk<8;bk=bk+1) begin
606        openb[bk]<=1'b0; had_pre[bk]<=1'b0; had_wr[bk]<=1'b0;
607        last_act[bk]<=0; last_pre[bk]<=0; wr_end[bk]<=0; open_row[bk]<=0;
608      end
609    end else begin
610      mc<=mc+1;
611      rd_valid<=1'b0;
612      if (rd_pending) begin
613        if (rd_cnt<=1) begin rd_valid<=1'b1; rd_data<=rd_val; rd_pending<=1'b0; end
614        else rd_cnt<=rd_cnt-1;
615      end
616      if (cke && !cs_n) begin
617        case ({ras_n,cas_n,we_n})
618          3'b011: begin // ACT
619            if (openb[ba]) begin model_error<=1'b1; $display("FAIL: ACT on open bank %0d @%0d", ba, mc); end
620            if (had_pre[ba] && (mc - last_pre[ba] < TRP)) begin model_error<=1'b1; $display("FAIL: tRP bank %0d @%0d", ba, mc); end
621            openb[ba]<=1'b1; open_row[ba]<=a; last_act[ba]<=mc;
622          end
623          3'b101: begin // READ
624            if (!openb[ba]) begin model_error<=1'b1; $display("FAIL: READ closed bank %0d @%0d", ba, mc); end
625            else if (mc - last_act[ba] < TRCD) begin model_error<=1'b1; $display("FAIL: tRCD read bank %0d @%0d", ba, mc); end
626            rd_pending<=1'b1; rd_cnt<=CL; rd_val<=mem[{ba, open_row[ba][3:0], a[3:0]}];
627          end
628          3'b100: begin // WRITE
629            if (!openb[ba]) begin model_error<=1'b1; $display("FAIL: WRITE closed bank %0d @%0d", ba, mc); end
630            else if (mc - last_act[ba] < TRCD) begin model_error<=1'b1; $display("FAIL: tRCD write bank %0d @%0d", ba, mc); end
631            mem[{ba, open_row[ba][3:0], a[3:0]}] <= wr_data;
632            wr_end[ba]<=mc+BURST; had_wr[ba]<=1'b1;
633          end
634          3'b010: begin // PRECHARGE
635            if (!openb[ba]) begin model_error<=1'b1; $display("FAIL: PRE closed bank %0d @%0d", ba, mc); end
636            else if (mc - last_act[ba] < TRAS) begin model_error<=1'b1; $display("FAIL: tRAS bank %0d @%0d", ba, mc); end
637            if (had_wr[ba] && (mc < wr_end[ba] + TWR)) begin model_error<=1'b1; $display("FAIL: tWR bank %0d @%0d", ba, mc); end
638            openb[ba]<=1'b0; last_pre[ba]<=mc; had_pre[ba]<=1'b1; had_wr[ba]<=1'b0;
639          end
640          3'b001: begin // REFRESH
641            for (bk=0;bk<8;bk=bk+1) if (openb[bk]) begin model_error<=1'b1; $display("FAIL: REF open bank %0d @%0d", bk, mc); end
642          end
643          default: ;
644        endcase
645      end
646    end
647  end
648endmodule
649"#,
650        t,
651    )
652}
653
654/// Emit `tb_ddr2_ctrl` — writes `n_words` (distinct bank/col addresses), reads them back, and asserts
655/// each read equals the written data with no `model_error`. The access pattern is generated here.
656pub fn emit_ddr2_controller_testbench(t: &DramTimings, n_words: u32) -> String {
657    let mut init = String::new();
658    for i in 0..n_words {
659        let addr = (i << 10) | i; // bank = i, col = i, row = 0 → distinct model index
660        let data = 0xA000u32 + i;
661        init.push_str(&format!(
662            "    addr[{i}]=26'h{addr:x}; data[{i}]=16'h{data:x};\n"
663        ));
664    }
665    let tpl = r#"`timescale 1ns/1ps
666module tb_ddr2_ctrl;
667  reg clk=0; always #5 clk=~clk;
668  reg rst=1;
669  reg req_valid; reg req_we; reg [25:0] req_addr; reg [15:0] req_wdata;
670  wire req_ready; wire resp_valid; wire [15:0] resp_rdata;
671  wire cke,cs_n,ras_n,cas_n,we_n,odt,wr_en,rd_valid;
672  wire [2:0] ba; wire [12:0] a; wire [15:0] wr_data; wire [15:0] rd_data; wire model_error;
673  ddr2_ctrl ctrl(.clk(clk),.rst(rst),.req_valid(req_valid),.req_we(req_we),.req_addr(req_addr),
674    .req_wdata(req_wdata),.req_ready(req_ready),.resp_valid(resp_valid),.resp_rdata(resp_rdata),
675    .cke(cke),.cs_n(cs_n),.ras_n(ras_n),.cas_n(cas_n),.we_n(we_n),.ba(ba),.a(a),.odt(odt),
676    .wr_data(wr_data),.wr_en(wr_en),.rd_data(rd_data),.rd_valid(rd_valid));
677  ddr2_bank_model memmodel(.clk(clk),.rst(rst),.cke(cke),.cs_n(cs_n),.ras_n(ras_n),.cas_n(cas_n),
678    .we_n(we_n),.ba(ba),.a(a),.odt(odt),.wr_data(wr_data),.wr_en(wr_en),.rd_data(rd_data),
679    .rd_valid(rd_valid),.model_error(model_error));
680  localparam N=%N%;
681  reg [25:0] addr [0:N-1];
682  reg [15:0] data [0:N-1];
683  reg [15:0] rdout;
684  integer i; integer errors; integer timeout;
685  task do_write(input [25:0] ad, input [15:0] d); begin
686    @(posedge clk);
687    req_valid <= 1'b1; req_we <= 1'b1; req_addr <= ad; req_wdata <= d;
688    @(posedge clk);
689    while (!req_ready) @(posedge clk);
690    req_valid <= 1'b0;
691  end endtask
692  task do_read(input [25:0] ad); begin
693    @(posedge clk);
694    req_valid <= 1'b1; req_we <= 1'b0; req_addr <= ad; req_wdata <= 0;
695    @(posedge clk);
696    while (!req_ready) @(posedge clk);
697    req_valid <= 1'b0;
698    timeout=0;
699    while (!resp_valid && timeout<500) begin @(posedge clk); timeout=timeout+1; end
700    rdout=resp_rdata;
701  end endtask
702  initial begin
703%INIT%    req_valid=0; req_we=0; req_addr=0; req_wdata=0; errors=0;
704    repeat (4) @(posedge clk); rst=0;
705    for (i=0;i<N;i=i+1) do_write(addr[i], data[i]);
706    for (i=0;i<N;i=i+1) begin
707      do_read(addr[i]);
708      if (rdout !== data[i]) begin errors=errors+1; $display("FAIL: word %0d addr %h got %h exp %h", i, addr[i], rdout, data[i]); end
709    end
710    if (model_error) begin errors=errors+1; $display("FAIL: behavioral model flagged a timing violation"); end
711    if (errors==0) $display("PASS: ddr2_ctrl round-tripped %0d words through the behavioral DDR2 model, no timing violations", N);
712    $finish;
713  end
714endmodule
715"#;
716    ddr2_bake(tpl, t)
717        .replace("%N%", &n_words.to_string())
718        .replace("%INIT%", &init)
719}
720
721#[cfg(test)]
722#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/ddr2_phy/tests.rs"]
723mod tests;