Skip to content
VLSI Mentor

DDR · Module 17

Arbitration

Arbitration runs after legality has already decided. It narrows subsets rather than comparing priority numbers, advances its pointer only on commit, and needs a bounded bypass because row-hit-first genuinely starves.

Three chapters have narrowed the problem. 17.1 built the pipeline and the commit point, 17.2 the entries, 17.3 the gate. Each deferred the same question, and this chapter answers it:

Several commands are legal this cycle. One command bus. Which one?

The answer has to be a mechanism rather than a preference, because the interesting part is not which policy you pick — it is that every policy that merely expresses a preference can starve someone, and the correction has to be built in deliberately.

1. What Is Actually Being Chosen

Be precise about the input, because vagueness here produces the wrong architecture.

The arbiter does not choose among requests. It chooses among legal command candidates, and the distinction has teeth: one entry contributes at most one candidate, several entries may contribute none, and the candidate an entry contributes may be a different command class than it contributed last cycle.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ENTRY   PROGRESS     CANDIDATE     LEGAL?   IN ARBITER'S INPUT?
    0     NEEDS_COL    RD bank 2      yes            YES
    1     NEEDS_ACT    ACT bank 5     yes            YES
    2     NEEDS_PRE    PRE bank 1     no  (tRAS)     no
    3     DATA         —              —              no
    4     NEEDS_COL    WR bank 7      yes            YES
    5     WAITING      —              —              no

Six entries, three candidates, and they are three different command classes. The arbiter picks one of the three. It cannot pick entry 2's PRE however much it might want to, and it does not see entries 3 or 5 at all.

2. Five Classes, One Bus, Two Mechanisms

The canonical description lists five things being mixed: read, write, refresh, precharge, activate. All five genuinely contend for one command bus. They do not all contend the same way, and collapsing them costs correctness.

ClassHow it enters the decisionWhy
Column readarbiter candidateone entry wants it; others may want something else
Column writearbiter candidatesame, plus a bus-turnaround cost §5 covers
ACTarbiter candidatesame, plus the cross-bank budget of 16.1
PREarbiter candidatesame, plus drain preference during 17.3's prepare
REFlegality gateits precondition is device-wide and its window forbids everything else

Why refresh is different, stated once more because it is the chapter's sharpest boundary. A REF cannot issue until all banks are idle (15.2, verified), and while it runs, normal commands are illegal rather than merely less preferred. Neither of those is a preference an arbiter can express. So by the time this block runs, 17.3 has already either allowed normal traffic or emptied the legal mask entirely — and the arbiter chooses among normal commands or does not run.

What is genuinely arbitrated is therefore four classes, and the description's word “fairly” is doing the real work: §8 shows that a reasonable-looking policy over those four starves one of them indefinitely.

3. Legality Is Already Decided

The arbiter must not re-check legality. This sounds like a mere layering nicety and is not.

If it re-checks, it has two sources of truth. The legality filter says a candidate is legal; the arbiter's copy says otherwise; the candidate silently never issues and the legal mask claims it should have. The bug is invisible from either side alone.

If it re-checks, it cannot report why nothing issued. Chapter 16.2 §3 made this argument at vector scale and it applies unchanged: an empty result from a combined legality-and-policy block is just a loop that found nothing, while an empty mask arriving at a pure policy block is a reportable fact with an attributable cause.

And if it re-checks, its input depends on its own history. A block that filters and then chooses, where the filter reads state the previous choice modified, is choosing from a set shaped by its own past behaviour. Reasoning about fairness becomes intractable — which matters, because §9 needs to prove a fairness property.

4. Layers, Not Numbers

The tempting implementation is a priority number per candidate and a maximum. It is compact, and it fails in a specific way: a single number cannot express “prefer this, but only among those that are already preferred for another reason”, and it silently trades off dimensions that should not be traded.

The alternative is successive narrowing. Each layer takes a set and returns a non-empty subset, or the set unchanged.

Arbitration as four successive narrowing layers over a bit vector of entries. The legal mask arrives from the legality filter of chapter sixteen point two, already gated by the refresh manager of chapter seventeen point three. Layer one selects the urgent subset, meaning entries whose age has crossed the starvation threshold; if that subset is empty the mask passes through unchanged. Layer two selects the preferred subset, meaning row hits, again passing through unchanged if empty. Layer three applies deterministic rotation, choosing the first surviving entry at or after the rotating pointer. The result is a one-hot grant, which travels to the commit stage of chapter seventeen point one. Below, a feedback path shows the pointer advancing only when a command actually commits, never on a grant alone.legal_mask16.2 + 17.3 gateLayer 1 — urgentaged past thresholdLayer 2 — preferredrow hitsLayer 3 — rotatedeterministicgrant_onehotone, or noneCommit17.1Pointeradvances on COMMITEmpty subsetpass through unchanged12

