Skip to content
VLSI Mentor

DDR · Module 13

Command Scheduling

An eight-step procedure for deciding when a command becomes temporally legal, worked through a full trace by hand — and a timing checker built in the opposite representation to the design, so the two cannot share a bug.

Three chapters have built a timing engine. Chapter 13.1 gave obligations their structure, Chapter 13.2 gave them magnitudes and a counting convention, and Chapter 13.3 gave them classes, scopes and the rule that combines them.

What is still missing is the thing you actually do with all of it:

Given a command trace, how do you prove it is legal — by hand at a whiteboard, or in a checker that does not simply repeat the controller's own arithmetic back to you?

This chapter is that procedure. It is deliberately mechanical, because the value of a procedure is that it works when you are tired and the waveform is confusing.

1. What “Scheduling” Means Here, and What It Does Not

The chapter is called Command Scheduling and it is important to be precise about which half of that subject is here, because the other half is a whole module.

A DDR controller facing a set of pending requests must answer two genuinely different questions:

“Which commands are legal right now?” — a question about the device's contract. It has one correct answer, computable from history, and it is the same answer for every controller attached to that device.

“Which of the legal commands should I issue?” — a question about policy. It has no single correct answer. It depends on fairness, on latency targets, on whether you favour row hits over age, on quality-of-service commitments.

This chapter answers the first question completely and does not touch the second. Module 17 is the second.

The division is not arbitrary tidiness; it is how the problem is actually structured. A scheduler that gets its policy wrong is a scheduler with poor performance. A scheduler that gets its legality wrong is broken, regardless of how clever its policy is. Legality is a precondition that policy operates inside, and mixing them produces a design where nobody can say whether a given stall was required or chosen.

2. The Procedure

Eight steps. Run them for one candidate command at one cycle.

Step 1 — Identify the candidate. Which command, with which operands. Without the operands you cannot do step 2.

Step 2 — Identify the resources it needs. Its bank. The data path, if it moves data. The command path, always. This step determines everything that follows, and Chapter 13.3 §4 is the reason: applicability is decided by resource.

Step 3 — Check semantic and state legality first. Chapter 13.1's first two layers. Do this before any timing arithmetic, because a state-illegal command's timing analysis is wasted work — and worse, a timing verdict on a command that needs a state transition sends you down the wrong path entirely.

Step 4 — List the applicable obligations. For each resource from step 2, every obligation attached to it that constrains this command's class. This is the step people skip, and skipping it produces the failure mode where an analysis is internally perfect and simply missed an edge.

Step 5 — Compute each obligation's deadline. deadline_i = E_i + D_i, where E_i is the triggering event's cycle and D_i the magnitude. Write them all down. Do not reduce yet.

Step 6 — Take the maximum. Chapter 13.3 §3's rule. The maximum is the earliest legal cycle. Note which obligation held it — that is the controlling one, and it is the answer to every later question.

Step 7 — Compare with the candidate's cycle. Legal if the candidate's cycle is at least the maximum. If not, the shortfall is the maximum minus the candidate's cycle, and the controlling obligation from step 6 is what to go and look at.

Step 8 — If issued, arm the new obligations. The issued command is itself a triggering event. Update the state after the decision, never before, and record the new deadlines for the next candidate's step 5.

The eight-step analysis as an exchange between five participants. The candidate command first declares its operands and the resources it needs. The state model is consulted for semantic and state legality before any timing work is done, and returns a verdict. The candidate then asks the obligation set which obligations are attached to the resources it needs, and the obligation set contributes one deadline per applicable obligation to the reduction. The reduction takes the maximum of those deadlines and passes both the maximum and the identity of the obligation that held it to the decision. The decision compares the maximum against the candidate's cycle and returns a verdict, either legal now or a shortfall together with the controlling obligation. Finally, if the command is issued, the decision informs the obligation set so that new obligations are armed for subsequent candidates.CandidateState modelObligationsReductionDecision1-3 · operands,resourcessemantic + stateverdict4 · what isattached?5 · one deadlineeach6 · max +controlling7 · legal, orshortfall8 · issue arms newones

3. A Worked Trace

Now run the procedure on a real sequence. This is the centrepiece of the chapter, and it is worth doing with a pencil rather than reading.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CONSTRAINT A   ACTIVATE  ──▶  column command      >= 4 cycles   BANK-LOCAL
  CONSTRAINT B   column    ──▶  column command      >= 2 cycles   SHARED data path
  CONSTRAINT C   ACTIVATE  ──▶  PRECHARGE           >= 7 cycles   BANK-LOCAL

  STATE RULE     a column command requires its bank OPEN on the right row
                 a PRECHARGE requires its bank OPEN

The trace. Two banks, a handful of candidates:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cycle 0    ACTIVATE bank 0          (issued)
  cycle 2    ACTIVATE bank 1          (issued)
  cycle 3    candidate: column, bank 0
  cycle 4    candidate: column, bank 0
  cycle 5    candidate: column, bank 1
  cycle 6    candidate: column, bank 1
  cycle 6    candidate: PRECHARGE bank 0
  cycle 8    candidate: PRECHARGE bank 0

Establishing the obligations

Step 8 for the two activates, which are given as issued:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ACTIVATE bank 0 at cycle 0   arms   A(bank 0) : column cmd to bank 0 from cycle 4
                               arms   C(bank 0) : PRECHARGE bank 0    from cycle 7
  ACTIVATE bank 1 at cycle 2   arms   A(bank 1) : column cmd to bank 1 from cycle 6
                               arms   C(bank 1) : PRECHARGE bank 1    from cycle 9

Candidate 1 — column command, bank 0, at cycle 3

Steps 1–2: a column command needing bank 0 and the shared data path. Step 3: semantic legal; bank 0 is open on the right row, so state legal. Step 4: A applies, attached to bank 0. B applies, attached to the data path — but no column command has been issued yet, so B has no triggering event and contributes no deadline. C does not apply: it constrains precharges. Step 5: A(bank 0) gives deadline 4. That is the only one. Step 6: max is 4, controlling obligation A. Step 7: 3 is less than 4 — ILLEGAL, shortfall 1 cycle, controlling A from the activate at cycle 0.

