Skip to content
VLSI Mentor

DDR · Module 17

The Command Scheduler

A DDR command scheduler does not schedule requests. It re-derives each outstanding request's next required command every cycle, and advances architectural state only at the commit point where a command is genuinely issued.

Sixteen modules have built the device's side of the contract: what a bank is, what a command means, what timing permits, what refresh obliges. This module builds the other side — the digital block that has to satisfy all of it, every cycle, for many outstanding requests at once.

It opens with the question the whole module turns on, and the answer is not the obvious one:

A DDR command scheduler does not schedule requests. It schedules commands — and the commands do not exist until the scheduler derives them, every cycle, from request state that keeps changing underneath it.

That distinction sounds pedantic until you try to build one. Then it decides your entire architecture.

1. Five Requests, One Issue Slot

Start where a scheduler actually starts: with more work than it can do, and no obvious ordering.

Five requests are outstanding. Bank state is what it is — nobody arranged it conveniently.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  BANK STATE
    bank 0    OPEN, row 12
    bank 1    OPEN, row 40
    bank 2    CLOSED
    bank 3    OPEN, row  7
    bank 4    PRECHARGING

  OUTSTANDING REQUESTS
    R0   READ   bank 0, row 12      arrived cycle 100
    R1   READ   bank 1, row 55      arrived cycle 102
    R2   WRITE  bank 2, row 19      arrived cycle 103
    R3   READ   bank 3, row  7      arrived cycle 111
    R4   WRITE  bank 4, row  2      arrived cycle 112

Before asking which one to serve, ask what each one needs next — because they do not all need the same kind of thing. Chapter 9.3's classification gives the answer:

ReqBank stateRowClassNext command needed
R0open, row 12matchrow hitcolumn RD
R1open, row 40differsrow conflictPRE
R2closedclosed bankACT
R3open, row 7matchrow hitcolumn RD
R4prechargingbank busynothing yet

Four different next commands across five requests — and one of them wants nothing at all.

2. Eight Questions per Request

The table in §1 answered only the first question. A scheduler must answer eight, and they are not interchangeable — each can independently veto.

Add the timing state §1 left out, because it is where two of the five requests actually die:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  TIMING STATE at cycle 120
    bank 0   last column command   cycle 118   (tCCD_L not yet met)
    bank 1   row 40 activated      cycle  95   (tRAS met — PRE allowed)
    bank 2   last ACT anywhere     cycle 119   (tRRD not yet met)
    bank 3   row  7 activated      cycle 119   (tRCD NOT met)
    bank 4   PRE issued            cycle 117   (tRP not yet met)

  REFRESH   pending, not urgent — normal issue still allowed

Now every question, for every request. EDUCATIONAL TIMING — NOT JEDEC VALUES.

QuestionR0R1R2R3R4
1. Request valid?yesyesyesyesyes
2. Next command needed?RDPREACTRDnone
3. State legal?yesyesyesyesno — bank busy
4. Timing legal?no — tCCDyesno — tRRDno — tRCDn/a
5. Resource legal?yesyesyesyesn/a
6. Refresh permits?yesyesyesyesn/a
7. Candidate legal?noYESnonono
8. Policy preferenceonly candidate
Issued?noYES — PREnonono

The three failure modes in that table are genuinely different, and a controller that cannot tell them apart cannot be debugged.

  • R4 fails at question 3: state. Nothing it does will help; it needs the device to finish precharging. Waiting is the only action.
  • R0, R2, R3 fail at question 4: timing. Each will become legal at a computable future cycle — this is Chapter 13.1 §6's distinction between a refusal that needs work and one that needs only patience.
  • Nothing fails at questions 5 or 6 this cycle. Both matter enormously when they do fire, and both are invisible in a controller that folds all six questions into one ready bit.

Now change one thing. Make refresh urgent:

QuestionR0R1R2R3R4REF
2. Next commandRDPREACTRDnoneREF
4. Timing legal?noyesnonon/a
6. Refresh permits?nonononon/ayes
Candidate legal?nononononoYES
Issued?nononononoYES — REF

R1 was the only legal candidate and is now blocked — not by anything about R1, but because question 6 turned off for every normal command at once. This is the refresh gate of Chapter 17.3, and it is the one veto that applies globally rather than per request.

Note what does not happen: refresh does not win an arbitration against R1. It is not a competitor with higher priority. R1 stops being a legal candidate at all, and REF becomes the only one. Legality changed; policy was never consulted. Modelling refresh as a high-priority request in the arbiter is §16's misconception and produces a controller that occasionally issues normal traffic into a refresh window.