The pass-through-when-empty rule is what makes layering safe. A layer that could return an empty set would let a preference veto a legal candidate — turning policy into legality, which §3 forbids. Written as subset != 0 ? subset : incoming, each layer can only ever narrow among things that were already going to be acceptable.

The order of the layers is the policy. Urgency before preference means a starved entry beats a row hit; swapping them means row hits beat starvation, and §8 shows what that costs. The mechanism is fixed; the ordering is the design decision, and it is visible in one line rather than buried in a comparator.

5. The Dimensions

What layers might exist, and what each is actually trading. No ranking is offered, because none is defensible in general.

DimensionPrefersBuysCosts
Row hitcolumn commands to an open rowavoids PRE plus ACT plus their waits — the largest single saving availablestarves conflicting requests to busy banks (§8)
Agethe oldest candidatebounds worst-case latencydiscards locality; an old conflict may cost far more than a young hit
Bank spreadcandidates in less-recently-used bankskeeps 16.1's concurrency budget in usemay pass over a cheap row hit
Directionstaying with the current read or write phaseavoids bus turnaround, which is real and expensivedelays the opposite direction, sometimes badly
Urgencycandidates past a starvation thresholdmakes fairness bounded rather than hoped-forpure overhead when nothing is starving

Direction deserves a caution. Read-to-write and write-to-read turnaround on the shared data bus is a genuine cost, and batching same-direction commands is a real technique. It is also the dimension most likely to be quantified badly: the cost depends on generation, on rank configuration and on the specific transition, and it is Module 23's to measure. This chapter notes that the dimension exists and that a policy which ignores it will turn the bus around more than it needs to.