Candidate 2 — column command, bank 0, at cycle 4

Steps 1–4: as before. B still has no triggering event. Step 5: A(bank 0) gives 4. Step 6: max is 4. Step 7: 4 is at least 4 — LEGAL. Issue it. Step 8: the issue arms B on the data path: the next column command is permitted from cycle 4 + 2 = 6.

Candidate 3 — column command, bank 1, at cycle 5

Steps 1–2: bank 1 and the data path. Step 3: bank 1 is open — state legal. Step 4: two obligations now. A(bank 1), because the candidate needs bank 1. And B, because it needs the data path, which the cycle-4 issue armed. Step 5: A(bank 1) gives 2 + 4 = 6. B gives 4 + 2 = 6. Step 6: max of 6 and 6 is 6. A tie — both obligations are controlling. Step 7: 5 is less than 6 — ILLEGAL, shortfall 1.

This candidate is the most instructive in the trace. Two obligations of different classes, at different scopes, triggered by different events, arrive at the same deadline by coincidence. An analysis that found only one of them would get the right answer here — and would be wrong the moment the numbers differed. Getting the right answer is not evidence that the method was right, which is the practical reason step 4 must be mechanical.

Candidate 4 — column command, bank 1, at cycle 6

Steps 4–5: A(bank 1) gives 6, B gives 6. Step 6: max 6. Step 7: 6 is at least 6 — LEGAL. Issue. Step 8: B is re-armed: the next column command is permitted from cycle 8.

Candidate 5 — PRECHARGE bank 0, at cycle 6

Steps 1–2: a precharge needing bank 0. Under this abstract constraint set it does not use the data path. Step 3: bank 0 is open — state legal. Step 4: C(bank 0) applies. A does not — it constrains column commands. B does not — this candidate does not use the data path. Step 5: C(bank 0) gives 0 + 7 = 7. Step 6: max 7, controlling C. Step 7: 6 is less than 7 — ILLEGAL, shortfall 1.

Note that B's deadline of 8 is larger than C's 7, and is correctly excluded. A careless analysis that took the maximum over all outstanding obligations rather than the applicable ones would report 8, and would refuse a legal precharge at cycle 7. That error is safe and slow — Chapter 13.3 §4's Bug 1 arriving through sloppy analysis instead of sloppy RTL.

Candidate 6 — PRECHARGE bank 0, at cycle 8

Step 5: C(bank 0) gives 7. Step 6: max 7. Step 7: 8 is at least 7 — LEGAL. It would also have been legal at 7.

The whole trace in one table

CycleCandidateSemStateABCmaxControllingVerdict
3col, bank 04n/a4A✗ short by 1
4col, bank 04n/a4A✓ issue
5col, bank 166n/a6A and B✗ short by 1
6col, bank 166n/a6A and B✓ issue
6PRE, bank 0n/an/a77C✗ short by 1
8PRE, bank 0n/an/a77C✓ issue

The worked trace — obligations and verdicts

10 cycles
Ten cycles of the worked trace. An activate to bank zero issues at cycle zero and an activate to bank one at cycle two. The bank-zero column deadline is cycle four and the bank-one column deadline is cycle six. A candidate column command to bank zero is refused at cycle three because the bank-zero deadline has not arrived, then issues at cycle four. That issue arms the shared data-path obligation, whose deadline becomes cycle six. A candidate column command to bank one is refused at cycle five because both the bank-one obligation and the shared obligation give a deadline of six, then issues at cycle six. A precharge candidate for bank zero is refused at cycle six because the bank-zero precharge deadline is cycle seven, and issues at cycle eight.bank 0 openingbank 0 openingshared path in useshared path in useprecharge windowprecharge windowshort by 1 — A controlsshort by 1 — A controlsA and B tie at 6A and B tie at 6precharge blocked by C, not Bprecharge blocked by C, notBprecharge legalprecharge legalCKissuedACT0ACT0ACT1ACT1COL0COL0COL1COL1PRE0PRE0A bank04444444444A bank10066666666B shared0000666888C bank07777777777candidate000COL0COL0COL1PRE0PRE0PRE0PRE0verdict000EARLYOKEARLYEARLYEARLYOKOKt0t1t2t3t4t5t6t7t8t9

4. Change One Event and Recompute

The trace above is a single data point. The way to test whether you have the method rather than the answer is to perturb it.

Move the activate to bank 1 from cycle 2 to cycle 0. Intuition says the bank-1 column command should become legal earlier — the row was opened two cycles sooner.

Recompute candidate 3 (column, bank 1, at cycle 5):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  A(bank 1)  =  0 + 4  =  4      (was 6 — two cycles earlier, as expected)
  B          =  4 + 2  =  6      (unchanged; still armed by the cycle-4 issue)

  max(4, 6)  =  6                (unchanged)

The candidate is still illegal at cycle 5. Opening the row earlier bought nothing at all, because a different obligation became controlling. The bank-local constraint was never the limiting one once the shared path was in use; it only looked that way because the two happened to tie.

Now perturb it the other way. Change constraint B's magnitude from 2 to 4. Candidate 3 becomes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  A(bank 1)  =  6        B  =  4 + 4  =  8        max  =  8

The candidate is now illegal until cycle 8 — two cycles worse — and B is unambiguously controlling. The bank-local constraint has become irrelevant. A shared constraint that grows can dominate every bank-local constraint at once, which is why shared-resource obligations deserve disproportionate attention in performance work: they are the ones that cannot be escaped by adding parallelism.

5. May Issue Is Not Should Issue

The procedure produces an earliest legal cycle. It is worth being explicit that this is not an instruction to issue at that cycle.

At cycle 6 in §3's trace, two candidates are pending: a column command to bank 1, which is legal, and a precharge of bank 0, which is not (it becomes legal at 7). The procedure says one is legal and the other is not. It does not say to issue the column command — that is a policy decision, and Module 17 owns it.

A scheduler is free to issue later than the earliest legal cycle. Chapter 13.3 §2's class distinction is what makes this true: a minimum separation has no upper bound, so waiting is always permitted. A controller may hold a legal command because something else is more urgent, because a bus turnaround would be wasteful, or because a fairness rule says another requester's turn has come.

