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.
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 — — noSix 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.
| Class | How it enters the decision | Why |
|---|---|---|
| Column read | arbiter candidate | one entry wants it; others may want something else |
| Column write | arbiter candidate | same, plus a bus-turnaround cost §5 covers |
ACT | arbiter candidate | same, plus the cross-bank budget of 16.1 |
PRE | arbiter candidate | same, plus drain preference during 17.3's prepare |
REF | legality gate | its 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.
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.
| Dimension | Prefers | Buys | Costs |
|---|---|---|---|
| Row hit | column commands to an open row | avoids PRE plus ACT plus their waits — the largest single saving available | starves conflicting requests to busy banks (§8) |
| Age | the oldest candidate | bounds worst-case latency | discards locality; an old conflict may cost far more than a young hit |
| Bank spread | candidates in less-recently-used banks | keeps 16.1's concurrency budget in use | may pass over a cheap row hit |
| Direction | staying with the current read or write phase | avoids bus turnaround, which is real and expensive | delays the opposite direction, sometimes badly |
| Urgency | candidates past a starvation threshold | makes fairness bounded rather than hoped-for | pure 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
// ─────────────────────────────────────────────────────────────────────
// 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
endmodule7. 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.
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 341Entry 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:
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 0One 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
// ── 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.
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
| Situation | Correct behaviour | Failure if mishandled |
|---|---|---|
legal_mask empty | no grant, pointer unchanged | fabricated grant from nothing |
| one legal candidate | granted regardless of preference | preference layer empties the set |
| all candidates urgent | layer 1 passes all; rotation decides | urgency becomes meaningless |
| all candidates row hits | layer 2 passes all; rotation decides | the common case, must not be special |
| urgent and row hit are disjoint | urgency wins — layer order | preference beats fairness |
| grant without commit | pointer holds | §7's spinning pointer |
NUM_ENTRIES = 1 | EN_W guarded; always grants the one entry | zero-width pointer |
AGE_THRESHOLD unreachable in AGE_W | elaboration failure | silent loss of the fairness layer |
| age saturates at maximum | still above threshold; still urgent | a wrapped age makes the oldest look youngest |
| reset mid-stream | pointer to 0 | pointer 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
Related tutorials
- Related topic
Scheduling Optimisation
Chapter 17.1 said legality admits and policy chooses, and deferred the quantification here. FR-FCFS saves 87% of row work and displaces a request without bound — and a bounded cap recovers most of the gain.
- Related topic
Controller-Scheduling Question
Whiteboard a scheduler. The failure is collapsing request, candidate, grant and committed command into one thing — and the interviewer's next three additions expose it. Includes a fairness fix that issues illegal commands.
- Related topic
Congestion Handling
What each layer should actually do once flow-control pressure exists — the four conditions that look alike, per-layer congestion responses, a hysteretic policy FSM with derived watermarks, congestion age and escalation, strict-priority starvation versus rotating-priority fairness, drain mode, admission throttling before ownership transfer, retry-induced congestion, congestion collapse, and bufferbloat.
- Related topic
Bulk Transfers
Promised nothing and permitted everything: why throughput and guarantee are different axes, and why a fixed-priority arbiter starves an endpoint forever while every safety property passes.
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.
