1use 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
20pub const K7_GRADE: Ddr2Grade = Ddr2Grade::MT47H64M16_25;
22pub const K7_TCK_PS: u64 = 5000;
24pub const SCATTER_ROWS: u32 = 64;
26pub const PROBE_EVERY: u64 = 128;
29
30pub 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
35pub fn k7_ddr2_ctrl_verilog() -> String {
38 emit_ddr2_controller_verilog(&k7_timing(8).dram_timings())
39}
40
41pub fn k7_dfi_ctrl_verilog() -> String {
44 emit_ddr2_dfi_controller_verilog(&k7_timing(4))
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct Controller {
51 pub name: &'static str,
52 pub bl: u64,
53 pub iface: Interface,
54}
55
56pub 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
68const ROW_WORDS: u32 = 256;
70const BANKS: u32 = 8;
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum Pattern {
74 Sequential,
76 Scattered,
78 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct Traffic {
101 pub pattern: Pattern,
102 pub words: u32,
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub struct Place {
108 pub bank: u8,
109 pub row: u16,
110 pub col: u16,
111}
112
113pub 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
119pub 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 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 pub fn every(&self) -> u64 {
154 if self.pattern == Pattern::Probe {
155 PROBE_EVERY
156 } else {
157 0
158 }
159 }
160
161 pub fn rows(&self) -> BTreeSet<(u8, u16)> {
163 self.order().into_iter().map(|w| (place(w).bank, place(w).row)).collect()
164 }
165
166 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 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct Delivery {
196 pub cycle: u64,
197 pub word: u32,
198 pub value: u64,
199}
200
201#[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
212pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct Interface {
273 pub parts: u8,
274 pub cols_per_part: u16,
275}
276
277#[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#[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
321pub 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
433pub 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
500fn limit(traffic: &Traffic) -> u64 {
502 u64::from(traffic.words) * (8 * 64 + traffic.every()) + 100_000
503}
504
505fn half_ns(tck_ps: u64) -> String {
507 format!("{}", tck_ps as f64 / 2000.0)
508}
509
510pub 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
606const 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
627fn 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
642pub 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
662pub 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
679fn 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;