With one exception, and it matters. Class B — fixed latency — is two-sided. Once a column command is issued, the data appears at the configured offset whether or not the controller is ready. So the freedom to defer applies to issuing commands, not to servicing the consequences of commands already issued. Chapter 10.2 and Chapter 11.2 are where that obligation is developed.

6. RTL — An Independent Timing Checker

This block is verification-only. It is not part of any controller, it is not synthesised, and it exists to watch a command stream and say whether it was legal. Saying so in the header is not a formality — a checker that gets instantiated in a design is a checker whose bugs become product bugs.

Collision check. Chapter 13.3's deadline_scoreboard tracks obligations and computes a maximum, and it is a design block using timestamps. Nothing in the corpus observes a command stream and produces a timing verdict with a diagnostic.

The engineering problem, and the design principle behind it. A checker that mirrors the design's arithmetic is nearly worthless: if the design has an off-by-one and the checker computes the same off-by-one, both agree and both are wrong. This is common-mode failure, and it is the dominant risk in timing verification precisely because the arithmetic is simple enough that two engineers naturally write it the same way.

So this checker is built to differ from deadline_scoreboard in three deliberate ways:

It uses countdowns where the design uses timestamps. Chapter 13.3 §6 established these are equivalent in what they compute and different in how they fail — and the crucial consequence is that a countdown checker has no timestamps to wrap, so it structurally cannot share the design's wraparound bug.

It observes commands rather than obligations. It derives obligations from the command stream itself, so it does not depend on the design's view of what is armed. A design that fails to arm an obligation is invisible to its own scoreboard and visible to this checker.

It reports rather than gates. It never blocks anything, so it cannot mask a violation by preventing it.