6. The Arbiter Block

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
// command_arbiter_layers
//
// CLASSIFICATION
//   Synthesizable educational RTL. One responsibility: choosing ONE
//   entry from a legal-candidate mask by successive narrowing, and
//   maintaining the rotation state that makes the choice fair.
//
// WHAT IT DOES NOT MODEL
//   - No legality whatsoever. legal_mask is an INPUT, already gated by
//     Chapter 17.3's refresh manager. §3: re-deciding it here is the
//     dangerous error, not merely the redundant one.
//   - No refresh candidate. REF is a gate, not an entry (§2).
//   - No entry storage or progress. Chapter 17.2 owns those.
//   - No commit. It produces INTENT; Chapter 17.1 decides whether that
//     intent becomes fact.
//   - No performance claim. The layer ORDER below is one defensible
//     policy, not a recommended or optimal one (Module 23).
// ─────────────────────────────────────────────────────────────────────
module command_arbiter_layers #(
  parameter int NUM_ENTRIES   = 8,
  parameter int AGE_W         = 8,
  // Starvation bound, in cycles waiting. A POLICY value: §9 derives
  // what it must exceed, and nothing about the device sets it.
  parameter int AGE_THRESHOLD = 64,
  parameter int EN_W = (NUM_ENTRIES <= 1) ? 1 : $clog2(NUM_ENTRIES)
) (
  input  logic                              clk,
  input  logic                              rst_n,

  // ── The legal-candidate set. One bit per entry. Chapter 16.2's
  //    bank_candidate_mask and Chapter 17.3's gate produce it.
  input  logic [NUM_ENTRIES-1:0]            legal_mask,

  // ── Policy inputs. row_hit_mask is a PREFERENCE, not a permission:
  //    an entry may be a row hit and illegal, or legal and not a hit.
  input  logic [NUM_ENTRIES-1:0]            row_hit_mask,
  input  logic [NUM_ENTRIES-1:0][AGE_W-1:0] entry_age,

  // ── From Chapter 17.1's commit point. The pointer advances on THIS
  //    and on nothing else. §7 is the whole argument.
  input  logic                              commit,

  // ── The grant. Intent, not action.
  output logic [NUM_ENTRIES-1:0]            grant_onehot,
  output logic                              grant_valid,
  output logic [EN_W-1:0]                   grant_entry,

  // ── Observability: the urgency layer changed the outcome. A healthy
  //    controller asserts this rarely; continuous assertion means the
  //    threshold is too low or the policy above it is too aggressive.
  output logic                              urgent_active,

  // ── Design error: a grant outside the legal mask. Must never assert.
  output logic                              err_grant_not_legal
);

  if (NUM_ENTRIES < 1)
    $fatal(1, "command_arbiter_layers: NUM_ENTRIES must be >= 1");
  // The threshold must be representable, or the comparison below can
  // never be true and the fairness mechanism is silently absent —
  // a failure that looks exactly like having no mechanism at all.
  if (AGE_THRESHOLD >= (1 << AGE_W))
    $fatal(1, "command_arbiter_layers: AGE_THRESHOLD unreachable in AGE_W bits");

  logic [EN_W-1:0]         ptr;
  logic [NUM_ENTRIES-1:0]  urgent_mask, pref_mask, candidates;

  // ── LAYER 1. Entries that have waited past the bound. Note this is
  //    ANDed with legal_mask: an aged entry that is not legal is not a
  //    candidate, because urgency is a preference and preferences may
  //    never create legality (§3).
  always_comb begin
    urgent_mask = '0;
    for (int i = 0; i < NUM_ENTRIES; i++)
      urgent_mask[i] = legal_mask[i]
                     && (entry_age[i] >= AGE_W'(AGE_THRESHOLD));
  end

  // ── LAYER 2. Row hits, among whatever layer 1 left.
  //    The pass-through-when-empty rule at every layer: a narrowing
  //    that would empty the set returns the set unchanged.
  logic [NUM_ENTRIES-1:0] after_l1;
  assign after_l1 = (urgent_mask != '0) ? urgent_mask : legal_mask;
  assign pref_mask = after_l1 & row_hit_mask;
  assign candidates = (pref_mask != '0) ? pref_mask : after_l1;

  assign urgent_active = (urgent_mask != '0);

  // ── LAYER 3. Deterministic rotation: the first surviving entry at or
  //    after ptr, wrapping. Determinism matters as much as fairness —
  //    a tie broken differently on two runs makes waveform comparison
  //    impossible, and most scheduler debugging is waveform comparison.
  always_comb begin
    grant_onehot = '0;
    grant_valid  = 1'b0;
    grant_entry  = '0;
    for (int k = 0; k < NUM_ENTRIES; k++) begin
      automatic int idx = (int'(ptr) + k) % NUM_ENTRIES;
      if (!grant_valid && candidates[idx]) begin
        grant_valid       = 1'b1;
        grant_onehot[idx] = 1'b1;
        grant_entry       = EN_W'(idx);
      end
    end
  end

  assign err_grant_not_legal = grant_valid && !legal_mask[grant_entry];

  // ── The pointer. §7: it moves when a command COMMITTED, not when one
  //    was granted. A grant that does not commit has consumed no turn,
  //    because no service was delivered.
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)
      ptr <= '0;
    else if (commit && grant_valid)
      ptr <= (int'(grant_entry) == NUM_ENTRIES - 1)
               ? '0 : (grant_entry + EN_W'(1));
  end

endmodule

7. The Pointer Moves on Commit

One line of that block is the one most often written wrong, and 17.1 §8's ownership table flagged it in advance: else if (commit && grant_valid).

Writing else if (grant_valid) is the natural thing to do. The arbiter granted; the turn is used; move along. It even looks more fair — every grant advances the rotation.

It is wrong, and the failure is load-dependent in the nastiest way.

The same argument applies to any state a policy keeps for fairness — a bypass counter, a direction-batch counter, a per-bank last-served timestamp. All of them record service, and service means commit.

8. Starvation, Concretely

It is easy to write “row-hit-first can starve requests” and move on. Here is it happening, because the details are what make the fix obvious.

Configuration. EDUCATIONAL TIMING — NOT JEDEC VALUES: the cycle numbers and ages below illustrate the mechanism and are not device figures. Bank 0 receives a continuous stream of requests to row 12, which is open. Entry 1 holds an older request to bank 1, row 55, where row 40 is currently open — a row conflict. AGE_THRESHOLD is disabled for this trace, so layer 1 never fires and the policy is pure row-hit-first.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cyc  legal_mask  row_hit_mask  candidates  grant  commit  entry1 age
  ───  ──────────  ────────────  ──────────  ─────  ──────  ──────────
  600  0b000011     0b000001      0b000001     e0     YES        41
  604  0b000011     0b000001      0b000001     e0     YES        45
  608  0b000011     0b000001      0b000001     e0     YES        49
  612  0b000011     0b000001      0b000001     e0     YES        53
  616  0b000011     0b000001      0b000001     e0     YES        57
   ...  (bank 0's row-hit stream continues indefinitely)
  900  0b000011     0b000001      0b000001     e0     YES       341

Entry 1 is legal on every single cycle. Its PRE is permitted — bank 1's tRAS elapsed long ago. It is in legal_mask throughout. It is simply never in candidates, because layer 2 finds a non-empty row-hit subset every cycle and narrows to it.

The rotation in layer 3 cannot help: rotation breaks ties among survivors, and entry 1 never survives to layer 3. A fairness mechanism placed below a preference layer is unreachable whenever the preference is satisfiable.

9. Bounding It

The fix has to make the starving entry reachable, which means a layer above the preference. That is what layer 1 is, and it is why the order in §4 is urgency first.

Re-run the trace with AGE_THRESHOLD = 64:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cyc  legal  row_hit  urgent   candidates  grant  urgent_active  entry 1
  ───  ─────  ───────  ──────   ──────────  ─────  ─────────────  ───────
  616  0b011  0b001    0b000     0b000001     e0        0     age 57
  620  0b011  0b001    0b000     0b000001     e0        0     age 61
  622  0b011  0b001    0b000     0b000001     e0        0     age 63
  623  0b011  0b001    0b010     0b000010     e1        1     age 64 ←
  624  0b011  0b001    0b000     0b000001     e0        0     age 0

One cycle. §8 left entry 1 at age 57 on cycle 616, and the counter advances once per cycle, so the threshold is crossed at cycle 623. Layer 1 then returns a non-empty subset, layer 2's row-hit narrowing is applied within that subset and finds nothing, so it passes through unchanged, and entry 1 is granted. Its age resets on allocation of the next request; urgent_active pulses.

Choosing the threshold. It must exceed the longest legitimate wait, or the mechanism fires constantly and the preference layer is effectively disabled. The longest legitimate wait is bounded below by the worst-case time for a request to become legal — which includes tRAS plus tRP plus tRCD in the conflict case, plus the refresh drain and occupancy of 17.3 §9, plus contention from the other NUM_ENTRIES - 1 entries. Chapter 17.2 §10's lifecycle trace measured fifteen cycles for one uncontended conflicting read; the threshold must sit well above that scaled by realistic contention.

And AGE_W must hold it. The elaboration guard in §6 enforces AGE_THRESHOLD < 2**AGE_W. Without it, a threshold of 64 with AGE_W = 6 makes the comparison unreachable, layer 1 never fires, and the controller silently reverts to §8's behaviour — a fairness mechanism that is present in the source and absent in the silicon.

10. Direction Is a Dimension With Its Own Starvation

The canonical description of this chapter puts read and write first among the things being mixed, and §5 listed direction as a dimension without developing it. It deserves its own treatment, because it is structurally different from every other layer and it introduces a second, independent starvation problem.

Why direction is different. Row-hit preference, age and bank spread are all properties of a candidate. Direction preference is a property of the relationship between a candidate and what was issued last. The same WR candidate is cheap after another write and expensive after a read, and nothing about the entry itself changes between those two cases.

That makes it the one dimension whose cost depends on the arbiter's own previous decision — which is precisely the self-referential shape §3 warned about in the legality context. Here it is unavoidable and legitimate, but it has to be handled explicitly rather than folded into a preference mask.

Why turnaround costs anything. The DQ bus is shared and bidirectional (Chapter 6.2). A read drives it from the device toward the controller; a write drives it the other way. Reversing requires the previous direction's data to finish and the bus to be handed over, and the device specifies minimum spacings for these transitions. Chapter 12.3 established that these turnaround gaps are dead cycles on the data bus — the interface is idle while nothing is wrong.

What that does to a scheduler. Alternating read, write, read, write pays a turnaround on every single command. Batching — serving several reads, then several writes — amortises one turnaround across many transfers. Hence the standard technique: prefer candidates matching the current direction, and switch only when a condition is met.

Where it goes in the stack. Direction preference belongs at layer 2's level — a preference, narrowing within whatever layer 1 left — and the phase counter that bounds it belongs with the pointer of §7, updated on commit, for exactly the reason given there. A batch counter advanced on grant counts commands that were never issued, and the batch ends early with the turnaround still unpaid.

What this chapter will not tell you. How many commands a batch should hold. That number depends on the turnaround cost in cycles for the specific generation and rank configuration, on the read-to-write ratio of the traffic, and on how much latency the writes can absorb — and it is Module 23's to quantify. What is general is the structure: a direction preference requires a phase bound, the bound is on the batch rather than the entry, and the counter advances on commit.

One honest consequence worth stating. Adding direction as a layer means the arbiter now has urgency, row hit and direction — three narrowing stages, each a mask operation on 17.1 §14's critical path. §14's frequency argument is not hypothetical; this is the chapter where a designer decides whether a third dimension is worth the cycle time it costs, and the answer is genuinely sometimes no.

11. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── P1. A grant is always within the legal mask. The most important
//    property here: it is the boundary between policy and correctness.
//    If this fails, the arbiter has started deciding legality (§3).
property p_grant_is_legal;
  @(posedge clk) disable iff (!rst_n)
    grant_valid |-> legal_mask[grant_entry];
endproperty
a_grant_is_legal: assert property (p_grant_is_legal);

// ── P2. One grant or none. A command bus carries one command.
property p_grant_onehot0;
  @(posedge clk) disable iff (!rst_n)
    $countones(grant_onehot) <= 1;
endproperty
a_grant_onehot0: assert property (p_grant_onehot0);

// ── P3. No grant exactly when nothing is legal. Both directions: a
//    grant from an empty mask is a fabricated candidate, and no grant
//    from a non-empty mask is a lost issue slot — a pure performance
//    bug that no protocol checker will ever see.
property p_grant_iff_legal_nonempty;
  @(posedge clk) disable iff (!rst_n)
    grant_valid == (legal_mask != '0);
endproperty
a_grant_iff_legal_nonempty: assert property (p_grant_iff_legal_nonempty);

// ── P4. The pointer moves only on a committed grant. §7's bug, caught
//    directly rather than through its downstream fairness symptom.
property p_pointer_only_on_commit;
  @(posedge clk) disable iff (!rst_n)
    (ptr != $past(ptr, 1)) |-> ($past(commit, 1) && $past(grant_valid, 1));
endproperty
a_pointer_only_on_commit: assert property (p_pointer_only_on_commit);

// ── P5. Bounded fairness, with its assumptions STATED because they are
//    what make it meaningful rather than vacuous. An entry that stays
//    legal and reaches the threshold must be granted on the next cycle:
//    at that point it is in urgent_mask, and layer 2 can only narrow
//    WITHIN that subset. Assumes the entry remains legal — if it stops
//    being legal, nothing is claimed, correctly.
property p_aged_entry_granted;
  @(posedge clk) disable iff (!rst_n)
    (legal_mask[0] && entry_age[0] >= AGE_W'(AGE_THRESHOLD)
     && $countones(urgent_mask) == 1)
      |-> grant_onehot[0];
endproperty
a_aged_entry_granted: assert property (p_aged_entry_granted);

// ── Covers. Without these the fairness layer may never have run.
c_urgent_fires:   cover property (@(posedge clk) disable iff (!rst_n)
                    urgent_active);
c_pref_overridden: cover property (@(posedge clk) disable iff (!rst_n)
                    urgent_active && (row_hit_mask & grant_onehot) == '0
                    && row_hit_mask != '0);
c_grant_no_commit: cover property (@(posedge clk) disable iff (!rst_n)
                    grant_valid && !commit);

What they do not prove. Nothing here says the policy is good — P1 through P5 would all pass on a policy that always grants the lowest legal index, which starves badly. Nothing proves the issued command is legal at the device: the arbiter trusts legal_mask, and if that input is wrong, P1 passes while the device receives an illegal command. That is 13.4's issue_timing_checker's job on the issued stream, and it is a genuinely independent check. And P5 covers the single-urgent-entry case only; with several urgent entries, rotation decides among them and the bound becomes NUM_ENTRIES times longer.

c_pref_overridden is the cover that matters most. If it never hits, the urgency layer has never actually overridden a row hit, and §8's scenario is unverified no matter how many cycles ran.

12. The Independent Model

Model the policy as a pure function in the testbench: given legal_mask, row_hit_mask, ages and the pointer, compute the expected grant with an independent implementation — a sorted list rather than mask arithmetic, so a mask bug cannot be mirrored.

Then, separately and more importantly, maintain a service-interval histogram per entry: how many cycles each waited while legal but not granted. Fairness is a statistical property, and an assertion cannot see it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ARBITRATION FAIRNESS REPORT          20,000 cycles
    entry   grants   legal-not-granted   max wait   p99 wait
      0     4,812         1,204              9          6
      1        61        13,445          3,118      2,904   ←
      2     4,790         1,266             11          7
      3     4,704         1,332             12          8
    urgent_active cycles : 61        (0.3%)
    c_pref_overridden    : 61 hits
    diagnosis : entry 1 is served ONLY by the urgency layer. Its
                natural selection rate is zero — every grant it
                received was a forced bypass.
    reading   : the mechanism is working and the policy is not.
                A bound of 3,118 cycles is being HIT, not approached.

The final line is the useful one. No assertion failed; the fairness bound is doing exactly what it was designed to do. And a max wait equal to the threshold, hit repeatedly, means the threshold is not a safety net — it has become the scheduling policy for that entry.

13. Corner Cases

SituationCorrect behaviourFailure if mishandled
legal_mask emptyno grant, pointer unchangedfabricated grant from nothing
one legal candidategranted regardless of preferencepreference layer empties the set
all candidates urgentlayer 1 passes all; rotation decidesurgency becomes meaningless
all candidates row hitslayer 2 passes all; rotation decidesthe common case, must not be special
urgent and row hit are disjointurgency wins — layer orderpreference beats fairness
grant without commitpointer holds§7's spinning pointer
NUM_ENTRIES = 1EN_W guarded; always grants the one entryzero-width pointer
AGE_THRESHOLD unreachable in AGE_Welaboration failuresilent loss of the fairness layer
age saturates at maximumstill above threshold; still urgenta wrapped age makes the oldest look youngest
reset mid-streampointer to 0pointer retains a stale position

The age-saturation row connects back to 17.2 §9's saturating counter. A wrapping age counter and this arbiter together produce the worst available outcome: the most-starved entry wraps to age zero, leaves urgent_mask, and becomes the least likely to be served.

14. Synthesis, Cost and Limits

Cost. Two mask ANDs, two zero-comparisons, NUM_ENTRIES age comparators, and a rotating priority encoder. The age comparators dominate: NUM_ENTRIES × AGE_W bits of comparison, all in parallel.

Timing. The rotate loop is the critical structure — a priority encoder over NUM_ENTRIES with a variable starting point, which synthesises to roughly twice the logic of a fixed-priority encoder of the same width. It sits in the middle of 17.1 §14's long combinational chain, after legality and before commit, so this is where arbiter complexity turns directly into frequency loss. Each additional layer adds a mask stage to that path, which is the real reason production arbiters have fewer policy dimensions than the literature suggests they might.

A pipelining note. Registering the grant to break the path means arbitrating on last cycle's legal mask — and the winner must then be re-validated against current legality at the commit stage, which is 17.1 §14's second argument for a commit point.

What a production arbiter has that this does not: read/write direction batching with hysteresis; per-bank-group awareness so that 16.4's same-group penalty influences selection; rank-aware turnaround; quality-of-service classes carried from the upstream protocol; write-data availability as a gating term; and separate arbiters for the command and data paths. Each is another layer or another gating term, and each costs frequency.

15. Debugging

Symptom: one request is never served, everything else is fine. §8. Check whether the entry is in legal_mask — if it is not, this is a legality problem and belongs upstream (16.2 publishes its three filters separately for this reason). If it is legal and never granted, it is being narrowed out by a preference layer, and the question is whether an override layer exists above that preference and whether its threshold is reachable.

Symptom: fairness works in simulation and not in hardware. §7. Check the pointer's update condition for commit. This is the signature bug: correct at low load, degrading smoothly as backpressure rises.

Symptom: the controller issues nothing although candidates exist. Check grant_valid against legal_mask != 0 — P3 asserts they are equal. If the mask is non-empty and no grant appears, a layer has returned an empty subset, which means a pass-through-when-empty rule is missing. Reading the layer outputs in order localises it in one step: the last non-empty mask names the offending layer.

Symptom: urgent_active is high almost continuously. The threshold is below the legitimate wait time, so the fairness layer has become the primary policy and the preference layer is effectively disabled. §9's sizing argument applies; check it against 17.2 §10's lifecycle timing scaled by contention.

Symptom: an illegal command reaches the device while P1 passes. legal_mask is wrong, not the arbiter. Reach for 13.4's issue_timing_checker on the issued stream — it is independent of the mask and will name the violated rule and the earliest legal cycle.

16. Misconceptions

“Arbitration decides legality.” §3 — and this is the dangerous direction, not merely the redundant one. Clue: illegal commands issued with sensible-looking grants.

“A row hit should always win.” §8 — unbounded starvation of the most expensive request in the set. Clue: excellent average latency, catastrophic tail latency.

“The oldest request should always win.” Pure age discards locality entirely; every conflict pays PRE plus ACT plus their waits. Clue: a low row-hit rate under traffic that has obvious locality.

“Round-robin is fair, so it fixes starvation.” Rotation below a preference layer is unreachable whenever the preference is satisfiable — §8's layer 3 never ran. Clue: a rotating arbiter that still starves.

“Refresh should have the highest priority.” 17.3 §2 — a competitor can lose. It is a gate. Clue: normal commands occasionally issued inside a refresh window.

“The pointer advances on grant.” §7. Clue: fairness that degrades under backpressure.

“More policy layers make a better scheduler.” Each layer lengthens the critical path (§14), and frequency is bandwidth. Clue: a sophisticated scheduler that had to drop a speed grade.

“A fairness bound means requests are served fairly.” §12 — an entry served only by its bound is not being scheduled, it is being rescued. Clue: max wait equal to the threshold, hit repeatedly.

17. Interview Reasoning

“Several commands are legal. How do you choose?” Describe narrowing rather than a priority number, and say why a layer must pass through when its subset is empty — a preference that can empty the set has become legality.

“How can row-hit-first starve a request?” Give §8's structure: a continuous hit stream in one bank, a legal conflict in another, and the observation that rotation below the preference never runs. Saying it can starve is weak; showing the mechanism is strong.

“Where does the fairness override go, and why there?” Above the preference. Below it, it is unreachable exactly when it is needed.

“What does your rotation pointer advance on?” Commit. Then explain why grant-advance passes light tests and fails under backpressure.

“How do you pick a starvation threshold?” It must exceed the longest legitimate wait — conflict timing, refresh drain and occupancy, and contention from the other entries — and it must be representable in the age field's width.

“Round-robin or fixed priority?” Neither in isolation. The useful answer names what each does to the layer structure and refuses to declare a universal winner, because one does not exist.

“Your arbiter passes every assertion and the controller performs badly. Now what?” Assertions check legality and structure; fairness is statistical. Build §12's per-entry service histogram — it is the only thing that shows an entry being served exclusively by its bypass.

18. Exercises

1. Swap layers 1 and 2 in §6. Re-run §8's trace and give the exact cycle at which entry 1 is first granted, or prove it is never granted.

2. AGE_W = 6, AGE_THRESHOLD = 64. What does the elaboration guard do, and what would happen without it? Name the symptom in §12's report.

3. Construct a legal mask and age vector where P5's antecedent holds but three entries are urgent. What bound applies now, and by what factor is it worse?

4. Add a direction-batching layer that prefers the current read or write phase. Where in the stack does it go, and what starvation does it introduce that no existing layer bounds?

5. c_pref_overridden is unhit after a long regression while c_urgent_fires hits often. What does that combination tell you, and which of §13's corner cases is it?

6. Rewrite the pointer update as else if (grant_valid). Give a 12-cycle stimulus with phy_cmd_ready low on nine of them, and show the pointer's final position under both versions.

7. An entry's age saturates at 2**AGE_W - 1 rather than wrapping. Write the failure the wrapping version produces, and say which of §13's rows it corresponds to.

8. Design a bypass-counting override instead of an age threshold. State the one thing it bounds better than age does, and the one thing it bounds worse.

19. Where This Goes

The scheduler is complete: entries, derivation, legality, a gate, a policy, and a commit. Everything from the request pool to the command bus has an owner.

What is still missing is the two ends. Chapter 17.5 builds them — how an upstream request is accepted, what the ready/valid contract actually guarantees, when a request is genuinely complete, and how the backpressure that begins at a stalled commit or a refresh drain propagates back through this arbiter, through 17.2's pool, and finally out to the master that issued the request in the first place.

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.