Skip to main content

logicaffeine_compile/codegen_sva/
ddr2_traffic.rs

1//! ddr2_traffic — the K7 traffic: one stream of 64-bit words that every DDR2 controller of the comparative bench
2//! writes to the same part and reads back, the device and testbenches it runs on, and the judgement of a run's log.
3//!
4//! The stream is words, not any controller's requests. A word index is LiteDRAM's native-port address under
5//! ROW_BANK_COL, and [`place`] puts it in a bank, a row and four x16 columns. Each controller carries the stream
6//! through its own request interface — whole words, or a 16-bit port's four parts — and its testbench logs every
7//! command the bus carried, every request the controller accepted and every word it delivered. [`judge`] holds the
8//! bus to the JEDEC checker ([`super::ddr2_jedec::check_bus`]), matches each request to the column command that
9//! served it, checks that every word read back is the word written, and measures: request → column command
10//! latency, the stream's span, and the bus's.
11//!
12//! The operating point is the Nexys A7's DDR2, Micron MT47H64M16HR-25, at DDR2-400: the clock LiteDRAM's
13//! controller drives it at from a 100 MHz system clock at 1:2, and a 1:1 controller from 200 MHz.
14
15use super::ddr2_dfi::emit_ddr2_dfi_controller_verilog;
16use super::ddr2_jedec::{check_bus, command_of_pins, BusCmd, BusFacts, Ddr2Grade, Ddr2Timing};
17use super::ddr2_phy::{emit_ddr2_controller_verilog, Ddr2Cmd};
18use std::collections::{BTreeSet, HashMap};
19
20/// The K7 part.
21pub const K7_GRADE: Ddr2Grade = Ddr2Grade::MT47H64M16_25;
22/// The K7 DRAM clock period: DDR2-400.
23pub const K7_TCK_PS: u64 = 5000;
24/// The rows of every bank the scattered stream draws its words from.
25pub const SCATTER_ROWS: u32 = 64;
26/// The DRAM clocks between the probe's reads: more than any K7 controller needs to serve one read with a refresh in
27/// its way, so each read meets the controller idle.
28pub const PROBE_EVERY: u64 = 128;
29
30/// The K7 part clocked at the K7 operating point, burst length `bl`.
31pub fn k7_timing(bl: u64) -> Ddr2Timing {
32    K7_GRADE.at(K7_TCK_PS, bl).expect("the K7 operating point is inside the grade's range")
33}
34
35/// `ddr2_ctrl` as the K7 bench builds, synthesises and drives it: the LOGOS DDR2 controller at the K7 part's table,
36/// burst length 8.
37pub fn k7_ddr2_ctrl_verilog() -> String {
38    emit_ddr2_controller_verilog(&k7_timing(8).dram_timings())
39}
40
41/// `ddr2_dfi_ctrl` as the K7 bench builds, synthesises and drives it: the LOGOS DFI 1:2 controller at the K7 part's
42/// rules, burst length 4.
43pub fn k7_dfi_ctrl_verilog() -> String {
44    emit_ddr2_dfi_controller_verilog(&k7_timing(4))
45}
46
47/// A controller the K7 traffic gate drives: its name in the K7 design table, the burst length its mode register
48/// sets, and how its requests carry a word.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct Controller {
51    pub name: &'static str,
52    pub bl: u64,
53    pub iface: Interface,
54}
55
56/// LOGOS's `ddr2_ctrl` (a 16-bit request port, BL8), LOGOS's `ddr2_dfi_ctrl` and LiteDRAM's controller (each a 64-bit
57/// native port, BL4 at 1:2).
58pub const K7_CONTROLLERS: [Controller; 3] = [
59    Controller { name: "k7_logos_ddr2_ctrl", bl: 8, iface: Interface { parts: 4, cols_per_part: 1 } },
60    Controller { name: "k7_logos_dfi_ctrl", bl: 4, iface: Interface { parts: 1, cols_per_part: 4 } },
61    Controller { name: "k7_litedram_ctrl", bl: 4, iface: Interface { parts: 1, cols_per_part: 4 } },
62];
63
64pub fn k7_controller(name: &str) -> Option<Controller> {
65    K7_CONTROLLERS.iter().copied().find(|c| c.name == name)
66}
67
68/// Words per row of the part: a 2 KB x16 row holds 256 words of 8 bytes.
69const ROW_WORDS: u32 = 256;
70const BANKS: u32 = 8;
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum Pattern {
74    /// Every word from 0 up: whole rows, bank after bank.
75    Sequential,
76    /// Distinct words drawn at random from [`SCATTER_ROWS`] rows of every bank.
77    Scattered,
78    /// The scattered words written as a stream, then read one at a time, each read presented [`PROBE_EVERY`] DRAM
79    /// clocks after the one before: the latency of a read that finds the controller idle, and how much it varies.
80    Probe,
81}
82
83impl Pattern {
84    pub fn label(self) -> &'static str {
85        match self {
86            Pattern::Sequential => "sequential",
87            Pattern::Scattered => "scattered",
88            Pattern::Probe => "probe",
89        }
90    }
91
92    pub fn parse(s: &str) -> Option<Pattern> {
93        [Pattern::Sequential, Pattern::Scattered, Pattern::Probe].into_iter().find(|p| p.label() == s)
94    }
95}
96
97/// The stream: `words` words written in order, then read back in the same order, each read presented as soon as the
98/// controller took the one before, or for the probe [`PROBE_EVERY`] clocks after it was presented.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct Traffic {
101    pub pattern: Pattern,
102    pub words: u32,
103}
104
105/// Where a word lives: its bank, its row, and the first of its four x16 columns.
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub struct Place {
108    pub bank: u8,
109    pub row: u16,
110    pub col: u16,
111}
112
113/// The place of word `w`: ROW_BANK_COL, the column word in the low eight bits, the bank above, the row above that.
114pub fn place(w: u32) -> Place {
115    assert!(w < 1 << 24, "word {w} is past the part's 2^24 words");
116    Place { bank: ((w / ROW_WORDS) % BANKS) as u8, row: (w / (ROW_WORDS * BANKS)) as u16, col: ((w % ROW_WORDS) * 4) as u16 }
117}
118
119/// The value written to word `w`: SplitMix64's output for `w`, a bijection, so every word's value is its own.
120pub fn word_value(w: u32) -> u64 {
121    let mut z = u64::from(w).wrapping_add(0x9E37_79B9_7F4A_7C15);
122    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
123    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
124    z ^ (z >> 31)
125}
126
127impl Traffic {
128    /// The words in request order.
129    pub fn order(&self) -> Vec<u32> {
130        match self.pattern {
131            Pattern::Sequential => (0..self.words).collect(),
132            Pattern::Scattered | Pattern::Probe => {
133                let region = SCATTER_ROWS * BANKS * ROW_WORDS;
134                assert!(self.words <= region / 2, "{} words crowd a region of {region}", self.words);
135                let mut s = 0x6A09_E667_F3BC_C909u64;
136                let mut seen = BTreeSet::new();
137                let mut out = Vec::with_capacity(self.words as usize);
138                while out.len() < self.words as usize {
139                    s ^= s << 13;
140                    s ^= s >> 7;
141                    s ^= s << 17;
142                    let w = (s % u64::from(region)) as u32;
143                    if seen.insert(w) {
144                        out.push(w);
145                    }
146                }
147                out
148            }
149        }
150    }
151
152    /// The DRAM clocks between read presentations: [`PROBE_EVERY`] for the probe, 0 (as soon as taken) otherwise.
153    pub fn every(&self) -> u64 {
154        if self.pattern == Pattern::Probe {
155            PROBE_EVERY
156        } else {
157            0
158        }
159    }
160
161    /// Every (bank, row) the stream touches.
162    pub fn rows(&self) -> BTreeSet<(u8, u16)> {
163        self.order().into_iter().map(|w| (place(w).bank, place(w).row)).collect()
164    }
165
166    /// The row-address bits a device must store for this stream: enough for its highest row.
167    pub fn row_bits(&self) -> u32 {
168        let top = self.rows().iter().map(|&(_, r)| u32::from(r)).max().unwrap_or(0);
169        (u32::BITS - top.leading_zeros()).max(1)
170    }
171
172    /// The testbench's ROM: per word in order, its index (6 hex digits) and value (16).
173    pub fn rom(&self) -> String {
174        self.order().into_iter().map(|w| format!("{w:06x}{:016x}\n", word_value(w))).collect()
175    }
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub enum Kind {
180    Write,
181    Read,
182}
183
184/// A request the requester presented or the controller accepted: the clock, write or read, the word, and which part.
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct Request {
187    pub cycle: u64,
188    pub kind: Kind,
189    pub word: u32,
190    pub part: u8,
191}
192
193/// A word the controller delivered back.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct Delivery {
196    pub cycle: u64,
197    pub word: u32,
198    pub value: u64,
199}
200
201/// A run's log: the bus, the requests as presented and as accepted, the deliveries, and the closing line (its clock
202/// and the testbench's error count). Clocks are DRAM clocks from the end of reset.
203#[derive(Clone, Debug, Default, PartialEq, Eq)]
204pub struct Log {
205    pub bus: Vec<BusCmd>,
206    pub presented: Vec<Request>,
207    pub requests: Vec<Request>,
208    pub deliveries: Vec<Delivery>,
209    pub end: Option<(u64, u64)>,
210}
211
212/// Read a log: `C <clock> <cs#> <ras#> <cas#> <we#> <ba> <a>`, `P <clock> <W|R> <word> <part>` (presented),
213/// `Q <clock> <W|R> <word> <part>` (accepted), `D <clock> <word> <hex value>`, `END <clock> <errors>`; blank lines
214/// pass.
215pub fn read_log(text: &str) -> Result<Log, String> {
216    let mut log = Log::default();
217    for (n, line) in text.lines().enumerate() {
218        let at = |what: &str| format!("line {}: {what}: {line:?}", n + 1);
219        let f: Vec<&str> = line.split_whitespace().collect();
220        let Some(&tag) = f.first() else { continue };
221        let num = |i: usize| -> Result<u64, String> {
222            f.get(i).and_then(|s| s.parse::<u64>().ok()).ok_or_else(|| at(&format!("field {i} is not a number")))
223        };
224        let arity = |k: usize| if f.len() == k { Ok(()) } else { Err(at(&format!("{} fields, {tag} has {k}", f.len()))) };
225        match tag {
226            "C" => {
227                arity(8)?;
228                let pin = |i: usize| num(i).and_then(|v| u8::try_from(v).map_err(|_| at("a pin level")));
229                let cmd = command_of_pins(pin(2)?, pin(3)?, pin(4)?, pin(5)?).ok_or_else(|| at("pins that encode no command"))?;
230                let ba = u8::try_from(num(6)?).map_err(|_| at("a bank address"))?;
231                let a = u16::try_from(num(7)?).map_err(|_| at("an address"))?;
232                log.bus.push(BusCmd { cycle: num(1)?, cmd, ba, a });
233            }
234            "P" | "Q" => {
235                arity(5)?;
236                let kind = match f[2] {
237                    "W" => Kind::Write,
238                    "R" => Kind::Read,
239                    _ => return Err(at("a request is W or R")),
240                };
241                let word = u32::try_from(num(3)?).map_err(|_| at("a word index"))?;
242                let part = u8::try_from(num(4)?).map_err(|_| at("a part"))?;
243                let r = Request { cycle: num(1)?, kind, word, part };
244                if tag == "P" {
245                    log.presented.push(r);
246                } else {
247                    log.requests.push(r);
248                }
249            }
250            "D" => {
251                arity(4)?;
252                let value = u64::from_str_radix(f[3], 16).map_err(|_| at("a value that is not hex"))?;
253                let word = u32::try_from(num(2)?).map_err(|_| at("a word index"))?;
254                log.deliveries.push(Delivery { cycle: num(1)?, word, value });
255            }
256            "END" => {
257                arity(3)?;
258                if log.end.is_some() {
259                    return Err(at("a second END"));
260                }
261                log.end = Some((num(1)?, num(2)?));
262            }
263            _ => return Err(at("an unknown tag")),
264        }
265    }
266    Ok(log)
267}
268
269/// How a controller's requests carry a word: in `parts` requests, part p at the word's first column plus
270/// p × `cols_per_part`.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct Interface {
273    pub parts: u8,
274    pub cols_per_part: u16,
275}
276
277/// A latency distribution in DRAM clocks: count, extremes, nearest-rank median and 99th percentile, mean.
278#[derive(Clone, Copy, Debug, PartialEq)]
279pub struct Stats {
280    pub n: u64,
281    pub min: u64,
282    pub max: u64,
283    pub p50: u64,
284    pub p99: u64,
285    pub mean: f64,
286}
287
288impl Stats {
289    fn of(mut v: Vec<u64>) -> Stats {
290        if v.is_empty() {
291            return Stats { n: 0, min: 0, max: 0, p50: 0, p99: 0, mean: 0.0 };
292        }
293        v.sort_unstable();
294        let n = v.len();
295        let rank = |q: f64| v[((q * n as f64).ceil() as usize).clamp(1, n) - 1];
296        Stats { n: n as u64, min: v[0], max: v[n - 1], p50: rank(0.5), p99: rank(0.99), mean: v.iter().sum::<u64>() as f64 / n as f64 }
297    }
298}
299
300/// What a run measured. `stream_cycles` runs from the first accepted request to the last delivered word;
301/// `payload_cycles` is what the stream needs of an x16 DDR bus (two clocks a word, each way); `bus_span` runs from
302/// the first command to the last beat; `row_hits` counts column commands that found their row already open by an
303/// earlier column command; the write and read latencies run from a request's acceptance to the column command that
304/// served it; `read_service` from the presentation of a word's first read request to the word's delivery, and
305/// `read_bus` to the last beat of the word's read data on the DRAM bus — the same span without any PHY's pipeline.
306#[derive(Clone, Debug, PartialEq)]
307pub struct Judgement {
308    pub words: u32,
309    pub requests: u32,
310    pub bus: BusFacts,
311    pub bus_span: u64,
312    pub stream_cycles: u64,
313    pub payload_cycles: u64,
314    pub row_hits: u64,
315    pub write_latency: Stats,
316    pub read_latency: Stats,
317    pub read_service: Stats,
318    pub read_bus: Stats,
319}
320
321/// Judge a run of `traffic` through a controller with request interface `iface`, its bus held to `t`.
322pub fn judge(t: &Ddr2Timing, traffic: &Traffic, iface: Interface, log: &Log) -> Result<Judgement, String> {
323    let Some((_, errors)) = log.end else {
324        return Err("the testbench did not finish: the log has no END".into());
325    };
326    if errors != 0 {
327        return Err(format!("the testbench counted {errors} errors"));
328    }
329    let order = traffic.order();
330    let want: Vec<(Kind, u32, u8)> = [Kind::Write, Kind::Read]
331        .into_iter()
332        .flat_map(|k| order.iter().flat_map(move |&w| (0..iface.parts).map(move |p| (k, w, p))))
333        .collect();
334    for (seen, what) in [(&log.presented, "presented"), (&log.requests, "accepted")] {
335        if seen.len() != want.len() {
336            return Err(format!("{} requests {what}, the stream makes {}", seen.len(), want.len()));
337        }
338        for (i, (r, &(kind, word, part))) in seen.iter().zip(&want).enumerate() {
339            if (r.kind, r.word, r.part) != (kind, word, part) {
340                return Err(format!("request {i} {what} is {:?} word {} part {}, the stream's is {kind:?} word {word} part {part}", r.kind, r.word, r.part));
341            }
342        }
343    }
344    for (i, (p, q)) in log.presented.iter().zip(&log.requests).enumerate() {
345        if q.cycle < p.cycle {
346            return Err(format!("request {i} accepted at cycle {}, before it was presented at {}", q.cycle, p.cycle));
347        }
348    }
349    let facts = check_bus(t, &log.bus).map_err(|v| format!("the bus breaks JEDEC DDR2: {v}"))?;
350    let rows = traffic.rows();
351    for c in log.bus.iter().filter(|c| c.cmd == Ddr2Cmd::Active) {
352        if !rows.contains(&(c.ba, c.a)) {
353            return Err(format!("an ACT at cycle {} to bank {} row {}, outside the stream", c.cycle, c.ba, c.a));
354        }
355    }
356
357    let mut cas: HashMap<(bool, u8, u16, u16), Vec<(u64, u64, bool)>> = HashMap::new();
358    for e in &facts.cas {
359        cas.entry((e.write, e.bank, e.row, e.col)).or_default().push((e.cycle, e.data_at, false));
360    }
361    let (mut wl, mut rl) = (Vec::new(), Vec::new());
362    let mut read_end: HashMap<u32, u64> = HashMap::new();
363    for r in &log.requests {
364        let p = place(r.word);
365        let key = (r.kind == Kind::Write, p.bank, p.row, p.col + u16::from(r.part) * iface.cols_per_part);
366        let served = cas.get_mut(&key).and_then(|v| v.iter_mut().find(|(c, _, used)| !*used && *c >= r.cycle));
367        let Some((at, data_at, used)) = served else {
368            return Err(format!("{:?} of word {} part {} accepted at cycle {} never reached the bus", r.kind, r.word, r.part, r.cycle));
369        };
370        *used = true;
371        match r.kind {
372            Kind::Write => wl.push(*at - r.cycle),
373            Kind::Read => {
374                rl.push(*at - r.cycle);
375                let end = read_end.entry(r.word).or_insert(0);
376                *end = (*end).max(*data_at + t.bl / 2);
377            }
378        }
379    }
380
381    if log.deliveries.len() != order.len() {
382        return Err(format!("{} words delivered, the stream reads {}", log.deliveries.len(), order.len()));
383    }
384    let reads_from = order.len() * usize::from(iface.parts);
385    let mut service = Vec::with_capacity(order.len());
386    let mut bus = Vec::with_capacity(order.len());
387    for (i, (d, &w)) in log.deliveries.iter().zip(&order).enumerate() {
388        if d.word != w {
389            return Err(format!("delivery {i} is word {}, the stream's next is word {w}", d.word));
390        }
391        if d.value != word_value(w) {
392            return Err(format!("word {w} read back {:016x}, the stream wrote {:016x}", d.value, word_value(w)));
393        }
394        let asked = log.presented[reads_from + i * usize::from(iface.parts)].cycle;
395        if d.cycle < asked {
396            return Err(format!("word {w} delivered at cycle {}, before its read was presented at {asked}", d.cycle));
397        }
398        service.push(d.cycle - asked);
399        bus.push(read_end[&w] - asked);
400    }
401
402    let mut hit = [false; 8];
403    let mut row_hits = 0;
404    for c in &log.bus {
405        match c.cmd {
406            Ddr2Cmd::Active => hit[usize::from(c.ba)] = false,
407            Ddr2Cmd::Read | Ddr2Cmd::Write => {
408                if std::mem::replace(&mut hit[usize::from(c.ba)], true) {
409                    row_hits += 1;
410                }
411            }
412            _ => {}
413        }
414    }
415    let first = log.requests.first().map_or(0, |r| r.cycle);
416    let last = log.deliveries.last().map_or(0, |d| d.cycle);
417    let word_clocks = 64 / (2 * u64::from(t.grade.dq_bits));
418    Ok(Judgement {
419        words: traffic.words,
420        requests: log.requests.len() as u32,
421        bus_span: facts.end - facts.first_cycle,
422        bus: facts,
423        stream_cycles: last - first,
424        payload_cycles: 2 * u64::from(traffic.words) * word_clocks,
425        row_hits,
426        write_latency: Stats::of(wl),
427        read_latency: Stats::of(rl),
428        read_service: Stats::of(service),
429        read_bus: Stats::of(bus),
430    })
431}
432
433/// `ddr2_device`: the part as a testbench sees it through an ideal PHY. It keeps each bank's open row and stores
434/// 16-bit columns for `ROWB` row-address bits; a WRITE takes its burst (with a per-beat mask, 1 = not written) on
435/// the command's clock, a READ returns its burst CL clocks later, both in the sequential burst order that wraps
436/// within the burst's aligned block. An ACT past the stored rows, or a column command to a bank with no open row,
437/// raises `model_error`. Bus timing is not its business: the log's JEDEC checker holds the bus to the rules.
438pub fn emit_ddr2_device_verilog(t: &Ddr2Timing) -> String {
439    r#"// ddr2_device — the DDR2 part through an ideal PHY: each bank's open row, 16-bit columns for ROWB row bits,
440// a WRITE burst taken with its command (wr_mask 1 = beat not written), a READ burst returned CL clocks later,
441// sequential burst order wrapping within the aligned block.
442module ddr2_device #(parameter ROWB = 1) (
443  input clk, input rst,
444  input cke, input cs_n, input ras_n, input cas_n, input we_n,
445  input [2:0] ba, input [12:0] a,
446  input [%BL%*16-1:0] wr_burst, input [%BL%-1:0] wr_mask,
447  output reg [%BL%*16-1:0] rd_burst, output reg rd_valid,
448  output reg model_error
449);
450  localparam BL = %BL%, CL = %CL%;
451  reg [15:0] mem [0:(8 << (ROWB + 10)) - 1];
452  reg openb [0:7];
453  reg [12:0] open_row [0:7];
454  reg [BL*16-1:0] pipe [1:CL];
455  reg pv [1:CL];
456  reg [BL*16-1:0] burst;
457  integer i, k;
458  function [ROWB+12:0] slot(input [2:0] b, input [12:0] r, input [9:0] c);
459    slot = {b, r[ROWB-1:0], c};
460  endfunction
461  function [9:0] beat(input [9:0] c, input [3:0] n);
462    beat = (c & ~(BL - 1)) | ((c + n) & (BL - 1));
463  endfunction
464  always @(posedge clk) begin
465    if (rst) begin
466      model_error <= 1'b0; rd_valid <= 1'b0; rd_burst <= 0;
467      for (k = 0; k < 8; k = k + 1) begin openb[k] <= 1'b0; open_row[k] <= 13'd0; end
468      for (k = 1; k <= CL; k = k + 1) begin pv[k] <= 1'b0; pipe[k] <= 0; end
469    end else begin
470      rd_valid <= pv[CL];
471      rd_burst <= pipe[CL];
472      for (k = CL; k > 1; k = k - 1) begin pv[k] <= pv[k - 1]; pipe[k] <= pipe[k - 1]; end
473      pv[1] <= 1'b0;
474      if (cke && !cs_n) case ({ras_n, cas_n, we_n})
475        3'b011: begin
476          if (a >= (13'd1 << ROWB)) begin model_error <= 1'b1; $display("FAIL: ACT to row %0d past the %0d stored rows", a, 1 << ROWB); end
477          openb[ba] <= 1'b1; open_row[ba] <= a;
478        end
479        3'b100: begin
480          if (!openb[ba]) begin model_error <= 1'b1; $display("FAIL: WRITE to bank %0d with no open row", ba); end
481          for (i = 0; i < BL; i = i + 1)
482            if (!wr_mask[i]) mem[slot(ba, open_row[ba], beat(a[9:0], i))] <= wr_burst[16 * i +: 16];
483        end
484        3'b101: begin
485          if (!openb[ba]) begin model_error <= 1'b1; $display("FAIL: READ from bank %0d with no open row", ba); end
486          for (i = 0; i < BL; i = i + 1) burst[16 * i +: 16] = mem[slot(ba, open_row[ba], beat(a[9:0], i))];
487          pv[1] <= 1'b1; pipe[1] <= burst;
488        end
489        3'b010: if (a[10]) begin for (k = 0; k < 8; k = k + 1) openb[k] <= 1'b0; end else openb[ba] <= 1'b0;
490        default: ;
491      endcase
492    end
493  end
494endmodule
495"#
496    .replace("%BL%", &t.bl.to_string())
497    .replace("%CL%", &t.cl.to_string())
498}
499
500/// The clocks a testbench waits for a stream before it gives up: far past any controller's need.
501fn limit(traffic: &Traffic) -> u64 {
502    u64::from(traffic.words) * (8 * 64 + traffic.every()) + 100_000
503}
504
505/// The half period of the testbench clock, in ns, for a DRAM clock of `tck_ps` driven 1:1.
506fn half_ns(tck_ps: u64) -> String {
507    format!("{}", tck_ps as f64 / 2000.0)
508}
509
510/// `tb_qor_dram` for `ddr2_ctrl`: the stream through its 16-bit request port, four requests a word (part p at
511/// column 4 × the column word + p), the device behind it at burst length `t.bl` with every beat but the addressed
512/// one masked. A request is presented as soon as the one before was taken; for the probe, a word's first read waits
513/// until [`Traffic::every`] clocks after the word before's, and the reads start two of those after the last write.
514/// It reads `words.hex` ([`Traffic::rom`]) and writes `dram.log` ([`read_log`]).
515pub fn emit_ddr2_ctrl_traffic_testbench(t: &Ddr2Timing, traffic: &Traffic) -> String {
516    r#"`timescale 1ns/1ps
517// tb_qor_dram — the K7 word stream through ddr2_ctrl's 16-bit request port into ddr2_device; logs the bus, the
518// presented and accepted requests and the delivered words to dram.log.
519module tb_qor_dram;
520  localparam N = %N%, BL = %BL%, REQS = 8 * N;
521  localparam [63:0] LIMIT = 64'd%LIMIT%, EVERY = 64'd%EVERY%;
522  reg clk = 1'b0;
523  always #%HALF% clk = ~clk;
524  reg rst = 1'b1;
525  reg [87:0] rom [0:N-1];
526  integer f, errors;
527  reg [63:0] cyc, next_at;
528  reg [31:0] r, got;
529  reg [47:0] acc;
530  reg shown;
531  wire req_ready, resp_valid, cke, cs_n, ras_n, cas_n, we_n, odt, wr_en, rd_valid, model_error;
532  wire [15:0] resp_rdata, wr_data;
533  wire [2:0] ba;
534  wire [12:0] a;
535  wire [BL*16-1:0] rd_burst;
536  wire [31:0] word_i = (r % (4 * N)) / 4;
537  wire [1:0] part = r[1:0];
538  wire [87:0] entry = rom[word_i];
539  wire req_we = r < 4 * N;
540  wire due = req_we || part != 2'd0 || EVERY == 0 || cyc >= next_at;
541  wire req_valid = r < REQS && due;
542  wire [25:0] req_addr = {entry[87:75], entry[74:72], entry[71:64], part};
543  wire [15:0] req_wdata = entry[16 * part +: 16];
544  wire [87:0] back = rom[got / 4];
545  ddr2_ctrl dut(.clk(clk), .rst(rst), .req_valid(req_valid), .req_we(req_we), .req_addr(req_addr),
546    .req_wdata(req_wdata), .req_ready(req_ready), .resp_valid(resp_valid), .resp_rdata(resp_rdata),
547    .cke(cke), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n), .we_n(we_n), .ba(ba), .a(a), .odt(odt),
548    .wr_data(wr_data), .wr_en(wr_en), .rd_data(rd_burst[15:0]), .rd_valid(rd_valid));
549  ddr2_device #(.ROWB(%ROWB%)) mem(.clk(clk), .rst(rst), .cke(cke), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n),
550    .we_n(we_n), .ba(ba), .a(a), .wr_burst({{((BL - 1) * 16){1'b0}}, wr_data}), .wr_mask({{(BL - 1){1'b1}}, 1'b0}),
551    .rd_burst(rd_burst), .rd_valid(rd_valid), .model_error(model_error));
552  always @(posedge clk) begin
553    if (rst) begin
554      cyc <= 64'd0; r <= 32'd0; got <= 32'd0; acc <= 48'd0; shown <= 1'b0; next_at <= 64'd0;
555    end else begin
556      cyc <= cyc + 64'd1;
557      if (!cs_n && !(ras_n && cas_n && we_n))
558        $fwrite(f, "C %0d %0d %0d %0d %0d %0d %0d\n", cyc, cs_n, ras_n, cas_n, we_n, ba, a);
559      if (req_valid && !shown)
560        $fwrite(f, "P %0d %c %0d %0d\n", cyc, req_we ? 8'd87 : 8'd82, entry[87:64], part);
561      if (req_valid && req_ready) begin
562        $fwrite(f, "Q %0d %c %0d %0d\n", cyc, req_we ? 8'd87 : 8'd82, entry[87:64], part);
563        r <= r + 32'd1;
564        shown <= 1'b0;
565        if (r == 4 * N - 1) next_at <= cyc + 2 * EVERY;
566        else if (!req_we && part == 2'd0) next_at <= next_at + EVERY;
567      end else if (req_valid) shown <= 1'b1;
568      if (resp_valid) begin
569        if (got[1:0] == 2'd3) begin
570          $fwrite(f, "D %0d %0d %h\n", cyc, back[87:64], {resp_rdata, acc});
571          if ({resp_rdata, acc} !== back[63:0]) begin
572            errors = errors + 1;
573            $display("FAIL: word %0d read back %h, wrote %h", back[87:64], {resp_rdata, acc}, back[63:0]);
574          end
575        end else acc[16 * got[1:0] +: 16] <= resp_rdata;
576        got <= got + 32'd1;
577      end
578    end
579  end
580  initial begin
581    $readmemh("words.hex", rom);
582    f = $fopen("dram.log", "w");
583    errors = 0;
584    repeat (4) @(posedge clk);
585    rst <= 1'b0;
586    wait (got == 4 * N || cyc >= LIMIT);
587    repeat (32) @(posedge clk);
588    if (got != 4 * N) begin errors = errors + 1; $display("FAIL: %0d of %0d parts delivered by cycle %0d", got, 4 * N, cyc); end
589    if (model_error) errors = errors + 1;
590    $fwrite(f, "END %0d %0d\n", cyc, errors);
591    $fclose(f);
592    if (errors == 0) $display("PASS: %0d words through ddr2_ctrl in %0d cycles", N, cyc);
593    else $display("FAIL: %0d errors", errors);
594    $finish;
595  end
596endmodule
597"#
598    .replace("%N%", &traffic.words.to_string())
599    .replace("%BL%", &t.bl.to_string())
600    .replace("%LIMIT%", &limit(traffic).to_string())
601    .replace("%EVERY%", &traffic.every().to_string())
602    .replace("%HALF%", &half_ns(t.tck_ps))
603    .replace("%ROWB%", &traffic.row_bits().to_string())
604}
605
606/// One DFI phase's signals as LiteDRAM's controller and its SDRAMPHYModel both name them, with their widths at the
607/// K7 configuration (13 address bits, 8 banks, a 32-bit phase of the x16 part at 1:2). `act_n`, DDR4's alone, is tied
608/// off on both sides.
609const DFI: [(&str, u32); 15] = [
610    ("address", 13),
611    ("bank", 3),
612    ("cas_n", 1),
613    ("cs_n", 1),
614    ("ras_n", 1),
615    ("we_n", 1),
616    ("cke", 1),
617    ("odt", 1),
618    ("reset_n", 1),
619    ("wrdata", 32),
620    ("wrdata_en", 1),
621    ("wrdata_mask", 4),
622    ("rddata_en", 1),
623    ("rddata", 32),
624    ("rddata_valid", 1),
625];
626
627/// The DFI of both phases as wires, and as the port connections that put them between controller and model.
628fn dfi_wiring() -> (String, String) {
629    let mut wires = String::new();
630    let mut ports = Vec::new();
631    for p in 0..2 {
632        for (sig, bits) in DFI {
633            let range = if bits > 1 { format!("[{}:0] ", bits - 1) } else { String::new() };
634            wires.push_str(&format!("  wire {range}dfi_p{p}_{sig};\n"));
635            ports.push(format!(".dfi_p{p}_{sig}(dfi_p{p}_{sig})"));
636        }
637        ports.push(format!(".dfi_p{p}_act_n(1'b1)"));
638    }
639    (wires, ports.join(", "))
640}
641
642/// `tb_qor_dram` for LiteDRAM: the stream through the native port of `litedram_ctrl`, the controller exactly as the
643/// bench synthesises it, its DFI wired to `litedram_phy_model`, LiteDRAM's own SDRAMPHYModel (both from
644/// `litedram_controller.py`); one command a word, write data offered for every accepted write in order, read data
645/// always accepted, reads presented as for `ddr2_ctrl`. The system clock runs at half the DRAM clock, and the log
646/// counts DRAM clocks: DFI phase p of system clock k is DRAM clock 2k + p, a request presented or accepted at system
647/// clock k is logged at 2k. It reads `words.hex` and writes `dram.log`.
648pub fn emit_litedram_traffic_testbench(t: &Ddr2Timing, traffic: &Traffic) -> String {
649    let (_, ports) = dfi_wiring();
650    let instance = format!(
651        "  litedram_ctrl ctrl(.sys_clk(clk), .sys_rst(rst),
652    .cmd_valid(cmd_valid), .cmd_ready(cmd_ready), .cmd_first(1'b0), .cmd_last(1'b0), .cmd_payload_we(cmd_we),
653    .cmd_payload_addr(centry[87:64]),
654    .wdata_valid(wdata_valid), .wdata_ready(wdata_ready), .wdata_first(1'b0), .wdata_last(1'b0),
655    .wdata_payload_data(wentry[63:0]), .wdata_payload_we(8'hff),
656    .rdata_valid(rdata_valid), .rdata_ready(1'b1), .rdata_first(1'b0), .rdata_last(1'b0), .rdata_payload_data(rdata),
657    {ports});"
658    );
659    dfi_traffic_testbench(t, traffic, "LiteDRAM's native port: litedram_ctrl as the bench synthesises it", "LiteDRAM", &instance)
660}
661
662/// `tb_qor_dram` for `ddr2_dfi_ctrl`: LiteDRAM's testbench with the LOGOS controller in `litedram_ctrl`'s place, on
663/// the same native port and DFI, against the same SDRAMPHYModel; it has no stream framing (first/last) and no read
664/// back-pressure, which LiteDRAM's crossbar does not read either.
665pub fn emit_ddr2_dfi_ctrl_traffic_testbench(t: &Ddr2Timing, traffic: &Traffic) -> String {
666    let (_, ports) = dfi_wiring();
667    let dfi: Vec<&str> = ports.split(", ").filter(|p| !p.contains("_act_n(")).collect();
668    let instance = format!(
669        "  ddr2_dfi_ctrl ctrl(.sys_clk(clk), .sys_rst(rst),
670    .cmd_valid(cmd_valid), .cmd_ready(cmd_ready), .cmd_payload_we(cmd_we), .cmd_payload_addr(centry[87:64]),
671    .wdata_valid(wdata_valid), .wdata_ready(wdata_ready), .wdata_payload_data(wentry[63:0]), .wdata_payload_we(8'hff),
672    .rdata_valid(rdata_valid), .rdata_payload_data(rdata),
673    {});",
674        dfi.join(", ")
675    );
676    dfi_traffic_testbench(t, traffic, "ddr2_dfi_ctrl's native port", "ddr2_dfi_ctrl", &instance)
677}
678
679/// The DFI traffic testbench around a controller `instance` on the native port (`what` names it in the header,
680/// `who` in the verdict).
681fn dfi_traffic_testbench(t: &Ddr2Timing, traffic: &Traffic, what: &str, who: &str, instance: &str) -> String {
682    let (wires, ports) = dfi_wiring();
683    r#"`timescale 1ns/1ps
684// tb_qor_dram — the K7 word stream through %WHAT%, its
685// DFI wired to LiteDRAM's SDRAMPHYModel; logs the DFI commands, the presented and accepted requests and the delivered
686// words to dram.log in DRAM clocks (phase p of system clock k is 2k + p).
687module tb_qor_dram;
688  localparam N = %N%;
689  localparam [63:0] LIMIT = 64'd%LIMIT%, EVERY = 64'd%EVERY%;
690  reg clk = 1'b0;
691  always #%HALF% clk = ~clk;
692  reg rst = 1'b1;
693  reg [87:0] rom [0:N-1];
694  integer f, errors;
695  reg [63:0] k, next_at;
696  reg [31:0] r, wsent, got;
697  reg shown;
698  wire cmd_ready, wdata_ready, rdata_valid;
699  wire [63:0] rdata;
700  wire [87:0] centry = rom[r % N];
701  wire [87:0] wentry = rom[wsent];
702  wire [87:0] back = rom[got];
703  wire cmd_we = r < N;
704  wire cmd_valid = r < 2 * N && (cmd_we || EVERY == 0 || 2 * k >= next_at);
705  wire wdata_valid = wsent < (r < N ? r : N);
706%WIRES%%INSTANCE%
707  litedram_phy_model part(.sys_clk(clk), .sys_rst(rst),
708    %PORTS%);
709  always @(posedge clk) begin
710    if (rst) begin
711      k <= 64'd0; r <= 32'd0; wsent <= 32'd0; got <= 32'd0; shown <= 1'b0; next_at <= 64'd0;
712    end else begin
713      k <= k + 64'd1;
714      if (!dfi_p0_cs_n && !(dfi_p0_ras_n && dfi_p0_cas_n && dfi_p0_we_n))
715        $fwrite(f, "C %0d %0d %0d %0d %0d %0d %0d\n", 2 * k, dfi_p0_cs_n, dfi_p0_ras_n, dfi_p0_cas_n, dfi_p0_we_n, dfi_p0_bank, dfi_p0_address);
716      if (!dfi_p1_cs_n && !(dfi_p1_ras_n && dfi_p1_cas_n && dfi_p1_we_n))
717        $fwrite(f, "C %0d %0d %0d %0d %0d %0d %0d\n", 2 * k + 1, dfi_p1_cs_n, dfi_p1_ras_n, dfi_p1_cas_n, dfi_p1_we_n, dfi_p1_bank, dfi_p1_address);
718      if (cmd_valid && !shown) $fwrite(f, "P %0d %c %0d 0\n", 2 * k, cmd_we ? 8'd87 : 8'd82, centry[87:64]);
719      if (cmd_valid && cmd_ready) begin
720        $fwrite(f, "Q %0d %c %0d 0\n", 2 * k, cmd_we ? 8'd87 : 8'd82, centry[87:64]);
721        r <= r + 32'd1;
722        shown <= 1'b0;
723        if (r == N - 1) next_at <= 2 * k + 2 * EVERY;
724        else if (!cmd_we) next_at <= next_at + EVERY;
725      end else if (cmd_valid) shown <= 1'b1;
726      if (wdata_valid && wdata_ready) wsent <= wsent + 32'd1;
727      if (rdata_valid) begin
728        $fwrite(f, "D %0d %0d %h\n", 2 * k, back[87:64], rdata);
729        if (rdata !== back[63:0]) begin
730          errors = errors + 1;
731          $display("FAIL: word %0d read back %h, wrote %h", back[87:64], rdata, back[63:0]);
732        end
733        got <= got + 32'd1;
734      end
735    end
736  end
737  initial begin
738    $readmemh("words.hex", rom);
739    f = $fopen("dram.log", "w");
740    errors = 0;
741    repeat (4) @(posedge clk);
742    rst <= 1'b0;
743    wait (got == N || k >= LIMIT);
744    repeat (32) @(posedge clk);
745    if (got != N) begin errors = errors + 1; $display("FAIL: %0d of %0d words delivered by system clock %0d", got, N, k); end
746    $fwrite(f, "END %0d %0d\n", 2 * k, errors);
747    $fclose(f);
748    if (errors == 0) $display("PASS: %0d words through %WHO% in %0d system clocks", N, k);
749    else $display("FAIL: %0d errors", errors);
750    $finish;
751  end
752endmodule
753"#
754    .replace("%WHAT%", what)
755    .replace("%WHO%", who)
756    .replace("%INSTANCE%", instance)
757    .replace("%N%", &traffic.words.to_string())
758    .replace("%LIMIT%", &limit(traffic).to_string())
759    .replace("%EVERY%", &traffic.every().to_string())
760    .replace("%HALF%", &half_ns(2 * t.tck_ps))
761    .replace("%WIRES%", &wires)
762    .replace("%PORTS%", &ports)
763}
764
765#[cfg(test)]
766#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/ddr2_traffic/tests.rs"]
767mod tests;