3. One Request Is Many Commands

R1 is a single transaction. Serving it takes a sequence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  R1  (one READ request, bank 1, row 55)

     PRE  bank 1          close row 40
       ↓  wait tRP
     ACT  bank 1, row 55  open the wanted row
       ↓  wait tRCD
     RD   bank 1, col c   launch the column access
       ↓  wait CL
     data burst returns

     completion

Three commands, three waiting periods, then a data phase — for one request. Meanwhile R0 needs exactly one command, and R4 needs none.

This is where the first genuinely tempting architecture appears, and it is worth taking seriously because it is not stupid.

This is why Chapter 17.2 is titled the command queue but is really about request entries: the storage holds intent, and the command is recomputed.

4. The Pipeline

Here is the spine. Every remaining chapter of Module 17 owns one box of it.

The DDR command scheduler pipeline, drawn as seven stages across four columns. Upstream requests enter through an ingress stage owned by chapter seventeen point five, which applies backpressure and allocates entries. Allocated entries live in a request entry pool owned by chapter seventeen point two, holding transaction intent rather than commands. Each cycle, next-command derivation reads every valid entry together with current bank state and produces a candidate command per entry. Those candidates pass through a legality filter, which is not rebuilt in this module but consumed from chapter sixteen point two's bank candidate mask and chapter thirteen point one's legality layers, and which is additionally gated by the refresh manager of chapter seventeen point three. The surviving legal candidates go to arbitration, owned by chapter seventeen point four, which selects at most one and asserts a grant. The grant reaches the commit stage owned by this chapter, which issues the command only if the physical interface accepts it, and only then advances architectural state. State updates flow back to bank state, timing state and entry progress, closing the loop.Upstream requestready / validIngress17.5 — accept, allocateEntry pool17.2 — intent, notcommandsNext-commandderivationthis chapter — everycycleLegality filter13.1 + 16.2 — reusedRefresh gate17.3 — obligationArbitration17.4 — policy, one winnerCOMMITthis chapter — §7Issued commandto the deviceState updatebank + timing + progressphy_cmd_ready19–21 — abstract hereBank state5.2 — reused12

Two features of this drawing are the whole architecture.

It is a loop, not a line. State updates feed back into derivation. That is why §3's pre-expansion fails: the derivation stage must see the current loop state, not a snapshot taken at admission.

Legality and policy are different boxes, and their order is fixed. Chapter 16.2 §3 established why at vector scale: an arbiter that also decides legality cannot report why nothing issued, and makes its own input depend on its own previous behaviour. Module 17 inherits that discipline rather than re-arguing it.

5. What This Chapter Does Not Build

An honest map, because a new module is not permission to grow a second copy of the architecture.

Pipeline stageAlready owned byBlockModule 17 does
Row classification9.3row_request_classifierconsume
Bank state5.2ddr_bank_state_tableconsume
Three legality layers13.1command_legality_layersconsume
Per-bank legal vector16.2bank_candidate_maskconsume
Timing deadlines13.3deadline_scoreboardconsume
Issued-stream checking13.4issue_timing_checkerreuse as a checker
Refresh obligation accounting15.3refresh_credit_ledgerwrap in 17.3

So what is left? Exactly one thing in this chapter, and it is the thing none of those blocks can do: decide when the whole machine is allowed to change its mind about reality.

6. Request Progress Is a State Machine

Each entry in the pool carries a progress state. The derivation stage reads it together with bank state; the commit stage advances it.

The per-request progress state machine inside a DDR controller. A request begins in the waiting state on allocation. From waiting it moves to needs precharge when its target bank holds a different row, to needs activate when the bank is closed, or directly to needs column when the bank already holds the wanted row, which is the row hit path. Needs precharge advances to needs activate once a precharge command commits. Needs activate advances to needs column once an activate command commits. Needs column advances to the data phase once the column command commits. The data phase advances to done when the burst completes. Done returns the entry to waiting only by way of deallocation. Every transition out of a needs state is labelled as requiring a commit, not a grant, which is the discipline this chapter exists to teach.WAITINGNEEDS_PRENEEDS_ACTNEEDS_COLDATADONEconflict: wrong row openconflict: wrong row openconflict:wrong row…closed bankclosed bankrow hitrowhitPRE commitsPRE commitsACT commitsACT commitscolumn cmd commitscolumn cmd commitsburst completeburstcomplete