Classification: verification-only observer. Counts cycles, compares numbers. It models no physical process whatever — the magnitudes are parameters that came from a datasheet, and the block would behave identically if they described something else entirely.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
//  issue_timing_checker
//
//  CLASSIFICATION
//    VERIFICATION-ONLY observer. NOT for synthesis, NOT part of any
//    controller. Instantiate in a testbench or bind it to a DUT.
//
//  WHAT IT MODELS
//    §2's procedure, applied to an observed command stream. It derives
//    obligations from the commands it sees, holds them as COUNTDOWNS,
//    and reports a violation with the diagnostic §8 argues for: which
//    rule, which resource, which source event, and by how much.
//
//  WHAT IT DOES NOT MODEL
//    Any physical process, any analog behaviour, any device internal.
//    Magnitudes are parameters. This block cannot tell what they mean.
//
//  DELIBERATE REPRESENTATION INVERSION
//    Chapter 13.3's deadline_scoreboard is a DESIGN block using
//    absolute TIMESTAMPS. This checker uses COUNTDOWNS. The inversion
//    is the entire point: a checker sharing the design's
//    representation shares the design's representation bugs. Having no
//    timestamps, this block cannot reproduce a wraparound error, so
//    the two disagree when the design has one.
//
//  CONVENTION (Chapter 13.2 §6, obeyed exactly)
//    A command issued at cycle N arming magnitude D forbids the
//    constrained command on cycles N .. N+D-1 and permits it from
//    N+D. Since the command bus carries one command per cycle, cycle N
//    itself cannot host a competing candidate, so the countdown is
//    loaded with D-1 and "blocked" means a nonzero count.
//
//  ABSTRACT RULE SET -- matches §3's worked trace exactly, so the
//  chapter's hand analysis and this block can be compared directly.
//    RULE A : ACTIVATE -> column cmd, BANK-LOCAL
//    RULE B : column   -> column cmd, SHARED data path
//    RULE C : ACTIVATE -> PRECHARGE,  BANK-LOCAL
//  A real checker carries Module 14's parameters. The SHAPE is what
//  transfers, and the shape is: per-resource countdowns, derived from
//  observed commands, reduced by maximum remaining.
// ─────────────────────────────────────────────────────────────────────
module issue_timing_checker #(
  parameter int BANKS = 4,
  // Abstract magnitudes, in cycles. Defaults are §3's trace values.
  parameter int MAG_A = 4,
  parameter int MAG_B = 2,
  parameter int MAG_C = 7,
  // Cycle counter width -- for REPORTING only. Nothing compares
  // against it, so it cannot produce a wraparound bug; it can only
  // make a printed cycle number ambiguous after a wrap.
  parameter int TS_W  = 16,
  parameter int BK_W  = (BANKS <= 1) ? 1 : $clog2(BANKS),
  // Countdown width: must hold the largest magnitude minus one.
  // Sized on the MAXIMUM of the three, +1 so that a count EQUAL to a
  // magnitude is representable. The distinction between sizing for a
  // count and sizing for an index is Chapter 13.1 §14's fifth exercise.
  parameter int MAXMAG = (MAG_A > MAG_B)
                           ? ((MAG_A > MAG_C) ? MAG_A : MAG_C)
                           : ((MAG_B > MAG_C) ? MAG_B : MAG_C),
  parameter int CNT_W  = (MAXMAG <= 1) ? 1 : $clog2(MAXMAG + 1)
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── The observed command stream. One command per cycle, at most.
  input  logic              cmd_valid,
  // 0 = ACTIVATE, 1 = column command, 2 = PRECHARGE.
  input  logic [1:0]        cmd_kind,
  input  logic [BK_W-1:0]   cmd_bank,

  // ── The verdict. A one-cycle pulse coincident with the offending
  //    command, so a waveform lines the report up with its cause.
  output logic              violation,
  // Which rule was violated: 0 = A, 1 = B, 2 = C. When more than one
  // is violated, the CONTROLLING one is reported -- the one with the
  // most remaining, which is §2 step 6 applied to a diagnostic.
  output logic [1:0]        violated_rule,
  output logic [BK_W-1:0]   violated_resource,
  // The cycle of the triggering event, and the cycle from which the
  // command would have been legal. §8's argument for this output.
  output logic [TS_W-1:0]   source_cycle,
  output logic [TS_W-1:0]   required_cycle,
  // How many cycles early. Zero is impossible while violation is high.
  output logic [CNT_W-1:0]  shortfall,

  // ── Observability for coverage: high when any obligation is live.
  output logic              any_outstanding
);

  localparam logic [1:0] KIND_ACT = 2'd0;
  localparam logic [1:0] KIND_COL = 2'd1;
  localparam logic [1:0] KIND_PRE = 2'd2;

  // ── Elaboration guards.
  if (BANKS < 1) begin : g_banks
    initial $fatal(1, "issue_timing_checker: BANKS must be >= 1");
  end
  if ((MAG_A < 0) || (MAG_B < 0) || (MAG_C < 0)) begin : g_mags
    initial $fatal(1, "issue_timing_checker: magnitudes must be >= 0");
  end
  if (TS_W < 1) begin : g_ts
    initial $fatal(1, "issue_timing_checker: TS_W must be >= 1");
  end

  // ── Cycle counter. REPORTING ONLY -- see the header.
  logic [TS_W-1:0] now;

  // ── Countdowns. Nonzero means the obligation still forbids its
  //    constrained command. Per bank for the bank-local rules, single
  //    for the shared rule -- which is Chapter 13.3 §4's scope rule
  //    made structural rather than commented.
  logic [CNT_W-1:0] cnt_a [BANKS];
  logic [CNT_W-1:0] cnt_c [BANKS];
  logic [CNT_W-1:0] cnt_b;

  // ── Source cycles, carried purely so the report can name the event.
  logic [TS_W-1:0]  src_a [BANKS];
  logic [TS_W-1:0]  src_c [BANKS];
  logic [TS_W-1:0]  src_b;

  // ── Which obligations constrain the observed command. Applicability
  //    by resource, exactly as §2 step 4 prescribes.
  logic blocks_a, blocks_b, blocks_c;
  logic [CNT_W-1:0] rem_a, rem_b, rem_c;

  always_comb begin
    rem_a = cnt_a[cmd_bank];
    rem_c = cnt_c[cmd_bank];
    rem_b = cnt_b;

    // A column command needs its bank (rule A) and the data path
    // (rule B). A precharge needs its bank (rule C). An activate is
    // unconstrained in this abstract rule set.
    blocks_a = cmd_valid && (cmd_kind == KIND_COL) && (rem_a != '0);
    blocks_b = cmd_valid && (cmd_kind == KIND_COL) && (rem_b != '0);
    blocks_c = cmd_valid && (cmd_kind == KIND_PRE) && (rem_c != '0);

    violation = blocks_a || blocks_b || blocks_c;

    // ── Report the CONTROLLING obligation: the largest remaining.
    //    Reporting an arbitrary violated rule would send a debugger to
    //    an obligation that may clear immediately while the command
    //    stays blocked -- Chapter 13.1 §14 exercise 4.
    if (blocks_c) begin
      // A precharge can only violate C here, so no comparison needed.
      violated_rule     = 2'd2;
      violated_resource = cmd_bank;
      source_cycle      = src_c[cmd_bank];
      required_cycle    = src_c[cmd_bank] + TS_W'(MAG_C);
      shortfall         = rem_c;
    end else if (blocks_a && (!blocks_b || (rem_a >= rem_b))) begin
      violated_rule     = 2'd0;
      violated_resource = cmd_bank;
      source_cycle      = src_a[cmd_bank];
      required_cycle    = src_a[cmd_bank] + TS_W'(MAG_A);
      shortfall         = rem_a;
    end else if (blocks_b) begin
      violated_rule     = 2'd1;
      // The shared data path is not a bank. Reported as zero, and the
      // rule code is what tells a reader the resource is shared.
      violated_resource = '0;
      source_cycle      = src_b;
      required_cycle    = src_b + TS_W'(MAG_B);
      shortfall         = rem_b;
    end else begin
      violated_rule     = 2'd0;
      violated_resource = '0;
      source_cycle      = '0;
      required_cycle    = '0;
      shortfall         = '0;
    end
  end

  // ── Any obligation live, for coverage. A run in which this never
  //    rises has not exercised the checker at all.
  always_comb begin
    any_outstanding = (cnt_b != '0);
    for (int unsigned i = 0; i < BANKS; i++) begin
      if ((cnt_a[i] != '0) || (cnt_c[i] != '0)) begin
        any_outstanding = 1'b1;
      end
    end
  end

  // ── Sequential: age every countdown, then apply any new arming.
  //    Arming takes priority over ageing on the same cycle, matching
  //    deadline_scoreboard's documented rule so the two are comparable.
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      now   <= '0;
      cnt_b <= '0;
      src_b <= '0;
      for (int unsigned i = 0; i < BANKS; i++) begin
        cnt_a[i] <= '0;
        cnt_c[i] <= '0;
        src_a[i] <= '0;
        src_c[i] <= '0;
      end
    end else begin
      now <= now + 1'b1;

      // Age.
      if (cnt_b != '0) cnt_b <= cnt_b - 1'b1;
      for (int unsigned i = 0; i < BANKS; i++) begin
        if (cnt_a[i] != '0) cnt_a[i] <= cnt_a[i] - 1'b1;
        if (cnt_c[i] != '0) cnt_c[i] <= cnt_c[i] - 1'b1;
      end

      // Arm. Loaded with MAG-1 per the convention in the header: the
      // issuing cycle cannot host a competing candidate, so only the
      // MAG-1 cycles after it need to be blocked.
      if (cmd_valid) begin
        if (cmd_kind == KIND_ACT) begin
          cnt_a[cmd_bank] <= (MAG_A >= 1) ? CNT_W'(MAG_A - 1) : '0;
          src_a[cmd_bank] <= now;
          cnt_c[cmd_bank] <= (MAG_C >= 1) ? CNT_W'(MAG_C - 1) : '0;
          src_c[cmd_bank] <= now;
        end else if (cmd_kind == KIND_COL) begin
          cnt_b <= (MAG_B >= 1) ? CNT_W'(MAG_B - 1) : '0;
          src_b <= now;
        end
      end
    end
  end

endmodule

Interface contract. cmd_valid, cmd_kind and cmd_bank describe the command being issued this cycle. violation pulses on the same cycle as the offending command, so a waveform aligns cause and report with no mental offset. Every report output is meaningful only while violation is high.

Parameter contract. MAXMAG is computed with nested ternaries rather than a function so the whole block stays a single self-contained module. CNT_W is sized as $clog2(MAXMAG + 1) — the count can equal a magnitude, so it must be sized as a count and not as an index. Sizing it $clog2(MAXMAG) would truncate whenever MAXMAG is a power of two, which is the truncating-cast bug class this corpus keeps meeting; with MAG_C of 8 it would silently forbid nothing at all.

Why the casts are safe. CNT_W'(MAG_A - 1) holds a value at most MAXMAG - 1, and CNT_W was sized for MAXMAG. TS_W'(MAG_C) widens, since magnitudes are small and TS_W is a counter width. SLOT-style bank indices are bounded by BANKS - 1 throughout.

Corner cases. MAG of 0 arms nothing and the rule never fires — correct, since a zero separation forbids nothing. MAG of 1 loads 0, so the obligation forbids only the issuing cycle, which no other command can occupy. BANKS == 1: BK_W is 1 by the guard and the bank-local rules degenerate to global, which is correct for a single-bank device and is exactly the configuration where Chapter 13.3 §4's Bug 2 is invisible. A command arriving on the cycle an obligation expires: the countdown reaches zero before that cycle's combinational check reads it, so the command is legal — matching Chapter 13.2 §6's convention that cycle N + D is legal.

Failure modes. Ageing after arming instead of before would decrement a freshly loaded countdown and under-block by one. Loading MAG instead of MAG - 1 over-blocks by one, and since a checker that over-blocks reports violations on legal commands, it produces false failures that will be dismissed as checker noise — which is worse than missing a bug, because it trains the team to ignore the checker. Reporting an arbitrary violated rule rather than the controlling one produces a diagnostic that sends debugging to the wrong triggering event.

7. Five Assertions Worth Writing

These are properties of the checker, which is itself a piece of logic that can be wrong.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── P1. The checker never reports a violation on a command that no
//    obligation constrains. Catches applicability errors -- the
//    checker blaming a rule that does not apply to this command class,
//    which is §2 step 4 implemented incorrectly.
property p_violation_implies_applicable_rule;
  @(posedge clk) disable iff (!rst_n)
    violation |-> ( ((cmd_kind == KIND_COL) && ((violated_rule == 2'd0)
                                             || (violated_rule == 2'd1)))
                 || ((cmd_kind == KIND_PRE) &&  (violated_rule == 2'd2)) );
endproperty
a_violation_implies_applicable_rule: assert property (p_violation_implies_applicable_rule);

// ── P2. A reported violation always has a nonzero shortfall, and a
//    command with no shortfall is never reported. An equivalence, so
//    it catches both a missed violation and a spurious one. The
//    spurious direction matters more than it looks: a checker that
//    cries wolf gets switched off.
property p_shortfall_matches_violation;
  @(posedge clk) disable iff (!rst_n)
    violation == (cmd_valid && ( ((cmd_kind == KIND_COL)
                                   && ((cnt_a[cmd_bank] != '0) || (cnt_b != '0)))
                              || ((cmd_kind == KIND_PRE)
                                   && (cnt_c[cmd_bank] != '0)) ));
endproperty
a_shortfall_matches_violation: assert property (p_shortfall_matches_violation);

// ── P3. The reported rule is the CONTROLLING one when a column
//    command violates both of its rules. Catches a report that names
//    whichever rule the if-chain happened to test first, which would
//    be true and misleading -- Chapter 13.1 §14 exercise 4 in the
//    checker rather than in the design.
property p_reported_rule_is_controlling;
  @(posedge clk) disable iff (!rst_n)
    (violation && (cmd_kind == KIND_COL)
              && (cnt_a[cmd_bank] != '0) && (cnt_b != '0))
      |-> ( (violated_rule == 2'd0) ? (cnt_a[cmd_bank] >= cnt_b)
                                    : (cnt_b >  cnt_a[cmd_bank]) );
endproperty
a_reported_rule_is_controlling: assert property (p_reported_rule_is_controlling);

// ── P4. Arming is scope-correct: an ACTIVATE to one bank must never
//    disturb another bank's countdown. THE property for Chapter 13.3
//    §4's Bug 1 -- a bank-local obligation leaking to other banks.
//    genvar-wrapped because a procedural for-loop is illegal inside a
//    property, and $past needs an explicit 1.
generate
  for (genvar gb = 0; gb < BANKS; gb++) begin : g_scope
    property p_arming_is_bank_local;
      @(posedge clk) disable iff (!rst_n)
        ( $past(cmd_valid, 1) && ($past(cmd_kind, 1) == KIND_ACT)
                              && ($past(cmd_bank, 1) != BK_W'(gb))
                              && ($past(cnt_a[gb], 1) == '0) )
          |-> (cnt_a[gb] == '0);
    endproperty
    a_arming_is_bank_local: assert property (p_arming_is_bank_local);
  end
endgenerate

// ── P5. The shared obligation is armed by a column command to ANY
//    bank. The mirror of P4, and THE property for Bug 2 -- a shared
//    obligation wrongly held per bank would fail this, because a
//    column command to one bank would leave the shared count idle.
property p_shared_arms_from_any_bank;
  @(posedge clk) disable iff (!rst_n)
    ( $past(cmd_valid, 1) && ($past(cmd_kind, 1) == KIND_COL) && (MAG_B >= 2) )
      |-> (cnt_b != '0);
endproperty
a_shared_arms_from_any_bank: assert property (p_shared_arms_from_any_bank);

What these prove. That the checker applies rules only where they belong, that its verdict and its shortfall agree in both directions, that its diagnostic names the controlling rule, and — most valuably — that its scoping is correct in both directions: bank-local obligations stay bank-local, and the shared obligation arms from any bank.

What these do not prove. The honest list is longer than usual, and worth reading as an argument about the limits of block-level verification.

They do not prove the magnitudes are right. Parameters. A checker with every magnitude one cycle short passes all five properties and certifies illegal traces as legal. This is the most dangerous possible checker bug, because it converts a verification asset into a false assurance, and no property of the checker can catch it — the check has to be against the datasheet, by a human, once, carefully.

They do not prove the rule set is complete. The abstract set has three rules; a real device has many more. A checker missing a rule is silent about violations of it, and silence is indistinguishable from correctness. Completeness is a review activity, not a simulation one.

P4 and P5 are implications with narrow antecedents. P4 requires an activate to a different bank whose count was already zero; P5 requires MAG_B >= 2. Neither fires on a lazily-stimulated run. Cover both, and cover any_outstanding.

8. DV — Why the Report Matters More Than the Verdict

A timing checker that prints TIMING VIOLATION at cycle 27 has done about a fifth of its job.

That message tells you something is wrong and nothing about what. The engineer who receives it must reconstruct, from a waveform, which command was at fault, which rule it violated, which event started the obligation, and how much earlier the command was than it should have been. That reconstruction is the expensive part, and the checker was in a position to do it for free — it already had every fact.

A report worth having:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  TIMING VIOLATION
    command        : column command, bank 3
    issued at      : cycle 27
    violated rule  : A  (ACTIVATE -> column command, bank-local)
    resource       : bank 3
    source event   : ACTIVATE bank 3, cycle 21
    legal from     : cycle 29
    short by       : 2 cycles

Every line answers a question the engineer would otherwise have to dig for, and the structure mirrors Chapter 13.1 §4's five parts of an obligation — which is not a coincidence, because a violation is an obligation that was not honoured, and naming all five parts is what makes it actionable.

The single most valuable line is violated rule, because it identifies the class of mistake. Rule A failing says the controller under-waited after opening a row — a bank-local obligation. Rule B failing says it under-waited on the shared data path. Those are different bugs in different parts of the design, and distinguishing them is the difference between a targeted fix and a search.

The second most valuable is source event, because it converts “something is wrong with timing” into a specific prior cycle to go and look at.

Reconciling checker and design. Run both and compare every cycle: deadline_scoreboard's all_clear against this checker's violation. They are computing the same thing in incompatible representations, so any disagreement is a bug in one of them — and which one is usually obvious from the direction. The design permitting what the checker forbids is the dangerous direction; the reverse usually means the checker's rule set is over-broad. This comparison is worth more than either block's own assertion suite, because it is the only check that spans the representation boundary.

9. Debugging — Reading a Timing Failure

Symptom. The checker reports a violation. You have the report from §8.

Unlike earlier chapters' symptoms, this one arrives with evidence, so the procedure is about interpreting rather than hunting. Work the report from the bottom up.

Report line saysMost likely causeNext step
Short by exactly 1, consistentlyOff-by-one in a convention, on one side or the otherChapter 13.2 §6. Determine whether the design or the checker is wrong — a checker that over-blocks by one is the more common of the two, and the more embarrassing.
Short by a large, varying amountThe obligation was not armed at allCheck whether the design arms this rule. An unarmed obligation looks like a wildly early command.
Rule B, and traffic spans banksShared obligation held per bank — Bug 2Chapter 13.3 §4. Confirm by running single-bank traffic; if the violation disappears, this is it.
Rule A, one bank onlyA bank-local obligation genuinely under-waitedThe controller's per-bank state. Compare the source cycle against the design's own record of it.
Violation on a command that looks legal by handThe magnitudes disagreeThe checker's parameters against the design's configuration against the datasheet. Three places, and all three must match.
Violations only after long uptimeThe design's wraparound — the checker has noneChapter 13.3 §11. The representation inversion is what made this visible.

The discriminator that separates the two most common cases is whether the shortfall is consistently 1. A constant shortfall of exactly one cycle across many unrelated violations is an off-by-one in a convention, and the fix is a single line. A varying shortfall is a missing or mis-armed obligation, and the fix is structural. Establishing which before investigating saves the most time, and it takes one glance at a handful of reports.

And the direction that needs the most discipline: suspect the checker. A checker that over-blocks produces violations on legal commands. If the team concludes the design is broken and “fixes” it by lengthening a magnitude, the result is a slower design and a checker that now passes — a bug successfully converted into permanent, invisible cost. Work the hand analysis from §2 on one reported violation before changing anything. Six steps with a pencil settles who is right.

10. Common Misconceptions

“A timing checker should reuse the controller's timer values and state.” Tempting because it guarantees agreement and avoids duplicated work, and duplicated work feels wasteful. Why it is wrong: guaranteed agreement is precisely the problem. A checker sharing the design's representation and state shares its bugs, and reports success on a broken design. Consequence: a verification asset that provides confidence without providing checking — the worst possible outcome, because the team stops looking. Replacement model: independent representation, derived from externally visible events. Debugging clue: a checker that has never once disagreed with its design over a long programme is more suspicious than one that occasionally does.

“If the command is legal at the earliest legal cycle, issue it there.” Tempting because earliest sounds optimal, and for a single command in isolation it is. Why it is wrong: it converts every minimum into a fixed latency and forfeits all scheduling freedom. §5 covered the consequence. Consequence: a policy layer that cannot improve anything because every decision was already made. Replacement model: the earliest legal cycle is a floor; policy chooses above it. Debugging clue: adding a smarter scheduler produces no measurable change.

“Improving any constraint improves performance.” Tempting because every constraint contributes to the maximum, so reducing one must reduce the maximum. Why it is wrong: only the controlling obligation determines the maximum. Reducing any other changes nothing at all until it becomes controlling. §4 demonstrated it — moving an activate two cycles earlier bought exactly zero. Consequence: optimisation work with no measurable result and no explanation. Replacement model: find the controlling constraint first; improve that; re-measure, because the controlling constraint will have changed. Debugging clue: a change that should have helped and did not.

“The analysis can be done once for a command type.” Tempting because the rules are fixed, so the answer feels like a property of the command class. Why it is wrong: deadlines depend on when prior events occurred, which changes constantly. The same column command to the same bank has a different earliest legal cycle every time. Consequence: a controller with precomputed per-type delays that is right on average and wrong specifically. Replacement model: legality is evaluated per candidate per cycle against current history. Debugging clue: violations that correlate with unusual command sequences rather than with any particular command.

“If all obligations are outstanding, take the maximum over all of them.” Tempting because the maximum rule says to take a maximum, and taking it over everything is the conservative reading. Why it is wrong: the maximum is over applicable obligations. §3's candidate 5 showed an obligation with a later deadline correctly excluded because the precharge did not use the data path. Consequence: commands needlessly delayed — safe and slow, and arrived at by being careful, which makes it particularly hard to argue with. Replacement model: step 4 first, step 6 second. Applicability is decided by resource, before any arithmetic. Debugging clue: a controller that is conservative in a way nobody can attribute to a specific rule.

“A checker that never fires is proof the design is correct.” Tempting because that is what a passing test looks like. Why it is wrong: it is equally consistent with a checker whose rules never applied, whose magnitudes are too short, or whose stimulus never produced an interesting sequence. Consequence: confidence proportional to nothing. Replacement model: a checker must be shown capable of firing — inject a deliberate violation and confirm it is caught, with the right rule and the right shortfall. Debugging clue: if you cannot say when the checker last reported anything, you do not know that it works.

“Timing analysis is what static timing analysis tools do.” Tempting because the phrase is the same and both concern time. Why it is wrong: static timing analysis is about whether a signal settles within a clock period — a question about the implementation of logic. This chapter is about whether a command is permitted at a cycle — a question about a protocol contract, independent of how fast anything settles. Consequence: engineers look for the wrong kind of problem, and the two communities talk past each other. Replacement model: two unrelated disciplines that share a word. Debugging clue: if the proposed fix is about gate delay, it is the other kind of timing.

11. Interview Reasoning

“Walk me through deciding whether a DDR command is legal at a given cycle.” Give the procedure, in order, and make the ordering an explicit point. Identify the command and operands; identify the resources it needs; check semantic and state legality first, since timing analysis of a state-illegal command is wasted; list obligations attached to those resources; compute each deadline as source plus magnitude; take the maximum and record which obligation held it; compare with the candidate's cycle. Then, if issued, arm the new obligations — because the issued command is itself a triggering event. The two steps worth flagging as error-prone are listing the obligations, where a missed edge produces a confidently early answer, and updating state, where hand analyses drift.

“How do you identify the controlling constraint, and why does it matter?” It is the obligation whose deadline equals the maximum. It matters because it is the only one worth changing: improving a non-controlling constraint moves nothing. The concrete version is the strong answer — three deadlines of 12, 15 and 14 means the command is legal at 15, and reducing the constraint that produced 12 changes nothing at all until it would exceed 15. This is why optimisation work so often produces no measurable result.

“Why should a timing checker use a different representation from the design?” To avoid common-mode failure. If both use countdowns and both make the same off-by-one, they agree and both are wrong — and the checker now provides false assurance, which is worse than no checker. Inverting the representation means the two fail differently: a countdown checker has no timestamps, so it structurally cannot reproduce a wraparound bug in a timestamp-based design. The related discipline is deriving obligations from the observed command stream rather than from the design's own armed signals, so the checker does not inherit the design's decision about what to arm.

“Your checker reports a violation. Is the design wrong?” Not necessarily, and saying so is the point of the answer. The checker is logic too, and an over-blocking checker reports violations on legal commands. Look at the shortfall first: a consistent shortfall of exactly one cycle across unrelated violations suggests a convention mismatch, in either party. Then work the hand analysis on one reported violation. The failure mode to avoid is lengthening a magnitude until the checker passes, which converts a possible checker bug into a permanently slower design and hides the evidence.

“A command is issued at its earliest legal cycle every time. Any concerns?” Two. It forfeits scheduling freedom — a minimum separation has no upper bound, so a controller may legitimately defer, and one that never does has no room for policy. And it suggests the design is treating minima as fixed latencies, which is a conceptual error that will cause real trouble where a genuine fixed latency appears, because there the offset is a two-sided commitment and being ready late loses data.

“How would you convince yourself a timing checker actually works?” Inject a deliberate violation and confirm it is caught with the correct rule, source event and shortfall — a checker that has never been shown to fire is not known to work. Then cover its antecedents, so the properties are not passing vacuously. Then reconcile it against an independent implementation every cycle and investigate any disagreement. And finally, check the magnitudes against the datasheet by hand, because no property of the checker can detect a checker whose parameters are wrong.

“Where does this stop and where does the scheduler begin?” This produces, for each candidate, a legality verdict and an earliest legal cycle — a question with one correct answer, determined by the device contract and identical for every controller attached to that device. The scheduler chooses among the legal candidates by policy, a question with no single correct answer. The reason to keep them apart is that a legality bug is a correctness failure while a policy bug is a performance one, and a design that mixes them cannot tell you whether a stall was required or chosen.

12. Engineering Exercises

1. Run the procedure cold. Using §3's constraint set, analyse a column command to bank 2 at cycle 19, given: ACTIVATE bank 2 at cycle 16, and a column command to bank 0 issued at cycle 18. Show all eight steps. Which obligation controls?

Worked: resources are bank 2 and the data path. Semantic and state legal, assuming bank 2 holds the right row. Applicable: A(bank 2), deadline 16 + 4 = 20; B, deadline 18 + 2 = 20. C does not apply. Maximum is 20, with A and B tied. 19 is less than 20, so illegal, short by 1, and both obligations are controlling — meaning reducing either alone would change nothing.

2. Break the tie two ways. In exercise 1, first move the activate to cycle 15, then instead move the bank-0 column command to cycle 17. Recompute each. Which single change makes the candidate legal at 19, and what does the pair of answers tell you?

Worked: with the activate at 15, A becomes 19 and B stays 20, so the maximum is 20 — still illegal. With the column command at 17, B becomes 19 and A stays 20, so the maximum is 20 — still illegal. Neither change alone helps, because each leaves the other obligation at 20. A tie means every tied obligation must improve together, which is a genuinely useful result: it explains why some performance problems resist single-parameter fixes entirely.

3. Find the missing edge. A colleague analyses §3's candidate 3 and reports it legal at cycle 5, having found only A(bank 1) with a deadline of 6. Wait — that gives 6, not 5. Reconstruct what they must have done wrong, and say which step of §2 they skipped.

Worked: a deadline of 6 cannot yield a verdict of legal at 5, so the arithmetic error is not in step 5. They must have used the bank-1 activate at cycle 2 with a magnitude that produced a deadline of 5 or less — most likely applying the convention as M - N > D versus >= D, or mis-reading the activate's cycle. Separately they also skipped step 4 by missing rule B, which coincidentally gives the same deadline here. Two independent errors that a correct answer would have masked, which is exercise 1's lesson in reverse.

4. Build the report. For §3's candidate 5, write out the full §8 report as the checker would emit it. Which line tells a debugger the most, and why?

Worked: command PRECHARGE bank 0; issued at cycle 6; violated rule C, ACTIVATE → PRECHARGE, bank-local; resource bank 0; source event ACTIVATE bank 0 at cycle 0; legal from cycle 7; short by 1. The most valuable line is violated rule, because C is bank-local and triggered by an activate — which immediately says the bug is in per-bank row-state timing rather than anywhere near the data path, and rules out the shared obligation whose deadline of 8 is the tempting red herring here.

5. Test the off-by-one, both directions. Set MAG_A to 4 and issue an ACTIVATE at cycle 0 and a column command at cycle 4. Confirm no violation. Now change the checker to load MAG_A rather than MAG_A - 1 and re-run. What is reported, and why is a checker that over-blocks more damaging in practice than one that under-blocks?

Worked: the correct version permits cycle 4 — the convention's earliest legal cycle. The modified version reports a violation short by 1 on a legal command. Over-blocking is more damaging socially: it produces failures on correct designs, the team learns to dismiss the checker's output, and the checker's real findings are then dismissed along with the noise. An under-blocking checker is silently useless; an over-blocking one actively destroys trust in the verification.

6. Invert the representation yourself. Rewrite issue_timing_checker's countdowns as absolute timestamps. What new failure mode have you introduced, what guard does it need, and which chapter owns that material?

7. Extend the rule set. Add a fourth abstract rule: column command → PRECHARGE, same bank, at least 3 cycles. Re-analyse every candidate in §3's trace. Which verdicts change? What does this say about the completeness argument in §7?

Worked: the new rule applies to candidates 5 and 6. The last column command before candidate 5 at cycle 6 is the bank-1 command, which is a different bank, so the bank-local rule does not apply to bank 0 — candidate 5 is unchanged. But the bank-0 column command at cycle 4 gives a bank-0 deadline of 4 + 3 = 7, which ties C's 7, so candidate 5 is still illegal at 6 and candidate 6 still legal at 8. No verdict changes — and that is the point: a missing rule produced correct answers throughout this trace, which is exactly why completeness cannot be established by testing.

8. Reconcile two implementations. Instantiate deadline_scoreboard and issue_timing_checker on the same command stream with matching magnitudes. Write the comparison logic. Then deliberately introduce an unsigned comparison into the scoreboard, shrink its TS_W to 4, and report how many cycles pass before the two disagree.

9. Prove the checker can fire. Construct the shortest stimulus that produces a violation on each of the three rules, with the correct rule reported each time. Then argue why a checker without this test should not be trusted.

13. Summary

Legality analysis is a procedure, and the procedure is worth running mechanically: identify the candidate, identify the resources it needs, check semantic and state legality before any arithmetic, list the obligations attached to those resources, compute each deadline as source plus magnitude, take the maximum and record which obligation held it, compare with the candidate's cycle, and — if issued — arm the new obligations, because the issued command is itself a triggering event.

Two steps carry most of the errors. Listing the applicable obligations is where a missed edge produces a confidently early answer, and the defence is to work from resources rather than memory. Updating state after each issue is where hand analyses drift into computing against stale history.

The worked trace made three things concrete that the earlier chapters could only assert. Two obligations of different classes and scopes can tie, so getting the right answer is not evidence the method was right. An obligation with a later deadline is correctly excluded when the candidate does not need its resource — applicability precedes arithmetic. And improving a non-controlling constraint changes nothing at all, which is why step 6 records the controlling obligation and not merely the maximum.

The earliest legal cycle is a floor, not a target. Minimum separations have no upper bound, so a scheduler may always defer; a design that issues at the earliest legal cycle every time has converted its minima into fixed latencies and spent its scheduling freedom before any policy could use it.

issue_timing_checker is verification-only and deliberately built in the opposite representation to the design block of Chapter 13.3. Countdowns against timestamps, obligations derived from observed commands rather than from the design's own state, reporting rather than gating. Those three choices are what stop a checker from sharing the bugs it was built to find. And its report — rule, resource, source event, legal-from cycle, shortfall — is where most of its value lives, because a verdict tells you something is wrong while a report tells you what to change.

14. Module 13 in One Chain, and What Comes Next

The module set out to answer one question: why can a controller not issue every logically valid command immediately, and how should an engineer reason about when a command becomes temporally legal?

The chain is complete.

A command must pass three independent tests — semantic, state and timing — and they draw on three different kinds of information: the command, the present state, and the history. Timing needs its own machinery because history is not recorded anywhere else. (Chapter 13.1)

Obligations originate in processes that take absolute time and are indifferent to any clock, so their magnitudes must be converted into integer cycle counts by a ceiling — which makes the same physical requirement cost more cycles at a higher clock, and makes a cycle count meaningless without a frequency. A magnitude is still not a rule until the sampling convention fixes which cycle is first legal. (Chapter 13.2)

Obligations come in four classes and attach to resources, and attaching one to the wrong resource produces two opposite bugs — fast and wrong, or safe and slow. Because legality is a conjunction of lower bounds, a candidate becomes legal at the maximum of every applicable deadline. (Chapter 13.3)

And that composes into a procedure you can run by hand or build into a checker, provided the checker is independent enough to disagree. (This chapter)

Timing is a set of temporal dependencies between events — a directed graph over event classes, where each edge carries a class, a scope and a magnitude, and where issuing a command adds edges. It was never a list of numbers to memorise.

What the framework does not yet have is its contents. Every edge in this module has been called A, B, C or α, and every magnitude has been a teaching value — with the single verified exception in Chapter 13.2 §5, which existed to prove the specification form was real rather than invented.

Module 14 fills it in. tRCD, tRP, tRAS, tRC, tWR, tCCD, tRRD, tFAW, CL, CWL — each one is an edge with the five parts of Chapter 13.1 §4, a class from Chapter 13.3 §2, a scope from Chapter 13.3 §4, and a magnitude resolved the way Chapter 13.2 resolved one. They will not be a list of fifteen things to memorise. They will be fifteen instances of a structure you already have.

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.