Skip to content
VLSI Mentor

DDR · Module 14

tRCD — RAS-to-CAS Delay

An accepted ACTIVATE constrains the earliest column command to that bank. It is a minimum separation, it is bank-local, and it is published in nanoseconds — three facts that between them explain most tRCD bugs.

Module 13 built a timing engine and deliberately left it empty. Every edge in it was called A, B or C, and every magnitude was a teaching value.

This module fills it in, and tRCD is the first edge:

An accepted ACTIVATE constrains the earliest legal column command to that bank.

That sentence looks like a definition you could have read anywhere. The work of this chapter is to show that it is not a definition at all — it is a five-part obligation, and every part of it is somewhere a real controller goes wrong.

1. The Obligation, in Five Parts

Chapter 13.1 §4 established that an obligation you cannot write in five parts is an obligation you cannot reason about. So here is tRCD, written out. This form recurs in all ten chapters of this module — it is the grammar, and it is worth getting used to.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  PARAMETER          tRCD

  1 triggering event   ACTIVATE accepted on bank B
  2 constrained event  a column command addressed to bank B
  3 resource           bank B                        BANK-LOCAL
  4 magnitude          published in NANOSECONDS, resolved to cycles
  5 sense              MINIMUM SEPARATION  (not a fixed latency)
  ─────────────────────────────────────────────────────────────────
  consequence        column command to bank B not before
                     act_cycle + ceil(tRCD / tCK)

Read that against the version you will hear in conversation — “tRCD is the RAS-to-CAS delay” — and notice what the short version omits. It does not say which bank. It does not say whether the delay is measured in cycles or in time. It does not say whether arriving later is also wrong. And it does not say whether the constraint applies to reads, writes, or both.

Each of those four omissions corresponds to a real class of controller bug, and §11 works through all of them.

2. Sense — Why It Is a Minimum and Not a Latency

Part 5 is the one people get wrong first, so take it first.

Chapter 13.3 §2 separated four constraint classes. tRCD is Class A, minimum separation: one-sided, with a lower bound and no upper bound.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ACT ──────── at least tRCD ────────▶  column command legal from here onward

Concretely: if tRCD resolves to 14 cycles and the ACTIVATE is accepted at cycle 100, a column command is illegal at cycles 100 through 113, legal at 114, and equally legal at 115, at 200, and at any later cycle. Nothing about tRCD says the controller must use the row promptly. A row may sit open and unused indefinitely as far as this obligation is concerned.

Contrast that with Chapter 14.9's CL, which is Class B, fixed latency — a two-sided commitment where being late is as much a failure as being early. The two parameters sit adjacent in every datasheet table and belong to different classes, which is the single most important structural fact in this module.

3. Why the Obligation Exists

One paragraph, architectural, and then we stop — because Modules 2 and 3 own this material and re-teaching it here would be duplication.

An ACTIVATE does not move data. It starts the process of making a row's contents available in the sensing circuitry: a wordline is driven, the cells disturb their bitlines, and the sense amplifiers resolve a small difference into logic levels. Chapter 3.5 §6 — “Selected Is Not Usable” — drew exactly this gap. A column command asks the device to select from that resolved data. Ask too early and there is nothing resolved to select.

That is the entire causal story, and the controller models none of it. Chapter 13.2 §1 drew the boundary: physics determines the magnitude, the datasheet publishes it, the controller counts. A controller holds a number from a speed-bin table and counts down. It has no visibility into the array and needs none.

4. Units — Nanoseconds, and Why That Matters

Here is a fact worth verifying rather than assuming, and it is verifiable from primary material.

That heading is the whole of §4's lesson. tRCD is a physical requirement, so it is published in absolute time. CL is a programmed pipeline depth, so it is published in cycles. The units are not a formatting choice; they tell you which kind of thing each parameter is.

And because tRCD is in nanoseconds, a controller must convert. Chapter 13.2 §3 did this work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  tRCD_cycles  =  ceil( tRCD_ns / tCK )

Take the verified 13.75 ns with a DDR4-3200 part. DDR4-3200 means 3200 MT/s, so CK is 1600 MHz and tCK is 0.625 ns — the relationship Chapter 4.2 §2 derived and this chapter does not re-derive.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  13.75 / 0.625  =  22.0  exactly        →  22 cycles

That one divides evenly, which is a coincidence worth not relying on. At a DDR4-2400 part, tCK is 0.8333 ns:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  13.75 / 0.8333  =  16.50               →  ceil  =  17 cycles

Seventeen, not sixteen, and not sixteen-and-a-half. Sixteen cycles would be 13.33 ns, which is less than 13.75 and therefore a violation. Chapter 13.2 §12 explained why a minimum rounds up unconditionally: there is nothing to average over.