Read the transition labels carefully. Not “PRE granted”. Not “PRE is a candidate”. “PRE commits” — and §7 is about why those three are not the same event.

Note also that WAITING is not a queue position. It is a classification pending: the entry is resident, and its class is recomputed against bank state until it resolves. An entry can be re-evaluated many times before generating its first command.

7. The Commit Point

Here is the central discipline of the module.

Three things happen in sequence, and each is weaker than the next:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CANDIDATE    "this entry's next command is ACT bank 2"
                 ← speculative. Many entries produce one each cycle.

  GRANT        "policy prefers entry 2's ACT this cycle"
                 ← intent. At most one. Still has not happened.

  COMMIT       "the ACT was issued and the device interface took it"
                 ← fact. THE ONLY EVENT PERMITTED TO CHANGE STATE.

Only the third may update anything. Not bank state, not timing history, not refresh accounting, not entry progress.

The commit condition itself is small:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  commit = grant_valid && phy_cmd_ready && normal_issue_allowed

Three terms, three owners. grant_valid from 17.4. phy_cmd_ready from the interface (Modules 19–21 own what is behind it). normal_issue_allowed from 17.3. The scheduler owns none of the three inputs and all of the consequence.

8. Who Owns Which State

Duplicated ownership is the most expensive class of controller bug, because both copies look right in isolation and the divergence only appears under a sequence nobody wrote a test for. So name the owner of every piece of architectural state once, and make every other block a reader.

StateOwnerUpdated onRead byMust not be updated on
Open row per bank5.2 bank statecommit of ACT / PREderivation, legalitycandidate generation; grant
Bank busy / transitioning5.2 bank statecommit + elapsed timederivation, legalitygrant
Timing history per obligation13.3 deadline ledgercommit of the constrained commandlegality filterany non-committed candidate
Legal-candidate vector16.2recomputed combinationallyarbitrationnever stored across cycles
Entry valid / metadata17.2 poolingress accept; completionderivation, arbitrationgrant
Entry progress17.2 poolcommit via advance_onehotderivationgrant; candidacy
Refresh debt and urgency15.3 ledger, wrapped by 17.3commit of REF; interval tickrefresh gate, arbitrationgrant of REF
Refresh busy / gate17.3 managercommit of REF; occupancy expirycommit stage, arbitrationcandidacy
Arbitration pointer17.4 arbitercommit — see belowarbitrationgrant alone
Commit countersthis chaptercommit / dropped grantdebug only

The table also settles a question that comes up constantly in review: may the derivation stage cache anything? No. Every entry in the right-hand column of that table is a cached derivation someone was tempted by. The legal-candidate vector in particular is explicitly marked never stored across cycles — §3's staleness argument applies to it with full force, and it is recomputed rather than registered for exactly that reason.

9. The Commit Block

