Skip to main content

logicaffeine_compile/codegen_sva/
ddr2_jedec.rs

1//! ddr2_jedec — a DDR2 part's datasheet timing at one clock, and the JESD79-2 command rules a bus trace is held to.
2//!
3//! [`Ddr2Grade`] is a speed grade as its datasheet prints it: nanosecond minimums and the CAS latency each clock
4//! allows. [`Ddr2Grade::at`] clocks it: every minimum rounds up to whole clocks (tRRD, tWR, tWTR and tRTP to no fewer
5//! than two), every maximum rounds down, and tRPA is tRP + tCK as an eight-bank part's is. [`check_bus`] replays a
6//! pin-level command trace against the clocked rules and returns the first violation, or the trace's facts: what
7//! was issued, and every column access with the cycle its data reaches the bus.
8//!
9//! Every DDR2 controller the comparative bench drives is judged by this one checker over the commands it put on the
10//! bus, so the controllers are compared under the same rules rather than each under its own model. The rules are
11//! JESD79-2's with additive latency 0, write latency CL − 1, and bursts never cut short. A READ or WRITE with A10 high
12//! precharges its bank itself: the precharge starts at the later of the clock an explicit PRECHARGE could have come
13//! and tRAS after the ACT, and the bank is ready tRP after it. The refresh debt counts from the trace's cycle 0, when
14//! the device is taken as freshly refreshed: by any cycle t it has had at least floor(t / tREFI) − 8 REFRESH commands,
15//! eight being the most JESD79-2 lets a controller postpone. A trace that writes a mode register is refused, not
16//! judged; that belongs to initialisation, not to the traffic the checker is for.
17
18use super::ddr2_phy::{ddr2_pins, Ddr2Cmd};
19use super::dram::DramTimings;
20use std::fmt;
21
22/// The REFRESH commands JESD79-2 lets a controller postpone: by any clock t it owes at least floor(t / tREFI) − this.
23pub const REFRESH_POSTPONE_MAX: u64 = 8;
24
25/// A DDR2 speed grade: its geometry, the CAS latency each clock period allows, and its datasheet minimums (and the
26/// tRAS and tREFI maximums), in picoseconds unless named in clocks.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Ddr2Grade {
29    pub part: &'static str,
30    pub banks: u32,
31    pub row_bits: u32,
32    pub col_bits: u32,
33    pub dq_bits: u32,
34    /// `(tCK floor, CL)` by rising CL: a CAS latency runs at any clock period from its floor up to `tck_max_ps`.
35    pub cl_floors: &'static [(u64, u64)],
36    pub tck_max_ps: u64,
37    pub trcd_ps: u64,
38    pub trp_ps: u64,
39    pub tras_ps: u64,
40    pub tras_max_ps: u64,
41    pub trc_ps: u64,
42    pub trrd_ps: u64,
43    pub tfaw_ps: u64,
44    pub twr_ps: u64,
45    pub twtr_ps: u64,
46    pub trtp_ps: u64,
47    pub trfc_ps: u64,
48    pub trefi_ps: u64,
49    pub tccd_ck: u64,
50}
51
52impl Ddr2Grade {
53    /// Micron MT47H64M16 (1 Gb, x16, 8 banks), speed grade -25: DDR2-800 at CL 6. The Nexys A7-100T carries this
54    /// part, MT47H64M16HR-25:H. Micron 1Gb DDR2 datasheet, Rev. V 6/10: Table 1 and Table 12's x16 rows.
55    pub const MT47H64M16_25: Ddr2Grade = Ddr2Grade {
56        part: "MT47H64M16-25",
57        cl_floors: &[(5000, 3), (3750, 4), (3000, 5), (2500, 6)],
58        trcd_ps: 15_000,
59        trp_ps: 15_000,
60        ..Self::MT47H64M16_25E
61    };
62
63    /// The same part's -25E grade: DDR2-800 at CL 5.
64    pub const MT47H64M16_25E: Ddr2Grade = Ddr2Grade {
65        part: "MT47H64M16-25E",
66        banks: 8,
67        row_bits: 13,
68        col_bits: 10,
69        dq_bits: 16,
70        cl_floors: &[(5000, 3), (3750, 4), (2500, 5)],
71        tck_max_ps: 8_000,
72        trcd_ps: 12_500,
73        trp_ps: 12_500,
74        tras_ps: 40_000,
75        tras_max_ps: 70_000_000,
76        trc_ps: 55_000,
77        trrd_ps: 10_000,
78        tfaw_ps: 45_000,
79        twr_ps: 15_000,
80        twtr_ps: 7_500,
81        trtp_ps: 7_500,
82        trfc_ps: 127_500,
83        trefi_ps: 7_800_000,
84        tccd_ck: 2,
85    };
86
87    /// The grade clocked at `tck_ps` with burst length `bl`, or why the part cannot run that way.
88    pub fn at(&self, tck_ps: u64, bl: u64) -> Result<Ddr2Timing, String> {
89        if bl != 4 && bl != 8 {
90            return Err(format!("{}: a DDR2 burst is 4 or 8 beats, not {bl}", self.part));
91        }
92        if tck_ps == 0 || tck_ps > self.tck_max_ps {
93            return Err(format!("{}: tCK {tck_ps} ps is outside the part's range (at most {} ps)", self.part, self.tck_max_ps));
94        }
95        let cl = self
96            .cl_floors
97            .iter()
98            .find(|&&(floor, _)| floor <= tck_ps)
99            .map(|&(_, cl)| cl)
100            .ok_or_else(|| format!("{}: tCK {tck_ps} ps is faster than any CAS latency it supports", self.part))?;
101        let up = |ps: u64| ps.div_ceil(tck_ps);
102        let up2 = |ps: u64| up(ps).max(2);
103        Ok(Ddr2Timing {
104            grade: *self,
105            tck_ps,
106            bl,
107            cl,
108            wl: cl - 1,
109            trcd: up(self.trcd_ps),
110            trp: up(self.trp_ps),
111            trpa: up(self.trp_ps + tck_ps),
112            tras: up(self.tras_ps),
113            tras_max: self.tras_max_ps / tck_ps,
114            trc: up(self.trc_ps),
115            trrd: up2(self.trrd_ps),
116            tfaw: up(self.tfaw_ps),
117            twr: up2(self.twr_ps),
118            twtr: up2(self.twtr_ps),
119            trtp: up2(self.trtp_ps),
120            trfc: up(self.trfc_ps),
121            trefi: self.trefi_ps / tck_ps,
122            tccd: self.tccd_ck,
123        })
124    }
125}
126
127/// A grade clocked: every rule in whole clocks. `wl` is `cl − 1`, `trpa` the precharge-all period.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub struct Ddr2Timing {
130    pub grade: Ddr2Grade,
131    pub tck_ps: u64,
132    pub bl: u64,
133    pub cl: u64,
134    pub wl: u64,
135    pub trcd: u64,
136    pub trp: u64,
137    pub trpa: u64,
138    pub tras: u64,
139    pub tras_max: u64,
140    pub trc: u64,
141    pub trrd: u64,
142    pub tfaw: u64,
143    pub twr: u64,
144    pub twtr: u64,
145    pub trtp: u64,
146    pub trfc: u64,
147    pub trefi: u64,
148    pub tccd: u64,
149}
150
151impl Ddr2Timing {
152    /// READ → PRECHARGE, same bank: AL + BL/2 − 2 + max(tRTP, 2).
153    pub fn read_to_precharge(&self) -> u64 {
154        self.bl / 2 - 2 + self.trtp.max(2)
155    }
156
157    /// WRITE → PRECHARGE, same bank: WL + BL/2 + tWR.
158    pub fn write_to_precharge(&self) -> u64 {
159        self.wl + self.bl / 2 + self.twr
160    }
161
162    /// WRITE → READ, any bank: WL + BL/2 + tWTR.
163    pub fn write_to_read(&self) -> u64 {
164        self.wl + self.bl / 2 + self.twtr
165    }
166
167    /// READ → WRITE, any bank: BL/2 + 2, one idle clock between the read data and the write data.
168    pub fn read_to_write(&self) -> u64 {
169        self.bl / 2 + 2
170    }
171
172    /// Column command → column command in one direction: max(tCCD, BL/2), so no burst is cut short.
173    pub fn cas_to_cas(&self) -> u64 {
174        self.tccd.max(self.bl / 2)
175    }
176
177    /// This clocked part as the DRAM table `ddr2_ctrl` and the planner read. The table counts write recovery from
178    /// the end of a burst issued with its command, so DDR2's write latency rides in `twr`; it has one turnaround for
179    /// both directions, so `tturn` takes the longer, WRITE→READ. WRITE→PRECHARGE and WRITE→READ are then exact,
180    /// READ→WRITE is never shorter, and READ→PRECHARGE is the burst, exact while tRTP is two clocks: the table has
181    /// no term for a longer one.
182    pub fn dram_timings(&self) -> DramTimings {
183        let bytes = u64::from(self.grade.dq_bits / 8);
184        DramTimings {
185            banks: self.grade.banks,
186            row_bytes: (1u64 << self.grade.col_bits) * bytes,
187            burst_bytes: self.bl * bytes,
188            data_cycles: self.bl / 2,
189            trcd: self.trcd,
190            trp: self.trp,
191            tras: self.tras,
192            trc: self.trc,
193            trrd: self.trrd,
194            tfaw: self.tfaw,
195            tccd: self.tccd,
196            trfc_pb: self.trfc,
197            trefi_pb: self.trefi,
198            twr: self.wl + self.twr,
199            tturn: (self.wl + self.twtr).max(2),
200            hot: false,
201        }
202    }
203}
204
205/// The command on the bus for `(CS#, RAS#, CAS#, WE#)` by the JEDEC truth table, `None` for the one reserved
206/// pattern (RAS# and CAS# high, WE# low) or a level that is not 0 or 1.
207pub fn command_of_pins(cs_n: u8, ras_n: u8, cas_n: u8, we_n: u8) -> Option<Ddr2Cmd> {
208    use Ddr2Cmd::*;
209    if [cs_n, ras_n, cas_n, we_n].iter().any(|&p| p > 1) {
210        return None;
211    }
212    if cs_n == 1 {
213        return Some(Deselect);
214    }
215    [Nop, Active, Read, Write, Precharge, Refresh, LoadMode].into_iter().find(|&c| ddr2_pins(c) == (cs_n, ras_n, cas_n, we_n))
216}
217
218/// One command as the bus carried it: the clock it was sampled on, the command its pins encode, the bank address
219/// and the address bus (the row for ACTIVATE, the column and A10 for READ and WRITE, A10 alone for PRECHARGE).
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub struct BusCmd {
222    pub cycle: u64,
223    pub cmd: Ddr2Cmd,
224    pub ba: u8,
225    pub a: u16,
226}
227
228/// A READ or WRITE, with the row its bank had open and the clock its first data beat occupies the bus.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub struct CasEvent {
231    pub cycle: u64,
232    pub write: bool,
233    pub bank: u8,
234    pub row: u16,
235    pub col: u16,
236    pub data_at: u64,
237}
238
239/// What a legal trace put on the bus. `end` is the clock after its last command or its last data beat, whichever
240/// is later; `data_cycles` the clocks its bursts held the data bus.
241#[derive(Clone, Debug, Default, PartialEq, Eq)]
242pub struct BusFacts {
243    pub activates: u64,
244    pub reads: u64,
245    pub writes: u64,
246    pub precharges: u64,
247    pub refreshes: u64,
248    pub first_cycle: u64,
249    pub last_cycle: u64,
250    pub end: u64,
251    pub data_cycles: u64,
252    pub cas: Vec<CasEvent>,
253}
254
255/// The first broken rule: the index of the command that broke it (the trace's length when the trace's end did), the
256/// clock, the rule's name, and the earlier command or limit it was measured against.
257#[derive(Clone, Debug, PartialEq, Eq)]
258pub struct Violation {
259    pub index: usize,
260    pub cycle: u64,
261    pub rule: &'static str,
262    pub detail: String,
263}
264
265impl fmt::Display for Violation {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        write!(f, "command {} at cycle {} breaks {}: {}", self.index, self.cycle, self.rule, self.detail)
268    }
269}
270
271#[derive(Clone, Copy)]
272struct Bank {
273    open: Option<u16>,
274    act_at: u64,
275    last_act: Option<u64>,
276    ready_at: u64,
277    ready_rule: &'static str,
278    last_rd: Option<u64>,
279    last_wr: Option<u64>,
280}
281
282/// Hold `trace` to the JESD79-2 command rules of `t`. NOP and DESELECT entries carry no command and are passed
283/// over; every other command must come on a later clock than the one before it.
284pub fn check_bus(t: &Ddr2Timing, trace: &[BusCmd]) -> Result<BusFacts, Violation> {
285    let nb = t.grade.banks as usize;
286    let mut banks = vec![
287        Bank { open: None, act_at: 0, last_act: None, ready_at: 0, ready_rule: "tRP", last_rd: None, last_wr: None };
288        nb
289    ];
290    let mut acts: Vec<u64> = Vec::new();
291    let (mut last_cas, mut last_rd, mut last_wr): (Option<u64>, Option<u64>, Option<u64>) = (None, None, None);
292    let mut refreshes = 0u64;
293    let mut prev: Option<u64> = None;
294    let mut facts = BusFacts::default();
295    let mut busy = 0u64;
296    let owed = |at: u64| (at / t.trefi).saturating_sub(REFRESH_POSTPONE_MAX);
297
298    for (index, c) in trace.iter().enumerate() {
299        if matches!(c.cmd, Ddr2Cmd::Nop | Ddr2Cmd::Deselect) {
300            continue;
301        }
302        let fail = |rule: &'static str, detail: String| Err(Violation { index, cycle: c.cycle, rule, detail });
303        if let Some(p) = prev {
304            if c.cycle <= p {
305                return fail("one command per cycle", format!("the command before it was at cycle {p}"));
306            }
307        } else {
308            facts.first_cycle = c.cycle;
309        }
310        prev = Some(c.cycle);
311        let paid = refreshes + u64::from(c.cmd == Ddr2Cmd::Refresh);
312        if owed(c.cycle) > paid {
313            return fail("refresh interval", format!("{paid} REFRESH by cycle {}, {} owed", c.cycle, owed(c.cycle)));
314        }
315        for (b, bank) in banks.iter().enumerate() {
316            if bank.open.is_some() && c.cycle - bank.act_at > t.tras_max {
317                return fail("tRAS max", format!("bank {b} open since cycle {} (at most {})", bank.act_at, t.tras_max));
318            }
319        }
320        let ba = usize::from(c.ba);
321        let banked = matches!(c.cmd, Ddr2Cmd::Active | Ddr2Cmd::Read | Ddr2Cmd::Write)
322            || (c.cmd == Ddr2Cmd::Precharge && c.a & (1 << 10) == 0);
323        if banked && ba >= nb {
324            return fail("address range", format!("bank {ba} of {nb}"));
325        }
326        match c.cmd {
327            Ddr2Cmd::Nop | Ddr2Cmd::Deselect => unreachable!("passed over above"),
328            Ddr2Cmd::LoadMode => return fail("mode register", "a mode-register write inside traffic".into()),
329            Ddr2Cmd::Active => {
330                if u32::from(c.a) >= 1 << t.grade.row_bits {
331                    return fail("address range", format!("row {} of {}", c.a, 1u32 << t.grade.row_bits));
332                }
333                let bank = banks[ba];
334                if let Some(row) = bank.open {
335                    return fail("ACT to an open bank", format!("bank {ba} has row {row} open since cycle {}", bank.act_at));
336                }
337                if c.cycle < bank.ready_at {
338                    return fail(bank.ready_rule, format!("bank {ba} is ready at cycle {}", bank.ready_at));
339                }
340                if let Some(la) = bank.last_act {
341                    if c.cycle < la + t.trc {
342                        return fail("tRC", format!("bank {ba} was activated at cycle {la}"));
343                    }
344                }
345                if let Some(&la) = acts.last() {
346                    if c.cycle < la + t.trrd {
347                        return fail("tRRD", format!("an ACT at cycle {la}"));
348                    }
349                }
350                if acts.len() >= 4 {
351                    let fourth = acts[acts.len() - 4];
352                    if c.cycle < fourth + t.tfaw {
353                        return fail("tFAW", format!("four ACTs since cycle {fourth}"));
354                    }
355                }
356                acts.push(c.cycle);
357                if acts.len() > 4 {
358                    acts.remove(0);
359                }
360                banks[ba] = Bank { open: Some(c.a), act_at: c.cycle, last_act: Some(c.cycle), last_rd: None, last_wr: None, ..bank };
361                facts.activates += 1;
362            }
363            Ddr2Cmd::Read | Ddr2Cmd::Write => {
364                let write = c.cmd == Ddr2Cmd::Write;
365                let auto = c.a & (1 << 10) != 0;
366                let col_mask = (1u16 << t.grade.col_bits) - 1;
367                if c.a & !(col_mask | (1 << 10)) != 0 {
368                    return fail("address range", format!("column address {:#x} past {} column bits", c.a, t.grade.col_bits));
369                }
370                let bank = banks[ba];
371                let Some(row) = bank.open else {
372                    return fail("CAS to an idle bank", format!("bank {ba} has no open row"));
373                };
374                if c.cycle < bank.act_at + t.trcd {
375                    return fail("tRCD", format!("bank {ba} was activated at cycle {}", bank.act_at));
376                }
377                if let Some(l) = last_cas {
378                    if c.cycle < l + t.cas_to_cas() {
379                        return fail("CAS to CAS", format!("a column command at cycle {l}"));
380                    }
381                }
382                if !write {
383                    if let Some(w) = last_wr {
384                        if c.cycle < w + t.write_to_read() {
385                            return fail("WRITE to READ", format!("a WRITE at cycle {w}"));
386                        }
387                    }
388                } else if let Some(r) = last_rd {
389                    if c.cycle < r + t.read_to_write() {
390                        return fail("READ to WRITE", format!("a READ at cycle {r}"));
391                    }
392                }
393                last_cas = Some(c.cycle);
394                let data_at = c.cycle + if write { t.wl } else { t.cl };
395                if write {
396                    last_wr = Some(c.cycle);
397                    banks[ba].last_wr = Some(c.cycle);
398                    facts.writes += 1;
399                } else {
400                    last_rd = Some(c.cycle);
401                    banks[ba].last_rd = Some(c.cycle);
402                    facts.reads += 1;
403                }
404                facts.data_cycles += t.bl / 2;
405                busy = busy.max(data_at + t.bl / 2 - 1);
406                facts.cas.push(CasEvent { cycle: c.cycle, write, bank: c.ba, row, col: c.a & col_mask, data_at });
407                if auto {
408                    let spacing = if write { t.write_to_precharge() } else { t.read_to_precharge() };
409                    let start = (c.cycle + spacing).max(bank.act_at + t.tras);
410                    let b = &mut banks[ba];
411                    b.open = None;
412                    if start + t.trp > b.ready_at {
413                        b.ready_at = start + t.trp;
414                        b.ready_rule = "tRP";
415                    }
416                }
417            }
418            Ddr2Cmd::Precharge => {
419                let all = c.a & (1 << 10) != 0;
420                let (period, rule) = if all { (t.trpa, "tRPA") } else { (t.trp, "tRP") };
421                for b in 0..nb {
422                    if !all && b != ba {
423                        continue;
424                    }
425                    let bank = banks[b];
426                    if bank.open.is_some() {
427                        if c.cycle < bank.act_at + t.tras {
428                            return fail("tRAS", format!("bank {b} was activated at cycle {}", bank.act_at));
429                        }
430                        if let Some(r) = bank.last_rd {
431                            if c.cycle < r + t.read_to_precharge() {
432                                return fail("READ to PRECHARGE", format!("bank {b} read at cycle {r}"));
433                            }
434                        }
435                        if let Some(w) = bank.last_wr {
436                            if c.cycle < w + t.write_to_precharge() {
437                                return fail("WRITE to PRECHARGE", format!("bank {b} written at cycle {w}"));
438                            }
439                        }
440                    } else if !all {
441                        continue;
442                    }
443                    let bank = &mut banks[b];
444                    bank.open = None;
445                    if c.cycle + period > bank.ready_at {
446                        bank.ready_at = c.cycle + period;
447                        bank.ready_rule = rule;
448                    }
449                }
450                facts.precharges += 1;
451            }
452            Ddr2Cmd::Refresh => {
453                for (b, bank) in banks.iter().enumerate() {
454                    if let Some(row) = bank.open {
455                        return fail("REFRESH with an open bank", format!("bank {b} has row {row} open"));
456                    }
457                    if c.cycle < bank.ready_at {
458                        return fail(bank.ready_rule, format!("bank {b} is ready at cycle {}", bank.ready_at));
459                    }
460                }
461                for bank in banks.iter_mut() {
462                    bank.ready_at = c.cycle + t.trfc;
463                    bank.ready_rule = "tRFC";
464                }
465                refreshes += 1;
466                facts.refreshes += 1;
467            }
468        }
469        facts.last_cycle = c.cycle;
470        busy = busy.max(c.cycle);
471    }
472    if prev.is_none() {
473        return Ok(facts);
474    }
475    let at_end = |rule: &'static str, detail: String| Err(Violation { index: trace.len(), cycle: busy, rule, detail });
476    if owed(busy) > refreshes {
477        return at_end("refresh interval", format!("{refreshes} REFRESH by the trace's last busy cycle {busy}, {} owed", owed(busy)));
478    }
479    for (b, bank) in banks.iter().enumerate() {
480        if bank.open.is_some() && busy - bank.act_at > t.tras_max {
481            return at_end("tRAS max", format!("bank {b} open since cycle {} at the trace's end", bank.act_at));
482        }
483    }
484    facts.end = busy + 1;
485    Ok(facts)
486}
487
488#[cfg(test)]
489#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/ddr2_jedec/tests.rs"]
490mod tests;