Notice also that the same 13.75 ns costs 22 cycles at DDR4-3200 and 17 at DDR4-2400. More cycles at the faster clock, exactly as Chapter 13.2 §4 predicted. A tRCD quoted in cycles without a data rate attached is not a fact.

Chapter 13.2 §6 fixed the convention this whole module obeys. Restated for tRCD, because §35 of this module's governance requires every parameter to state it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ACTIVATE accepted (sampled) on cycle  N
  resolved requirement                  D  cycles

  column command sampled at M is LEGAL   ⟺   M - N >= D

  therefore   earliest legal M  =  N + D
              forbidden cycles  =  N, N+1, … , N+D-1     exactly D of them

With N of 100 and D of 22: cycles 100 through 121 are forbidden — that is 22 cycles, including the cycle the ACTIVATE itself was accepted on — and cycle 122 is the first legal column command.

The trigger cycle is forbidden because its separation is zero, and zero is not at least 22. That is not a special case; it falls straight out of the inequality.

6. Both Column Commands — and an Honest Limit

§1's part 2 said “a column command addressed to bank B.” Does that mean reads, writes, or both?

For DDR4, both. Micron's DDR4 documentation presents the bank-group column-spacing parameters with figure titles covering both read bursts and write bursts, and DDR4's speed-bin tables publish a single tRCD rather than separate read and write values. The ACTIVATE → column command edge in DDR4 is one obligation serving both column command types.

The tRCD obligation as an exchange over time. The controller issues an activate command naming a bank and row, which the device accepts on a sampling edge. That acceptance arms a bank-local obligation whose magnitude is the resolved tRCD in cycles. A column command offered to the same bank before the obligation expires is refused as too early, and the refusal names tRCD as the controlling constraint. Once the obligation expires, the same column command to the same bank is legal, and remains legal indefinitely afterwards because a minimum separation has no upper bound. A column command to a different bank is unaffected throughout, because the obligation is bank-local.ControllerBank B statetRCD obligationBank CACTIVATE row Rarm: D cycles, bankBcolumn cmd — tooearlyrefused: tRCDcontrolscolumn cmd —unaffectedexpires at N + Dcolumn cmd — legalnow

7. RTL — A Two-Event Minimum-Spacing Guard

Collision check first, as every chapter in this module will do.

Chapter 13.3's deadline_scoreboard tracks abstract obligations with timestamps and reduces them to a maximum. Chapter 13.2's timing_spec_resolver converts a specification into a cycle count. Chapter 11.4's write_recovery_guard counts down one obligation from a data event. Chapter 4.5's bank_group_spacer holds two group-scoped separations.

What none of them is, is a named parameter bound to a per-bank resource. deadline_scoreboard is scope-agnostic by design — its slots mean whatever you wire them to. This block is the first in the curriculum to make the binding explicit: this obligation, on this bank, from this event class.

That binding is the new educational responsibility, and it is not a trivial one — §11 shows that the majority of tRCD bugs are binding errors rather than arithmetic errors.

The engineering problem. Track, per bank, whether enough cycles have elapsed since that bank's last ACTIVATE to permit a column command, and report which bank is blocked and for how long.

Classification: controller-side sequential timing bookkeeping. Counts cycles, compares numbers.

What it models: per-bank cycle accounting for one minimum-separation obligation.