One responsibility, deliberately. It does not arbitrate, does not check legality, does not store entries. It converts intent into fact and publishes exactly who is permitted to advance.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
// command_issue_commit
//
// CLASSIFICATION
//   Synthesizable educational RTL. One architectural responsibility:
//   turning an arbitration GRANT into a COMMIT, and publishing the
//   one-hot state-advance permission that — and only that — follows.
//
// WHAT IT DOES NOT MODEL
//   - No arbitration policy. grant_* is an input (Chapter 17.4).
//   - No legality. A grant is assumed already legal (13.1, 16.2).
//   - No entry storage or progress state. That is Chapter 17.2's pool;
//     this block only says WHO MAY advance, never advances anything.
//   - No refresh accounting. normal_issue_allowed is an input (17.3).
//   - No PHY. phy_cmd_ready is an abstract digital contract (19-21) —
//     no DQS, no training, no delay lines.
//   - No DDR command encoding. Commands stay symbolic; Module 7 owns
//     the physical encoding.
// ─────────────────────────────────────────────────────────────────────
module command_issue_commit #(
  parameter int NUM_ENTRIES = 8,
  // Index into the entry pool. Guarded: $clog2(1) is 0, and a
  // zero-width signal is not legal.
  parameter int EN_W  = (NUM_ENTRIES <= 1) ? 1 : $clog2(NUM_ENTRIES),
  // Observability counters. Width is a REPORTING choice: nothing
  // compares against them, so a wrap can only make a printed number
  // ambiguous — it cannot produce a functional bug.
  parameter int CNT_W = 16
) (
  input  logic                    clk,
  input  logic                    rst_n,

  // ── Arbitration's answer. INTENT. Chapter 17.4 produces it from a
  //    legal-candidate mask; by contract it is already legal, and this
  //    block does not re-check that (§11's P4 checks the contract).
  input  logic                    grant_valid,
  input  logic [EN_W-1:0]         grant_entry,
  // 0 = PRE, 1 = ACT, 2 = column command, 3 = REF.
  input  logic [1:0]              grant_cmd,

  // ── The downstream half of the handshake. The command goes out only
  //    if the interface takes it this cycle.
  input  logic                    phy_cmd_ready,

  // ── Chapter 17.3's gate. Low while a refresh occupies the device.
  input  logic                    normal_issue_allowed,

  // ── The issued command. Combinational: it leaves the same cycle the
  //    commit happens, because the commit IS the issue.
  output logic                    issue_valid,
  output logic [EN_W-1:0]         issue_entry,
  output logic [1:0]              issue_cmd,

  // ── THE COMMIT POINT. Identical to issue_valid by construction, and
  //    published separately because every consumer of "may I change
  //    state?" must name this signal rather than grant_valid. §7.
  output logic                    commit,

  // ── One-hot advance permission. Exactly one bit set on a commit to a
  //    normal command, zero bits otherwise. Chapter 17.2's pool
  //    consumes this; nothing else may advance an entry.
  output logic [NUM_ENTRIES-1:0]  advance_onehot,

  // ── Observability. A grant that did not commit is not an error — it
  //    is ordinary backpressure — but it must be VISIBLE, because a
  //    high dropped rate is the signature of several real bugs (§15).
  output logic                    grant_dropped,
  output logic [CNT_W-1:0]        commit_count,
  output logic [CNT_W-1:0]        dropped_count,

  // ── Set if a grant names an entry outside the pool. A design error,
  //    surfaced rather than silently masked by the index width.
  output logic                    grant_index_error
);

  // ── Parameter legality, resolved at elaboration. A zero-entry pool
  //    has no meaning and would make advance_onehot zero-width.
  if (NUM_ENTRIES < 1)
    $fatal(1, "command_issue_commit: NUM_ENTRIES must be >= 1");
  if (CNT_W < 1)
    $fatal(1, "command_issue_commit: CNT_W must be >= 1");

  localparam logic [1:0] CMD_REF = 2'd3;

  // ── A REF is not owned by an entry. Chapter 17.3 requests it on
  //    behalf of the device, not on behalf of a request, so it commits
  //    without advancing anything in the pool.
  logic grant_is_ref;
  assign grant_is_ref = (grant_cmd == CMD_REF);

  // ── The three terms of §7. Note what is NOT here: no legality
  //    re-check, no policy, no queue inspection.
  //    A REF is exempt from normal_issue_allowed for the obvious
  //    reason — that gate exists to hold NORMAL traffic off while
  //    refresh proceeds, and gating REF with it would deadlock.
  assign commit = grant_valid
                && phy_cmd_ready
                && (normal_issue_allowed || grant_is_ref);

  assign issue_valid = commit;
  assign issue_entry = grant_entry;
  assign issue_cmd   = grant_cmd;

  assign grant_dropped = grant_valid && !commit;

  // ── Index sanity. Only meaningful when the pool is not a power of
  //    two; when it is, EN_W cannot represent an out-of-range value and
  //    this is constant-false, which is correct rather than dead.
  assign grant_index_error =
      grant_valid && !grant_is_ref && (int'(grant_entry) >= NUM_ENTRIES);

  // ── The one-hot advance permission. Built by comparison rather than
  //    by a shift so that an out-of-range index yields ZERO bits set
  //    rather than an aliased bit — failing safe, and visible through
  //    grant_index_error above.
  always_comb begin
    advance_onehot = '0;
    if (commit && !grant_is_ref) begin
      for (int i = 0; i < NUM_ENTRIES; i++)
        if (int'(grant_entry) == i) advance_onehot[i] = 1'b1;
    end
  end

  // ── Observability counters. Saturating, so a long run cannot wrap a
  //    reported figure back through zero and suggest the rate fell.
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      commit_count  <= '0;
      dropped_count <= '0;
    end else begin
      if (commit && !(&commit_count))
        commit_count <= commit_count + CNT_W'(1);
      if (grant_dropped && !(&dropped_count))
        dropped_count <= dropped_count + CNT_W'(1);
    end
  end

endmodule

What the block deliberately lacks. There is no always_ff holding a grant across a stall. A dropped grant is simply not committed, and arbitration re-presents it next cycle from candidates re-derived against current state. Latching it would recreate §3's staleness bug at a smaller scale: a held grant is a frozen decision, and by the time the interface accepts it the state it was computed against may be gone.

10. Cycle-by-Cycle

EDUCATIONAL TIMING — NOT JEDEC VALUES. The magnitudes are chosen to make the mechanism visible in ten cycles; real spacings come from Module 14.

Entry 1 holds R1 from §1 — bank 1 currently open on row 40, wanted row 55, so a row conflict needing PRE then ACT then RD. The interface stalls in the middle, and refresh intervenes at the end.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cyc  grant  entry cmd  phy_rdy  norm_ok  commit  advance  progress after
  ───  ─────  ───── ───  ───────  ───────  ──────  ───────  ──────────────
  200    1      1   PRE     1        1       YES    0b0010   NEEDS_ACT
  201    0      -    -      1        1        no    0b0000   NEEDS_ACT
  202    0      -    -      1        1        no    0b0000   NEEDS_ACT   (tRP)
  203    1      1   ACT     0        1        no    0b0000   NEEDS_ACT   ←
  204    1      1   ACT     1        1       YES    0b0010   NEEDS_COL
  205    0      -    -      1        1        no    0b0000   NEEDS_COL
  206    1      1   RD      1        0        no    0b0000   NEEDS_COL   ←
  207    1      1   RD      1        0        no    0b0000   NEEDS_COL
  208    1      1   RD      1        1       YES    0b0010   DATA
  209    0      -    -      1        1        no    0b0000   DATA

Two cycles carry the entire lesson, and they fail for different reasons:

Cycle 203 — the interface refused. grant_valid is high, the command is legal, policy chose it. phy_cmd_ready is low, so no command left the controller. Progress stayed at NEEDS_ACT. Had it advanced on the grant, the controller would believe bank 1 holds row 55 and would issue RD at cycle 204 to a bank that is still closed — the ghost activate of §7, and the resulting protocol violation would appear to originate at the RD.

Cycles 206–207 — refresh held the gate. Everything about the RD is fine; normal_issue_allowed is low because 17.3's manager has the device occupied. Same non-commit, entirely different cause — and grant_dropped counts both identically, which is exactly why §15's debugging procedure needs a second signal to separate them.

dropped_count reaches 3 over the trace; commit_count reaches 3. A healthy controller has a nonzero dropped count — it means arbitration is optimistic and the interface is applying real backpressure. A dropped count of zero under load is the suspicious reading, not a high one.

11. What the Assertions Prove

Where these live. The properties below are written for the block's own clk and rst_n, which command_issue_commit has, so they may sit inside the module or in a bind unit. The DV model of §12 is separate from all of them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── P1. The commit condition is exactly §7's three terms. This looks
//    tautological against the RTL, and it is — deliberately. Its value
//    is as a REGRESSION anchor: it is the property that fails the day
//    somebody adds a fourth term, or latches a grant across a stall.
property p_commit_definition;
  @(posedge clk) disable iff (!rst_n)
    commit == (grant_valid && phy_cmd_ready
               && (normal_issue_allowed || grant_cmd == 2'd3));
endproperty
a_commit_definition: assert property (p_commit_definition);

// ── P2. THE central property of the module. No entry may be given
//    advance permission without a commit. This is the one that catches
//    every §7 bug at its source rather than three cycles downstream.
property p_no_advance_without_commit;
  @(posedge clk) disable iff (!rst_n)
    (advance_onehot != '0) |-> commit;
endproperty
a_no_advance_without_commit: assert property (p_no_advance_without_commit);

// ── P3. At most one entry advances per commit. The command interface
//    carries one command; two entries progressing from one commit means
//    two requests are sharing a single command's effect.
property p_advance_onehot;
  @(posedge clk) disable iff (!rst_n)
    $countones(advance_onehot) <= 1;
endproperty
a_advance_onehot: assert property (p_advance_onehot);

// ── P4. The contract with Chapter 17.4, stated as an assumption this
//    block relies on but does not enforce: a grant names a real entry.
//    Written as an assertion on the OUTPUT that detects the violation,
//    so a broken arbiter is caught here rather than corrupting the pool.
property p_grant_index_in_range;
  @(posedge clk) disable iff (!rst_n)
    !grant_index_error;
endproperty
a_grant_index_in_range: assert property (p_grant_index_in_range);

// ── P5. A dropped grant is not lost work: it must be RE-PRESENTED once
//    the blockage clears, not silently abandoned.
//    NOTE ON THE SHAPE. The blocking condition must be sampled on the
//    cycle AFTER the drop, never alongside it. Conjoining grant_dropped
//    with phy_cmd_ready and normal_issue_allowed in one cycle yields a
//    CONTRADICTION -- those three together imply commit, and commit
//    implies !grant_dropped -- so such a property is vacuous and can
//    never fail. Written as a two-cycle sequence it is non-vacuous:
//    if the stall never lifts, the antecedent never matches and nothing
//    is claimed, which is the intended weakness.
property p_dropped_grant_returns;
  @(posedge clk) disable iff (!rst_n)
    (grant_dropped ##1 (phy_cmd_ready && normal_issue_allowed))
      |-> grant_valid;
endproperty
a_dropped_grant_returns: assert property (p_dropped_grant_returns);

// ── Covers. An assertion that never fires on a stimulus that never
//    reaches the interesting state proves nothing at all.
c_commit_then_stall: cover property
  (@(posedge clk) disable iff (!rst_n) commit ##1 grant_dropped);
c_refresh_blocked:   cover property
  (@(posedge clk) disable iff (!rst_n)
     grant_valid && phy_cmd_ready && !normal_issue_allowed);
c_ref_during_block:  cover property
  (@(posedge clk) disable iff (!rst_n)
     commit && grant_cmd == 2'd3 && !normal_issue_allowed);

What they do not prove. Nothing here establishes that the granted command was legal — that is 13.1's and 16.2's contract, verified by 13.4's issue_timing_checker watching the issued stream. Nothing establishes that the right entry was chosen — that is policy, and 17.4 owns it. Nothing establishes progress toward completion: P5 is bounded re-presentation, not liveness. A controller can satisfy every property here and still starve a request forever.

12. The Independent Model

The DV model must not mirror the RTL. If the checker computes commit the same way the design does, it agrees with the bug.

Build it from the observed stream instead. Maintain, independently:

  • a per-bank reference state — open or closed, and which row — updated only on an observed issued command;
  • a per-entry expected progress state, advanced only on an observed issue naming that entry;
  • a timing ledger of last-issue cycles per obligation, reusing 13.3's deadline representation;
  • a refresh obligation ledger from 15.3.

Then check the RTL's claims against reconstruction rather than against recomputation. The model never reads grant_valid, commit, or advance_onehot — it reads only what left the controller, which is the same thing the device sees.

A diagnostic that earns its place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  GHOST STATE ADVANCE
    cycle                : 203
    entry                : 1     (request R1, bank 1, row 55)
    RTL progress before  : NEEDS_ACT
    RTL progress after   : NEEDS_COL
    observed issue       : none
    grant_valid          : 1
    phy_cmd_ready        : 0
    commit               : 0
    model progress       : NEEDS_ACT   (unchanged — no command observed)
    diagnosis            : entry advanced on GRANT, not COMMIT
    predicted failure    : column command at cycle 204 to a CLOSED bank 1
    first visible symptom: protocol violation reported at cycle 204

The last two lines are what make it worth building. The model names the cycle the corruption began and the cycle the symptom will appear — two cycles apart, which is precisely the gap that makes this bug class expensive to find by inspection.

13. Corner Cases

SituationCorrect behaviourFailure if mishandled
grant_valid with phy_cmd_ready lowno commit, no advance, grant re-presentedghost advance
normal_issue_allowed low, normal commandno commitrefresh window violated
normal_issue_allowed low, REF grantedcommits — the gate exempts REFdeadlock: refresh can never issue
NUM_ENTRIES = 1EN_W guarded to 1; one-hot is one bitzero-width signal, elaboration failure
grant_entry out of rangeadvance_onehot all zero, error flaggedaliased bit advances the wrong request
grant and phy_cmd_ready both rise same cyclecommits that cyclea needless lost slot
commit on consecutive cyclespermitted; each advances its own entrythrottling that looks like a timing bug
reset during a granted stallcounters clear, nothing commitsphantom commit at reset release
REF commitno entry advances; advance_onehot zeroa request advances on somebody else's command

The REF exemption deserves the emphasis it gets twice. It is the kind of detail that makes a controller hang in a way that looks like a refresh-manager bug when the actual error is one missing term in the commit condition.

14. Synthesis, Cost and What Is Missing

What this block costs. command_issue_commit is a three-input AND, a comparator tree of NUM_ENTRIES equality checks against grant_entry, and two saturating counters. The comparator tree is the only part that grows with the pool, and it grows linearly in area and logarithmically in depth. Nothing here is a timing concern.

Where the real timing pressure is — and it is not here. The critical path in this architecture runs from bank state, through next-command derivation, through the legality filter, through arbitration, to grant_valid, and then through this block to issue_valid. That is a long combinational chain, and it must close in one cycle because the candidate set is only valid for the cycle it was computed in.

Production controllers attack this by pipelining the chain — deriving candidates in one stage and arbitrating in the next. That is a legitimate design, and it buys back frequency, but it reintroduces §3's problem in miniature: the arbiter is now choosing among candidates computed against last cycle's state. The resolution is to re-validate the winner against current state at the commit stage, which is a second, stricter reason the commit point exists. This chapter's block is deliberately unpipelined so the mechanism is visible; a design running at real DDR frequencies would not be.

Reset. The counters reset asynchronously and the commit path is purely combinational, so nothing can commit while rst_n is low — grant_valid will itself be low, because the arbiter is reset too. The block has no state that could survive a reset and produce a phantom first commit.

What a production controller has that this does not. Stating these plainly matters more than the RTL does, because the gap is where the “I built a DDR controller” claim usually breaks down:

  • Write data path coupling. A column write command may issue only if the write data is available to launch at the required offset (Chapter 12.2). This block's commit condition has no data-availability term.
  • Read data return tracking. Nothing here reserves the return slot or tracks which entry owns the burst; 17.5 opens this and Modules 19–21 own the capture.
  • Rank and channel dimensions. One command bus is assumed. A multi-rank controller has rank-to-rank turnaround and chip-select fan-out that change both legality and arbitration (5.4).
  • Read/write turnaround as a first-class cost. Direction changes on the shared data bus are expensive, and a real scheduler batches to amortise them — a policy dimension 17.4 opens and Module 23 quantifies.
  • Power state management. Power-down and self-refresh entry and exit gate everything above, and are not modelled anywhere in Module 17.
  • Error handling. ECC, parity on the command bus, and retry are absent.

None of these change the commit discipline. All of them add terms to the commit condition — which is itself the argument for keeping that condition in one named place rather than scattering it across the design.

15. Debugging

Symptom: the controller issues nothing, but requests are resident and the queue is filling.

Work the pipeline backwards, because each stage can produce the same silence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  is grant_valid ever high?
    ├─ NO → arbitration has no legal candidates
    │        ├─ is the legal mask empty?
    │        │    ├─ YES → is it empty because of timing, state, or the
    │        │    │        refresh gate? 16.2's bank_candidate_mask
    │        │    │        publishes the three filters SEPARATELY
    │        │    │        precisely so this question has an answer.
    │        │    └─ NO  → arbitration bug: legal candidates ignored
    │        └─ are entries valid at all? → 17.2's pool
    └─ YES → grants exist but nothing commits
             ├─ phy_cmd_ready stuck low      → interface / Modules 19-21
             ├─ normal_issue_allowed stuck low → 17.3: refresh manager
             │    never left its busy state
             └─ both high but commit low     → the commit condition has
                                               grown a fourth term

dropped_count climbing while commit_count stays flat localises this in one read: grants are being produced and universally refused, so the fault is at or below the commit stage, not in candidate generation.

Symptom: one request is served twice. The device receives two ACTs for one row opening. Check whether progress advances on commit; then check whether it advances once per commit. A for loop that sets more than one bit of advance_onehot, or a pool that advances on grant_valid in one path and commit in another, produces exactly this. P3 catches the first; P2 catches the second.

Symptom: works at low load, fails at saturation. The commit stage is a strong suspect precisely because at low load phy_cmd_ready is almost always high and grant_dropped almost never asserts — so a commit-versus-grant confusion is unexercised until backpressure appears. This is why c_commit_then_stall exists as a cover: if that cover is unhit, the test suite has never exercised the bug's precondition, and a passing regression means nothing.

16. Misconceptions

“The scheduler schedules memory requests.” It schedules commands. A request is not an object the DDR interface accepts. Consequence: the architecture stores the wrong thing and cannot express a row conflict's three-command expansion. Replacement: requests are resident state; commands are derived and re-derived. Clue: a design where a request maps one-to-one onto an issue slot.

“A request maps to one DDR command.” A row hit does; a closed bank needs two; a conflict needs three, with waits between. Clue: a queue whose depth is sized as if each entry issues once.

“A command queue is a FIFO of future commands.” §3. Consequence: commands computed against state that has since changed. Replacement: store intent, re-derive. Clue: a PRE issued to a bank that is already closed.

“Queue order must equal issue order.” Allocation order, scheduling order and completion order are three different orders (17.2 §5). Clue: a controller whose bank parallelism collapses to one bank at a time.

“If a request is valid, its next command is legal.” Validity is about the request; legality is about the device's state and timing right now. Clue: an ACT issued inside a tRP window.

“If a command is legal, it should issue.” Several may be legal; one issue slot (16.1 §2). Legality admits; policy chooses. Clue: an arbiter with no defined behaviour when two candidates are equally legal.

“A candidate changes controller state.” Candidate generation is speculative and happens for every entry every cycle. Consequence: state churns on commands that were never even preferred. Clue: bank state changing on a cycle when nothing was issued.

“A granted command changes state.” The whole of §7. Clue: a controller that is correct at low load.

“A READ completes when the READ command issues.” It completes when the burst returns and is collected (17.5 §8). Freeing the entry at column-command issue loses the data's destination. Clue: read data returning with no owner.

“One FSM can represent a DDR controller.” There are many concurrent state machines — one progress FSM per entry, one per bank, a refresh manager, an arbiter pointer — and collapsing them into one serialises a device built for concurrency. Clue: a controller that never has two banks active.

17. Interview Reasoning

“What does a DDR command scheduler actually schedule?” Legal next-command candidates derived from outstanding transaction state — not requests. The follow-up that separates memorisation from understanding: where do those candidates come from, and how often are they computed? Every cycle, from request state and current bank state.

“Why not pre-expand every request into a command FIFO?” §3. A strong answer names both failure directions — the stale PRE and the missed row hit — and observes that the expansion is cheap while the staleness is not.

“What is the commit point, and what happens if state advances on grant?” The single event grant && phy_ready && gate. Advancing on grant produces ghost activates, phantom deadlines and lost refresh credits — and the symptom always appears downstream of the cause.

“Why is legality evaluated before arbitration rather than after?” So the arbiter's input does not depend on the arbiter's own history, so an empty candidate set is a reportable fact about the machine, and so a state refusal and a timing refusal stay distinguishable (16.2 §3).

“Your controller is correct in simulation and fails on hardware under load. Where do you look first?” At everything that is unexercised when backpressure is absent — commit-versus-grant, simultaneous allocate and free, refresh arriving during a stall. Then check whether the corresponding covers were ever hit.

“Which state belongs per bank and which per request?” Open row and timing history are per bank, because they describe the device. Progress, target and identity are per request, because they describe the transaction. Duplicating either into the other is the ownership error Chapter 17.2 §4 catalogues.

18. Exercises

1. For §1's five requests, write the candidate set if bank 1 were instead open on row 55. Which request changes class, and which command disappears from the set?

2. Construct a cycle sequence in which entry 3 is granted on four consecutive cycles and commits on none. Give a different legitimate cause for each cycle.

3. advance_onehot is built by comparison rather than by 1 << grant_entry. Give a value of NUM_ENTRIES and grant_entry where the shift silently corrupts and the comparison fails safe.

4. The commit condition exempts REF from normal_issue_allowed. Remove the exemption and trace what happens once the manager asserts its gate. At which cycle does the controller become permanently stuck, and why does the symptom look like a refresh bug?

5. Write an SVA property asserting that bank state changes only on a commit naming that bank. Which module must it be bound into, and why can it not live in command_issue_commit?

6. Rewrite P5 as a single-cycle conjunction — (grant_dropped && phy_cmd_ready && normal_issue_allowed) |=> grant_valid. Prove from the commit condition that its antecedent can never be true, and name the verification result that would nonetheless report it as passing.

7. A colleague proposes latching a dropped grant so it re-issues automatically when the interface frees. Name the bug class this reintroduces, and the specific cycle in §10's trace where the latched grant would be wrong.

19. Where This Goes

The spine is in place, and four of its boxes are still stubs.

Chapter 17.2 builds the entry pool this chapter consumed as given — what an entry must retain, and why allocation order is not issue order. 17.3 builds the manager behind normal_issue_allowed, turning Module 15's obligation into scheduler control. 17.4 builds the policy that produces grant_valid, and shows a row-hit-first scheduler starving a bank. 17.5 closes the loop at the top, where backpressure that began at this commit stage finally reaches the upstream master.

One sentence survives all four: nothing changes until a command commits.

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.