What it does not model: any physical process. There is no representation of wordlines, bitlines, sense amplification or charge anywhere in it. The magnitude arrives as an input from Chapter 13.2's resolver, which got it from a speed-bin table. The block would behave identically if the obligation described a disk seek.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
//  act_to_column_guard
//
//  CLASSIFICATION
//    Controller-side sequential timing bookkeeping. Counts cycles.
//
//  WHAT IT MODELS
//    The tRCD obligation of §1, bound to a concrete resource:
//      trigger     = ACTIVATE accepted on bank b
//      constrained = column command addressed to bank b
//      scope       = BANK-LOCAL  (one countdown per bank)
//      sense       = MINIMUM SEPARATION (lower bound only)
//
//  WHAT IT DOES NOT MODEL
//    Any physical process inside the array. No wordline, bitline,
//    sense-amplifier or charge behaviour appears here, because a
//    controller has no visibility into any of it. The magnitude is an
//    INPUT, resolved by Chapter 13.2's timing_spec_resolver from a
//    value published in nanoseconds.
//
//  SCOPE IS STRUCTURAL, NOT COMMENTED
//    The countdown is an ARRAY INDEXED BY BANK. That is the whole
//    defence against Chapter 13.3 §4's Bug 1: a single shared
//    countdown would be safe and would serialise banks that have no
//    obligation to each other. Indexing is the fix, and it is visible
//    in the declaration rather than asserted in a comment.
//
//  CONVENTION (Chapter 13.2 §6, obeyed exactly)
//    ACTIVATE accepted on cycle N with resolved magnitude D forbids a
//    column command on cycles N .. N+D-1 and permits it from N+D.
//    Since the command bus carries one command per cycle, cycle N
//    cannot host a competing column command, so the countdown is
//    loaded with D-1 and "blocked" means a nonzero count. D == 0 and
//    D == 1 therefore both forbid nothing observable, and both are
//    legal parameterisations -- see the corner cases.
//
//  SIMULTANEITY
//    activate and expiry on the same cycle -> ACTIVATE WINS (re-arms).
//    activate to bank b while bank b is already counting -> re-arm.
//      Legal and correct: a second ACTIVATE to an open bank is a row
//      conflict (Chapter 9.5), which the STATE layer refuses. If one
//      is nonetheless issued, the fresh obligation is the live one.
//    reset mid-obligation -> all obligations abandoned.
// ─────────────────────────────────────────────────────────────────────
module act_to_column_guard #(
  parameter int BANKS = 16,
  // Largest resolved magnitude this instance must represent, in
  // cycles. Sized as a COUNT, not an index: the count can equal
  // MAX_CYCLES, so it needs $clog2(MAX_CYCLES+1) bits. Getting this
  // wrong is Chapter 13.1 §14's fifth exercise, and with a
  // power-of-two MAX_CYCLES it silently forbids nothing.
  parameter int MAX_CYCLES = 32,
  parameter int BK_W  = (BANKS      <= 1) ? 1 : $clog2(BANKS),
  parameter int CNT_W = (MAX_CYCLES <= 1) ? 1 : $clog2(MAX_CYCLES + 1)
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── The resolved requirement, in cycles. An input rather than a
  //    parameter because a controller re-resolves it on a frequency
  //    change (Chapter 13.2 §7) and writes it to a register.
  input  logic [CNT_W-1:0]  trcd_cycles,

  // ── Triggering event: an ACTIVATE was accepted on this bank.
  input  logic              act_accepted,
  input  logic [BK_W-1:0]   act_bank,

  // ── Candidate: a column command someone wants to issue.
  input  logic              col_req,
  input  logic [BK_W-1:0]   col_bank,

  // ── Timing verdict for that candidate. This is the bit that feeds
  //    one slot of Chapter 13.1's command_legality_layers.
  output logic              col_timing_ok,
  // How much longer this bank must wait. Zero when legal.
  output logic [CNT_W-1:0]  col_wait_remaining,

  // ── Observability. Which banks are currently blocked by THIS
  //    obligation -- published because §11's first discriminator is
  //    "is more than one bank blocked by one activate?", and that
  //    question needs a per-bank view to answer.
  output logic [BANKS-1:0]  bank_blocked,
  output logic              any_blocked,

  // ── A column command was requested while still forbidden. Reported,
  //    never suppressed: this block advises, and hiding the attempt
  //    would conceal a scheduler bug.
  output logic              early_attempt
);

  // ── Elaboration guards.
  if (BANKS < 1) begin : g_banks
    initial $fatal(1, "act_to_column_guard: BANKS must be >= 1");
  end
  if (MAX_CYCLES < 0) begin : g_max
    initial $fatal(1, "act_to_column_guard: MAX_CYCLES must be >= 0");
  end

  // ── Per-bank countdown. THE scope decision, made structurally.
  logic [CNT_W-1:0] wait_cnt [BANKS];

  always_comb begin
    for (int unsigned b = 0; b < BANKS; b++) begin
      bank_blocked[b] = (wait_cnt[b] != '0);
    end
    any_blocked = (bank_blocked != '0);

    // Applicability by resource: only the CANDIDATE'S bank matters.
    // A different bank's countdown is irrelevant to this candidate,
    // which is what "bank-local" means operationally.
    col_wait_remaining = wait_cnt[col_bank];
    col_timing_ok      = (wait_cnt[col_bank] == '0);
    early_attempt      = col_req && !col_timing_ok;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int unsigned b = 0; b < BANKS; b++) begin
        wait_cnt[b] <= '0;
      end
    end else begin
      // Age every outstanding countdown first, then apply arming, so
      // that a fresh ACTIVATE is not decremented on its own arming
      // cycle. Reversing these two would under-block by one cycle --
      // the single most common defect in this block's shape.
      for (int unsigned b = 0; b < BANKS; b++) begin
        if (wait_cnt[b] != '0) begin
          wait_cnt[b] <= wait_cnt[b] - 1'b1;
        end
      end

      if (act_accepted) begin
        // Load D-1: the arming cycle is already unavailable to a
        // competing column command, so only the following D-1 cycles
        // need to be held. See the CONVENTION block.
        wait_cnt[act_bank] <= (trcd_cycles >= CNT_W'(1))
                                ? (trcd_cycles - CNT_W'(1))
                                : '0;
      end
    end
  end

endmodule

Interface contract. trcd_cycles is the resolved magnitude in cycles, not a nanosecond value — the conversion happened upstream, and putting a nanosecond number on this port is §11's speed-grade bug. col_timing_ok answers only the tRCD question; a real issue decision conjoins it with the state layer and with every other applicable obligation, which is Chapter 13.1's whole point and Chapter 14.4's subject.

Parameter contract. CNT_W is sized $clog2(MAX_CYCLES + 1) because it holds a count. With MAX_CYCLES of 32 that is 6 bits; sizing it $clog2(32) would give 5 bits, which cannot represent 32, so a resolved magnitude of exactly 32 would wrap to zero and forbid nothing at all — a violation on every access, arising from one character.

Why the cast is safe. trcd_cycles - CNT_W'(1) is guarded by the >= 1 test immediately above it, so it never underflows. CNT_W'(1) is a widening cast of a literal.

Resource scope. wait_cnt is indexed by bank. Read the declaration and the scope is unambiguous — which is the point. A reviewer can check the scope of this obligation in one line rather than by reading the logic.

Corner cases. trcd_cycles == 0: loads zero, forbids nothing, and is the degenerate “no separation required” configuration — legal, and not the same thing as a disabled constraint (Chapter 13.2 §12). trcd_cycles == 1: also loads zero, because the only forbidden cycle is the arming cycle, which no competing column command can occupy. BANKS == 1: BK_W is 1 by the guard and the block degenerates to a single countdown — correct for a one-bank device, and precisely the configuration in which a scope bug is invisible. Activate and expiry coinciding: arming wins, because the age loop's result is overwritten by the arm in the same always_ff.

Synthesis implications. BANKS counters of CNT_W bits, each with a decrement, plus a one-hot arm decode and a read mux on col_bank. For 16 banks and 6-bit counts that is 96 flops — cheap, and the reason nobody should be tempted to share one counter to save area.

Failure modes. Arming before ageing under-blocks by one cycle, uniformly, at every speed grade. A single shared wait_cnt instead of the array is Chapter 13.3 §4's Bug 1: safe, slow, silent. Indexing wait_cnt by act_bank instead of col_bank in the combinational block answers the wrong question entirely — it reports whether the last activated bank is ready, which is right whenever traffic is single-bank and wrong as soon as it is not.

8. The Boundary, in Cycles

An educational magnitude, chosen small enough to see the whole obligation.

act_to_column_guard — the forbidden interval and its boundary

10 cycles
Ten cycles with an educational tRCD magnitude of four. An activate to bank two is accepted at cycle one, arming the bank-two countdown. The countdown holds three, then two, then one across cycles two, three and four, so bank two is blocked across cycles one through four. A column command to bank two offered at cycle four is refused as an early attempt because the countdown has not reached zero. At cycle five the countdown is zero and the same column command to bank two is legal, which is the arming cycle plus four. Throughout the blocked interval a column command to bank seven is legal, because the obligation is bank-local and bank seven was never activated.bank 2 forbiddenbank 2 forbiddenlegal, and stays legallegal, and stays legalACT bank 2 — obligation armedACT bank 2 — obligationarmedone cycle early — refusedone cycle early — refusedcycle N+4 — first legalcycle N+4 — first legalCKact_acceptedact_bank0222222222wait_cnt[2]0032100000wait_cnt[7]0000000000col_req bank2col_timing_okearly_attemptbank7 legalt0t1t2t3t4t5t6t7t8t9

Three things in this trace are worth naming, because each is a §9 property.

The forbidden interval is cycles 1 through 4 — four cycles, including the arming cycle. That matches §5's contract exactly: D forbidden cycles, earliest legal at N + D = 1 + 4 = 5.

The cycle-4 candidate is the one-cycle-early case, and early_attempt reports it rather than the block silently absorbing it. A guard that quietly held the request would hide the fact that something upstream asked for an illegal command.

bank7 legal is high for the entire trace. Bank 7 was never activated, so it carries no tRCD obligation, and bank 2's obligation says nothing about it. That row is the visual form of bank-local, and it is the row that goes wrong in Chapter 13.3 §4's Bug 1.

9. Five Assertions Worth Writing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── P1. No column command is permitted while its own bank is still
//    counting. The safety property. Catches an inverted comparison and
//    an off-by-one in the wrong direction.
property p_no_column_while_blocked;
  @(posedge clk) disable iff (!rst_n)
    (wait_cnt[col_bank] != '0) |-> !col_timing_ok;
endproperty
a_no_column_while_blocked: assert property (p_no_column_while_blocked);

// ── P2. The obligation lasts EXACTLY as long as §5's contract says.
//    Checked by construction from the arming event rather than by
//    re-deriving the RTL's own expression: D cycles after an arm with
//    magnitude D, the bank must be clear; D-1 cycles after, it must
//    not be. Catches BOTH off-by-one directions, which a one-sided
//    property cannot do.
//    genvar-wrapped because a procedural for-loop is illegal inside a
//    property, and every $past carries an explicit count.
generate
  for (genvar gb = 0; gb < BANKS; gb++) begin : g_boundary
    property p_clear_at_exactly_N_plus_D;
      @(posedge clk) disable iff (!rst_n)
        ( $past(act_accepted, 1) && ($past(act_bank, 1) == BK_W'(gb))
                                 && ($past(trcd_cycles, 1) >= CNT_W'(2)) )
          |-> (wait_cnt[gb] == ($past(trcd_cycles, 1) - CNT_W'(1)));
    endproperty
    a_clear_at_exactly_N_plus_D: assert property (p_clear_at_exactly_N_plus_D);
  end
endgenerate

// ── P3. SCOPE. An ACTIVATE to one bank must never disturb another
//    bank's countdown. THE property for Chapter 13.3 §4's Bug 1 --
//    the safe-and-slow failure that no functional test detects and
//    that ships to production regularly.
generate
  for (genvar gc = 0; gc < BANKS; gc++) begin : g_scope
    property p_activate_is_bank_local;
      @(posedge clk) disable iff (!rst_n)
        ( $past(act_accepted, 1) && ($past(act_bank, 1) != BK_W'(gc))
                                 && ($past(wait_cnt[gc], 1) == '0) )
          |-> (wait_cnt[gc] == '0);
    endproperty
    a_activate_is_bank_local: assert property (p_activate_is_bank_local);
  end
endgenerate

// ── P4. A countdown only ever decreases by one, or reloads on an arm.
//    Catches a decrement that skips values -- which would make the
//    obligation short without changing its start or its end condition,
//    and is therefore invisible to P1.
generate
  for (genvar gd = 0; gd < BANKS; gd++) begin : g_monotone
    property p_countdown_steps_by_one;
      @(posedge clk) disable iff (!rst_n)
        ( ($past(wait_cnt[gd], 1) != '0) && !$past(act_accepted, 1) )
          |-> (wait_cnt[gd] == ($past(wait_cnt[gd], 1) - CNT_W'(1)));
    endproperty
    a_countdown_steps_by_one: assert property (p_countdown_steps_by_one);
  end
endgenerate

// ── P5. Reset abandons every obligation. Correct because a reset
//    controller has no history to honour, and asserted so that a
//    partial reset -- a real bug in multi-bank arrays -- is caught.
property p_reset_clears_all;
  @(posedge clk)
    !rst_n |=> (bank_blocked == '0);
endproperty
a_reset_clears_all: assert property (p_reset_clears_all);

What these prove. That a blocked bank refuses, that the obligation's length matches the stated convention in both directions, that the obligation is genuinely bank-local, that the countdown cannot skip, and that reset is complete.

What these do not prove. Three gaps, and the first is the one that matters most in this module.

Nothing here proves trcd_cycles is correct. It is an input. A guard fed a magnitude one cycle short satisfies all five properties and violates the device's contract on every single access. The resolved value must be checked against the speed-bin table by a human, once, carefully — and then re-checked on every frequency change, because §4 showed the cycle count moves with tCK. No property of this block can catch a wrong magnitude, which is why §10's checker recomputes it from the nanosecond value independently.

Nothing here proves the right obligations were armed. If act_accepted is driven by something that is not actually an accepted ACTIVATE — a requested one, say, or one that the state layer refused — the obligation is armed at the wrong time and every property still passes.

Vacuity. P2 through P4 are implications with narrow antecedents; P2 additionally requires trcd_cycles >= 2. A run that never activates bank gb, or that runs with a magnitude of 1, passes them all trivially. Cover act_accepted per bank, cover early_attempt, and cover a magnitude above 2.

10. DV — Reconstructing tRCD Independently

Chapter 13.4 §8 established the discipline: a checker that shares the design's representation shares its bugs. Applied to tRCD, that means three specific choices.

Use timestamps, because the design uses countdowns. Record the cycle of each bank's last accepted ACTIVATE; on each column command, compute now - act_cycle[bank] and compare against the requirement. The design counts down; the checker subtracts. Neither can reproduce the other's arithmetic slip.

Recompute the magnitude from nanoseconds, not from the register. This is the tRCD-specific point and it is the most valuable thing in this section. The design is told a cycle count. The checker should be told 13.75 ns and the clock period, and should do its own ceil — because then a resolver bug, a wrong speed-bin entry, or a stale value after a frequency change produces a disagreement rather than an agreed wrong answer. §9 just said no block-level property can catch a wrong magnitude; this is how you catch it.

Observe accepted commands, not internal arm signals. If the checker watches the design's act_accepted, it inherits the design's opinion about what was accepted.

A report worth emitting, in the shape Chapter 13.4 §8 argued for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  TIMING VIOLATION
    command        : READ, bank 2
    issued at      : cycle 118
    violated       : tRCD
    resource       : bank 2                 (bank-local)
    source event   : ACTIVATE bank 2, cycle 100
    requirement    : 13.75 ns / 0.625 ns = 22 cycles  (ceil)
    legal from     : cycle 122
    short by       : 4 cycles

The requirement line is doing real work. It shows the nanosecond value, the clock period and the conversion, so a reader can check the arithmetic without leaving the report. A checker that printed only “requires 22 cycles” would be agreeing with the design about the one number most likely to be wrong.

Reconcile against the design every cycle. Compare the checker's verdict with col_timing_ok. They compute the same thing from different representations, so any disagreement is a bug in one of them — and the direction tells you which is more urgent: the design permitting what the checker forbids is the dangerous one.

11. Debugging

Symptom. Column commands are being refused, or a protocol checker reports tRCD violations, and the bank state looks entirely correct.

Candidate mechanismEvidenceDiscriminator
Scope error — one countdown shared across banksSeveral banks blocked by one ACTIVATE; throughput flat in bank countCompare one-bank against all-banks throughput. If it does not improve, the obligation is coupling banks. §9's P3.
Wrong index in the combinational readVerdict tracks the last activated bank, not the candidate'sActivate bank 3, then offer a column command to bank 9. If bank 9 is refused, col_bank is not being used.
ns value placed on a cycles portRequirement is wrong by roughly the factor 1/tCKCompare trcd_cycles against ceil(ns / tCK). A 13.75 on a cycles port instead of 22 is not subtle once you look.
Speed-grade off-by-oneCorrect at one data rate, one cycle short at another§4's conversion. Truncation instead of ceil agrees with the ceiling at every rate where the division is exact, so it survives single-rate testing.
Arming before ageingEvery obligation exactly one cycle short, uniformlyCount forbidden cycles on a waveform. §5 fixes the answer at exactly D.
Armed on request, not on acceptanceViolations only when the state layer refused an ACTIVATECheck what drives act_accepted. A refused activate must arm nothing.
Stale magnitude after a frequency changeViolations begin after a frequency transition and persistWhether the resolver re-ran. Chapter 13.2 §7.

The discriminator that separates the top two fastest is a two-bank experiment: activate bank 3, immediately offer a column command to bank 9. Correct behaviour permits it. If bank 9 is refused, you have either a shared countdown or a wrong index, and which one is settled by whether bank 9's own counter is nonzero.

The discriminator for the bottom four is whether the failure is frequency-dependent. A bug that appears at one data rate and not another is in the ns-to-cycle conversion. A bug that appears at every data rate identically is in the counting convention. That one question splits the table in half, and it costs one extra simulation at a different clock period.

Responsible layer. A tRCD refusal with a plausible elapsed time is not a bug — it is the device contract being honoured, and the right question becomes whether the scheduler had something else to do, which is Module 17's territory.

12. Common Misconceptions

“If the bank is open, a READ is legal.” Tempting because Module 9's state model says OPEN and openness is what reads need. Why it is wrong: state and timing are independent questions over different information (Chapter 13.1 §2). The bank is open and the tRCD obligation is outstanding. Consequence: a controller that issues on state alone violates the contract in a way functional simulation against an untimed model will not catch. Replacement model: OPEN plus tRCD elapsed. Debugging clue: failures appear only against a timing-checked model or on silicon.

“tRCD is how long it takes to read a row.” Tempting because it is the delay you wait after activating, and reading is what you wanted. Why it is wrong: tRCD ends where the column command becomes legal. The data arrives CL cycles after that column command, and the burst continues for further transfer positions. Consequence: latency budgets that omit CL and the burst — understating ACTIVATE-to-last-beat substantially. Replacement model: tRCD, then CL, then the burst; three terms, three chapters. Debugging clue: measured latency consistently exceeds the model by roughly CL plus the burst length.

“tRCD is a number of cycles.” Tempting because controllers configure it in cycles and tuning tools display it in cycles. Why it is wrong: it is published in nanoseconds — §4 verified the column heading. The cycle count is derived, and it changes with the clock. Consequence: a cycle count carried across a speed-grade change under-waits at the faster rate. Replacement model: the nanosecond value is the source of truth; the cycle count is a per-frequency derivation. Debugging clue: violations only in the fastest speed bin.

“A DDR4 timing table generalises to DDR5.” Tempting because the parameter names largely survived, so the tables look comparable. Why it is wrong: §6 is a live example — there is credible reporting that DDR5 separates read and write activate-to-column delays, and I could not confirm it either way. Names surviving is not semantics surviving. Consequence: a controller ported on the assumption of identical semantics, with a parameter that means something slightly different. Replacement model: verify per generation, per parameter. Debugging clue: a port that works except for one command type.

“Waiting longer than tRCD is also a violation.” Tempting because symmetry, and because CL genuinely is two-sided. Why it is wrong: tRCD is Class A, a lower bound with no upper bound. Consequence: a controller that forces the column command to the earliest legal cycle, forfeiting all scheduling freedom before the scheduler runs. Replacement model: minimum separation permits arbitrary delay; only fixed latencies are two-sided. Debugging clue: a smarter scheduler yields no improvement.

“More conservative tRCD can never cause a functional problem.” Tempting because waiting longer is always legal for a Class A constraint, so conservatism looks free. Why it is wrong: it is functionally safe and it is not free — and worse, conservatism applied at the wrong scope stops being safe in the other direction. A designer who “adds margin” by sharing one countdown across banks has made the design safe for tRCD and destroyed bank independence. Consequence: silent throughput loss attributed to the workload. Replacement model: margin is cheap in magnitude and expensive in scope. Debugging clue: throughput does not scale with banks.

13. Interview Reasoning

“What does tRCD constrain?” Give the five parts rather than a phrase. An accepted ACTIVATE on a bank is the triggering event; a column command to that bank is the constrained event; the resource is that bank, so the obligation is bank-local; the magnitude is published in nanoseconds and must be resolved to cycles by a ceiling; and the sense is a minimum separation, so later is always fine. Then the consequence: the earliest legal column command is the activate cycle plus the resolved count.

“Why is tRCD not the same thing as CAS latency?” They are different classes of constraint on different edges. tRCD is a minimum separation that governs whether the column command may be issued; CL is a fixed, programmed latency that governs when data appears after an accepted column command. One is a permission with a lower bound; the other is a two-sided commitment. The verified evidence is in the datasheet itself — Micron's DDR4 speed-bin table quotes tRCD in nanoseconds and CL as a bare cycle count, because one is a physical requirement and the other is a configured pipeline depth.

“Why can a row be open while a READ is still timing-illegal?” Because openness is a statement about the bank's present state and tRCD is a statement about elapsed time since the state changed, and no state model records when it was entered. The architectural version: ACTIVATE starts a process rather than performing one, so the bank enters OPEN at command acceptance while the data it exposes is not yet resolved. The controller consequence is that the issue term needs a third conjunct that the state model cannot supply.

“tRCD is 13.75 ns. How many cycles?” The right first move is to ask for the data rate, because the question is unanswerable without it — and saying so is the answer. Given DDR4-3200: that is 3200 MT/s, so CK is 1600 MHz and tCK is 0.625 ns, giving exactly 22 cycles. Given DDR4-2400: tCK is 0.8333 ns, 13.75 / 0.8333 is 16.5, and the ceiling is 17 — because a minimum rounds up unconditionally. Worth volunteering that the faster part needs more cycles for the same physical delay.

“Your controller stalls bank 9 whenever bank 3 is activated. What is wrong?” A scope error: a bank-local obligation held in shared state. The consequence is that banks stop being independent, so a workload spread across banks runs as though it had one — and the reason this is a bug rather than a trade-off is that it is invisible: every timing requirement is met and no assertion fires. The test is whether aggregate throughput improves with bank parallelism.

“How would you verify a tRCD implementation independently?” Invert the representation — timestamps in the checker against countdowns in the design — and, specifically for tRCD, recompute the magnitude from the nanosecond value and the clock period inside the checker rather than reading the design's configured cycle count. That is the only way to catch a wrong resolved magnitude, because no property of the guard itself can see that its input is wrong. Then reconcile every cycle and treat any disagreement as a bug in one of the two.

14. Engineering Exercises

1. Resolve and bound the interval. A part specifies tRCD as 13.75 ns and runs at DDR4-2666, where tCK is 0.75 ns. Compute the resolved cycle count, the first forbidden cycle, the number of forbidden cycles and the first legal cycle for an ACTIVATE accepted at cycle 40.

Worked: 13.75 / 0.75 = 18.33, so the ceiling is 19 cycles. Forbidden cycles are 40 through 58 — nineteen of them, including the arming cycle. First legal column command is cycle 59. Note the quantisation waste: 19 cycles is 14.25 ns for a 13.75 ns requirement, so 0.5 ns is unrecoverable (Chapter 13.2 §3).

2. Find the speed-dependent bug. A colleague implements the conversion as cycles = ns / tCK with truncating integer arithmetic on picosecond values. At which of DDR4-3200, DDR4-2666 and DDR4-2400 does this produce a violation, and why might it pass a review?

Worked: at DDR4-3200, 13750 / 625 = 22 exactly, so truncation and ceiling agree and the design is correct. At DDR4-2666, 13750 / 750 = 18.33 → 18 where 19 is required — a violation. At DDR4-2400, 13750 / 833 = 16.5 → 16 where 17 is required — a violation. It passes review because the reviewer checks the one speed grade the team is currently bringing up, and if that is DDR4-3200 the arithmetic is exactly right. A converter tested at a single frequency is effectively untested.

3. Classify the obligation. Without looking at §1, write tRCD's five parts. Then check. Which part did you leave implicit, and what bug does leaving it implicit permit?

4. Repair a scope error. You inherit a guard with a single wait_cnt register shared by 16 banks. Describe the measurement that confirms the problem, the change, the register cost, and one new bug the change could introduce.

Worked: measure one-bank against sixteen-bank throughput; if aggregate throughput does not rise, banks are coupled. The change is to index wait_cnt by bank. The cost is 16 counters instead of one — for 6-bit counts, 96 flops instead of 6. The new bug: if that shared counter was also carrying an obligation that is genuinely shared — anything on the data path, for instance — replicating it per bank converts safe-and-slow into fast-and-wrong (Chapter 13.3 §4). Auditing what the counter was carrying is the mandatory first step, and skipping it turns a performance fix into a data-corruption bug.

5. Test the boundary in both directions. Write directed stimulus proving both that a column command at N + D - 1 is refused and that one at N + D is permitted. Why is a test that only checks the second of these insufficient, and which of §9's properties covers the gap?

Worked: a test that only checks N + D passes on a design whose obligation is too short — a guard that permitted N + D - 3 would also permit N + D. Only the refusal test bounds the obligation from below. P2 covers both directions by asserting the exact countdown value one cycle after arming, which is why it is written as an equality rather than an inequality.

6. Size the counter wrongly on purpose. Set MAX_CYCLES to 32 and change CNT_W to $clog2(MAX_CYCLES). Drive trcd_cycles at 32. What is the resolved behaviour, why is it the worst possible failure direction, and which property catches it?

Worked: $clog2(32) is 5 bits, which cannot hold 32, so the value truncates to 0 and the guard forbids nothing — a contract violation on every access rather than a delay. It is the worst direction because it fails open. P2 catches it immediately, since the asserted countdown value would be 31 and the actual is 0.

7. Decide what belongs where. The checker in §10 recomputes the requirement from nanoseconds. Argue the opposite position — that the checker should read the design's configured cycle count — then rebut it. Under what circumstance would the opposing position actually be correct?

15. Summary

tRCD is the first populated edge in Module 13's graph, and it is worth holding as five parts rather than a phrase. An accepted ACTIVATE on a bank is the triggering event. A column command to that bank is the constrained event. The bank is the resource, so the obligation is bank-local — held per bank in hardware, structurally, not by comment. The magnitude is published in nanoseconds and resolved to cycles by a ceiling. And the sense is minimum separation: later is always legal.

The units are verified rather than assumed. Micron's DDR4 speed-bin tables head their columns Data Rate (MT/s) | CL | tRCD (ns) | tRP (ns) | tRC (ns) — tRCD in absolute time because it is a physical requirement, CL as a bare cycle count because it is a configured pipeline depth. That single heading distinguishes the two constraint classes better than any explanation.

The boundary is exact: with an ACTIVATE accepted at cycle N and a resolved magnitude D, cycles N through N + D - 1 are forbidden — D of them, counting the arming cycle — and N + D is the first legal column command.

act_to_column_guard enforces it with a countdown indexed by bank, which is the scope decision made visible in a declaration. Its most consequential limitation is that it cannot check its own magnitude: a guard fed a value one cycle short passes every property and violates the contract on every access, which is why §10's checker resolves the nanosecond value independently.

16. What Comes Next

tRCD governs the beginning of a row's useful life. Chapter 14.2 governs the end of it.

PRECHARGE presents the same five-part structure with one difference that makes it the module's best teaching case for a distinction Chapter 13.1 introduced and nothing has yet stressed: a controller can mark a bank closed the instant it issues the PRECHARGE, and be completely correct about the state — while being completely wrong if it reads CLOSED as ACTIVATE legal now.” State and timing diverge there more visibly than anywhere else in DDR.

Continue learning

Standards & specifications

Governing standard
JEDEC JESD79 (DDR SDRAM)(opens JEDEC Solid State Technology Association in a new tab)

Defines the DDR SDRAM device itself — signals, command encoding, mode registers, timing parameters and the initialisation sequence — one document per generation. Memory-controller microarchitecture, address-mapping policy, PHY training algorithms and board-level design are not specified by it.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the DDR curriculum.