DDR · Module 9
Row Hits
A row hit is not “the row number matches”. It needs the same bank, the same row, and a bank that is actually usable — and getting any of the three wrong gives a hit rate that looks excellent and means nothing.
Chapter 9.1 established the four-state bank model and Chapter 9.2 established what the open row actually is. Both were about state on its own.
This chapter is the first of three that ask what a request does when it meets that state, and it takes the best case:
What makes a request to the same bank and the same row special — and what exactly has to be true for it to count?
The second half is where the work is. "Same row" is the obvious definition and it is wrong in two independent ways, each of which produces a classifier that reports an excellent hit rate while measuring nothing.
1. What a Hit Actually Requires
A request is a row hit when the bank it names is in BANK_OPEN and the row it holds is the row requested.
Three conditions, and the usual definition omits two of them:
row hit == same bank
AND requested row == held row
AND bank state is BANK_OPENCondition one — same bank. Row identifiers are per bank, so "the requested row" only means anything once a bank is fixed. §3 develops this.
Condition two — the rows match. The obvious part.
Condition three — the bank is BANK_OPEN, not merely associated with that row. Chapter 9.1 §5 built a model in which a bank in BANK_OPENING already has the target row in row_q. A naive comparison against that register reports a hit for a bank that is not yet usable — which is Chapter 3.5 §6's selected is not usable returning as a classification bug rather than a state bug.
2. The Taxonomy, Stated Once
The module needs one vocabulary, used identically in all six chapters. Here it is, derived directly from Chapter 9.1's four states.
| Bank state | Requested row | Class | Response required | Owned by |
|---|---|---|---|---|
BANK_OPEN | equals held row | HIT | none — column access is meaningful | this chapter |
BANK_CLOSED | any | MISS | one activate | 9.4 |
BANK_OPEN | differs from held row | CONFLICT | a precharge, then an activate | 9.5 |
BANK_OPENING / BANK_CLOSING | any | BUSY | wait for the transition in flight | 9.5 |
3. Row Numbers Repeat Per Bank
This is short and it is the most commonly skipped condition.
Chapter 5.2 established that each bank owns its own row decoder and its own sense amplifiers. So each bank numbers its rows from zero independently. Row 100 exists in every bank, and those are unrelated locations holding unrelated data.
Therefore:
bank 0 holds row 100
request: bank 1, row 100
→ NOT a hit. Bank 1's state is whatever it is,
and bank 0's held row is irrelevant to it.Chapter 8.1 §1 made the same point from the addressing side: a row operand is half a coordinate, and row 0x1234 is not a location until a bank is named.
The failure this produces is distinctive. A classifier that compares the requested row against any open row, or against a single global "last opened row," reports hits for unrelated banks. With several banks open on similar rows — which sequential traffic under Chapter 8.3's interleaved policy produces constantly — the hit rate climbs toward nonsense. §9 is about recognising that from the number alone.
4. A Hit Is Not Free
A row hit means one thing only: the row-state transition that would otherwise be required is not required. That is genuinely valuable and it is not the same as fast.
What a hit does not mean:
It does not mean zero latency. A column access still has to be issued, the array's column path still has to deliver, and Chapter 6.5 already established that the interval from a column command to data is a real pipeline depth. A hit removes an activate; it does not remove the access.
It does not mean the command may be issued now. State legality is necessary and not sufficient — Chapter 7.3 §4's distinction. A hit says the state permits a column access; whether timing permits it is a Module 13/14 question, and this chapter's classifier deliberately cannot answer it.
It does not mean unlimited bandwidth. Chapter 5.3 established that banks in a group share a column data path, so consecutive hits to the same group contend for a resource the hit classification knows nothing about.
And it does not mean the access is cheap in every sense. A workload achieving a high hit rate by serialising everything into one bank has excellent locality and no bank parallelism — which is Chapter 8.3 §3's trade-off, and Chapter 9.6 §8 shows a hit rate rising while throughput falls.
5. RTL — The Request Classifier
The engineering problem
Given a request naming a bank and a row, and the per-bank state from Chapter 9.1's model, produce exactly one classification — and make the three conditions of §1 structural rather than a matter of getting a comparison right.
Why hardware needs it
Every request has to be classified before anything can be decided about it. The classification is what a scheduler consumes, what telemetry counts, and what a checker gates on, so it sits on the request path and must be cheap.
Classification
SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL. Level B and C only.
What it models
Classification of one request against per-bank row context into hit, miss, conflict, busy, or an invalid bank index.
What it does NOT model
State (Chapter 9.1). Timing legality (Modules 13, 14) — a hit says nothing about whether a command may be issued now. The action required (Chapter 9.4 and 9.5 own that). Scheduling (Module 17). Column access and data (Modules 10 to 12). Shared column-path contention (Chapter 5.3). Rank (Chapter 8.5) — one rank's banks at a time, and a multi-rank controller instantiates it per rank.
Interface and parameter contract
// ─────────────────────────────────────────────────────────────────────────
// row_request_classifier
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
//
// MODELS: classification of ONE request against per-bank row context.
//
// HOLDS NO STATE. Bank state arrives as an input from Chapter 9.1's
// bank_row_fsm. A classifier with its own copy would be a second model
// that can disagree with the first -- the exact hazard Chapter 9.2 exists
// to detect.
//
// MODELS NO PHYSICAL OR ANALOG BEHAVIOUR. No signal represents a charge,
// a bitline voltage, a sense amplifier or a transistor.
//
// NOT A SCHEDULER. It issues nothing and orders nothing (Module 17).
// NOT A TIMING CHECK. A hit says the STATE permits a column access, never
// that one may be issued now (Modules 13, 14).
//
// RELATION TO EXISTING BLOCKS: Chapter 5.2's ddr_bank_state_table exposes
// a two-state row_hit for the bank a command names. This produces FOUR
// classes against FOUR states, and REQ_BUSY cannot be expressed in a
// two-state model at all.
//
// GENERATION: GENERATION-NEUTRAL ARCHITECTURAL MODEL.
// ─────────────────────────────────────────────────────────────────────────
// The module's taxonomy as a type. Chapter 9.3 Section 2.
typedef enum logic [2:0] {
REQ_HIT = 3'd0, // BANK_OPEN and the held row is the requested row
REQ_MISS = 3'd1, // BANK_CLOSED -- no row held (Chapter 9.4)
REQ_CONFLICT = 3'd2, // BANK_OPEN holding a DIFFERENT row (Chapter 9.5)
REQ_BUSY = 3'd3, // a transition is in flight (Chapter 9.5)
REQ_BAD_BANK = 3'd4 // the index names no bank
} req_class_e;
module row_request_classifier #(
parameter int NUM_BANKS = 4,
parameter int ROW_W = 16,
parameter int BA_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS)
) (
input logic req_valid,
input logic [BA_W-1:0] req_bank,
input logic [ROW_W-1:0] req_row,
// ── Row context from Chapter 9.1's FSM. Read, never written.
input bank_row_state_e state_in [NUM_BANKS],
input logic [ROW_W-1:0] row_in [NUM_BANKS],
output req_class_e req_class,
output logic class_valid,
// Broken out so a consumer can use one without decoding the enum, and
// so Section 7's mutual-exclusion property has something to check.
output logic hit,
output logic miss,
output logic conflict,
output logic busy,
output logic bad_bank
);
if (NUM_BANKS < 1) begin : g_nb
initial $fatal(1, "row_request_classifier: NUM_BANKS must be >= 1");
end
if (ROW_W < 1) begin : g_rw
initial $fatal(1, "row_request_classifier: ROW_W must be >= 1");
end
// ── Index legality, in the shape Chapter 9.1 Section 5 explains: a cast
// to BA_W truncates for a power-of-two NUM_BANKS and makes the
// comparison permanently false, so the arm is removed instead.
logic idx_bad;
if (NUM_BANKS >= (1 << BA_W)) begin : g_idx_full
assign idx_bad = 1'b0;
end else begin : g_idx_partial
assign idx_bad = ({1'b0, req_bank} >= (BA_W+1)'(NUM_BANKS));
end
// ── The addressed bank's context. Defaulted before the guarded read so
// an out-of-range index never indexes the arrays.
bank_row_state_e sel_state;
logic [ROW_W-1:0] sel_row;
always_comb begin
sel_state = BANK_CLOSED;
sel_row = '0;
if (!idx_bad) begin
sel_state = state_in[req_bank];
sel_row = row_in[req_bank];
end
end
// ── Classification. A single priority chain, so exactly one class is
// produced for a valid request by construction rather than by
// coincidence of mutually exclusive terms.
//
// NOTE THE ORDER. The state is tested BEFORE the row comparison,
// which is what makes Section 1's third condition structural: a bank
// in BANK_OPENING already carries its target row in row_in, so a
// classifier that compared rows first would report a hit for a bank
// that is not yet usable.
always_comb begin
req_class = REQ_MISS;
if (idx_bad) begin
req_class = REQ_BAD_BANK;
end else begin
unique case (sel_state)
BANK_CLOSED: req_class = REQ_MISS;
BANK_OPENING, BANK_CLOSING: req_class = REQ_BUSY;
BANK_OPEN: req_class = (sel_row == req_row)
? REQ_HIT : REQ_CONFLICT;
default: req_class = REQ_BAD_BANK;
endcase
end
end
assign class_valid = req_valid;
// ── Views. Every one is gated on req_valid: a classification of nothing
// is not a class, and a telemetry block counting these (Chapter 9.6)
// would otherwise count idle cycles as misses.
assign hit = req_valid && (req_class == REQ_HIT);
assign miss = req_valid && (req_class == REQ_MISS);
assign conflict = req_valid && (req_class == REQ_CONFLICT);
assign busy = req_valid && (req_class == REQ_BUSY);
assign bad_bank = req_valid && (req_class == REQ_BAD_BANK);
endmoduleState representation
None. The block is a function of the request and the supplied context.
Combinational behaviour
An index check, a guarded array read, a five-way priority chain, and five gated views. The unique case carries a default arm, which is not redundant: sel_state is a four-value enum so the case is complete over legal values, and the default catches a state variable holding an out-of-range or unknown encoding rather than letting the chain fall through to its initial REQ_MISS. A miss is the worst possible default for a corrupted state, because it is a plausible answer that a consumer will act on.
Sequential behaviour and reset
Neither. Classification must not be registered, and the reason is a real design argument rather than a preference: a registered class is a class of the state as it was, and bank state can change in the interim. A consumer acting on a stale hit would issue a column access against a bank that has since been precharged.
Cycle-by-cycle example
NUM_BANKS = 4. Context: bank 1 OPEN(0x0104), bank 2 CLOSED, bank 3 OPENING with target 0x0200.
| Request | sel_state | Class | Why |
|---|---|---|---|
b1, 0x0104 | OPEN | HIT | all three conditions hold |
b1, 0x0105 | OPEN | CONFLICT | open, wrong row |
b2, 0x0300 | CLOSED | MISS | nothing held |
b3, 0x0200 | OPENING | BUSY | row matches and it is still not a hit |
b0, 0x0000 | CLOSED | MISS | never opened |
Row 4 is the one to study. The request names the row bank 3 is opening, and the classification is BUSY. row_in[3] already equals req_row — so a classifier that compared rows before testing the state would call this a hit and permit a column access into the opening interval. §1's third condition exists for this row of this table.
How to simulate, and expected output
Drive the table above and check req_class plus the five views. Then:
req_valid low with any context must leave all five views low, whatever req_class shows. This matters for Chapter 9.6: telemetry gated on the views must not count idle cycles.
NUM_BANKS = 3 with req_bank = 3 must give REQ_BAD_BANK and, critically, hit low — an invalid index must never classify as the best case. This is the only configuration where the index arm can fire.
Bank in BANK_CLOSING holding the requested row must give BUSY, not HIT. The mirror of row 4.
Corrupt state_in[b] to an out-of-range encoding and confirm REQ_BAD_BANK rather than REQ_MISS.
Expected waveform
§6. The shape to look for is the same request classified differently at two cycles as the bank's state moves underneath it.
Synthesis implications
A NUM_BANKS-to-1 multiplexer on the state and another on the row, a ROW_W-bit comparator, and a small priority chain. The row comparator is the widest element, and at a 17-bit row it is trivial. This is genuinely cheap logic sitting on a path that decides everything downstream, which is why real controllers compute it a cycle ahead of the issue decision.
Corner cases
NUM_BANKS == 1 gives BA_W == 1 through the guard, selects the partial-index arm, and correctly reports index 1 as invalid. Non-power-of-two NUM_BANKS is the only configuration in which REQ_BAD_BANK is reachable — a regression using only powers of two never exercises it. ROW_W == 1 is legal and makes the comparator one bit. req_valid low is not an error and not a class.
Failure modes and debugging clues
A hit rate near 100% with varied traffic means the bank is being ignored in the comparison, or the state is not being consulted — §9. BUSY never asserting means completion events are arriving in the same cycle as requests, which is a testbench artefact rather than a design property. REQ_MISS on a bank that is demonstrably open means state_in is not the same object the FSM is driving.
Limitations
One request per cycle. One rank. It reports what a request is, never what to do about it — Chapters 9.4 and 9.5 add that. It cannot see the shared column path, so two consecutive hits in one bank group look identical to two in different groups. And it says nothing about timing, which is the limitation most likely to be forgotten because a hit feels like permission.
6. Classification, in Cycles
row_request_classifier — the class is a property of the moment
10 cyclesCompare cycles 1 and 3. req_bank is 3, req_row is 0x0200, and row_in[3] is 0x0200 at both. The request is bit-identical and the row register matches at both. At cycle 1 the class is BUSY; at cycle 3 it is HIT.
The only thing that changed is state_in[3] — and that is §1's third condition doing the only job it has. A classifier comparing bank and row alone reports HIT at both cycles, and the cycle-1 hit is a licence to issue a column access into the opening interval.
Cycle 5 is the conflict, with the same bank and a different row. Cycle 7 is the miss, on a bank that holds nothing. Three classes, one bank state model, and the difference between them is entirely in the pair of (state, row) values the classifier read.
Note that hit is low at every cycle where req_valid is low, including cycles where req_class still shows a stale combinational value. That gating is what keeps Chapter 9.6's counters honest.
REPRESENTATIVE EDUCATIONAL STATE TRANSITIONS. No interval here corresponds to a DDR timing parameter.
7. Four Assertions Worth Writing
// P1 -- a hit requires ALL THREE conditions of Section 1. Written as one
// property because they are one claim, and because a classifier that
// satisfied two of them would be the Section 6 cycle-1 bug.
property p_hit_requires_open_and_matching_row;
@(posedge clk)
hit |-> (state_in[req_bank] == BANK_OPEN)
&& (row_in[req_bank] == req_row);
endproperty
assert property (p_hit_requires_open_and_matching_row);
// P2 -- the non-hit classes each pin down their own state, so no class
// can absorb another's cases. A conflict in particular must have an OPEN
// bank AND a differing row: a classifier firing conflict on a closed bank
// would send a precharge to a bank with nothing to close.
// NOTE THE `and` OPERATOR. These are three implications, and `|->` yields
// a PROPERTY rather than a boolean -- so `&&` cannot join them. `and` is
// the property-level conjunction, which is what this needs.
property p_classes_pin_their_states;
@(posedge clk)
(miss |-> (state_in[req_bank] == BANK_CLOSED))
and (conflict |-> (state_in[req_bank] == BANK_OPEN)
&& (row_in[req_bank] != req_row))
and (busy |-> (state_in[req_bank] == BANK_OPENING)
|| (state_in[req_bank] == BANK_CLOSING));
endproperty
assert property (p_classes_pin_their_states);
// P3 -- exactly one class per valid request, and none when there is no
// request. Exhaustiveness matters because every consumer downstream
// chooses its action from this set: a request in no class would stall
// silently while each individual signal looked reasonable.
property p_exactly_one_class;
@(posedge clk)
req_valid ? $onehot({hit, miss, conflict, busy, bad_bank})
: (!hit && !miss && !conflict && !busy && !bad_bank);
endproperty
assert property (p_exactly_one_class);
// P4 -- an invalid bank index can never be the best case. Separated from
// P1 because it is the property that catches an index check removed by a
// well-meaning optimisation: with it gone, an out-of-range index reads
// whatever the array returns and can classify as a hit.
property p_bad_index_never_hits;
@(posedge clk)
bad_bank |-> !hit && !miss && !conflict && !busy;
endproperty
assert property (p_bad_index_never_hits);What these prove. P1 is the chapter's definition made checkable, and it is the one that fails on the §6 cycle-1 bug. P2 stops the classes overlapping, and its conflict clause is the most valuable single line — a conflict on a closed bank would produce a precharge for a bank holding nothing, which Chapter 9.1 would then reject and a controller might loop on. P3 proves the class set is a genuine partition, which is what makes it safe to build a decision on. P4 protects the index check.
What they do not prove. Nothing here says a hit may be acted on. Timing legality is Modules 13 and 14' and no property in this module substitutes for it — a hit is state permission only. Nothing says the supplied state is correct: state_in is an input, so P1 proves the classifier used the context it was given, not that the context matches the device. That comparison is Chapter 9.2's. Nothing proves anything physical — no property here concerns charge, sensing, restoration or bitline state, and the block contains no representation of them. And nothing proves the classification is useful: a classifier could be perfectly correct while the address map above it destroys locality, which is Module 18's subject.
8. DV — Classify From State, Not From Intent
There is one error in this area that dominates all the others, and it is worth its own section because it produces a flattering result rather than a broken one.
Classify against the bank state that actually held, at the cycle the request was classified. Not against what the controller intended, not against a request's own history, and not against the state after the resulting commands were issued.
The tempting shortcut: a request stream is generated, the generator knows which row each request wants, and it is easy to classify by comparing each request against the previous request to the same bank. That is a workload locality metric, and it is a perfectly good one — but it is not a hit rate. It ignores everything the controller did in between: an auto-precharge that closed the row, a refresh, a conflicting request from another requester, a close-page policy.
The two diverge in a specific direction. Request-to-request comparison overstates the hit rate, because every close the controller performed for its own reasons is invisible to it. A team measuring that number is measuring their workload and believing they are measuring their controller.
The requirements, concretely:
Sample the class at the cycle the request is presented to the classifier, with req_valid high, and record it with the transaction. A class recomputed later is a class of a different state.
Count only classified requests. The five views are gated on req_valid for this reason; telemetry taking req_class directly would fold idle cycles into whatever the enum happened to hold.
Keep the four classes separate all the way to the report. Chapter 9.6 develops why, and the CXL chapter cited in §2 makes the same point: a blended rate hides which failure dominates, and the two failures have different fixes.
Cover BUSY deliberately. It only occurs when a request arrives during a transition, which a testbench issuing one request per completed operation never produces. A regression with zero BUSY classifications has not tested the class, and Chapter 9.5 is where it matters.
And cross-check the classifier against the monitor's model. Chapter 9.2's row_context_monitor maintains an independent row context; classifying the same request against both and comparing catches a classifier reading a stale or wrong state input — which P1 cannot catch, because P1 checks the classifier against the state it was handed.
9. Debugging — A Hit Rate That Is Too Good
Symptom. A telemetry report shows a row-hit rate above 95% for a workload with no particular locality — random or strided traffic across a large footprint. Performance does not match what that hit rate would imply.
This symptom is worth practising because the number looks like good news. Nobody investigates a hit rate that is too high, which is exactly why this class of bug survives.
Candidate mechanisms.
- The bank is ignored in the comparison — the classifier compares the requested row against some open row, or against a single last-opened row, rather than against that bank's. §3.
- The state is not consulted, so requests during
BANK_OPENINGclassify as hits because the target row already sits inrow_in. §1's third condition. - The class is being derived by comparing each request with the previous request to the same bank, rather than against bank state. §8's shortcut.
- The counters are not gated on
req_valid, so idle cycles increment whichever counter the stale enum selects — and if that isREQ_HIT, the rate approaches 100% with no traffic at all. - A counter has wrapped. The miss and conflict counters overflow first if they are narrower or if the workload is long, and a wrapped counter reports a flattering ratio. Chapter 9.6 §5 is about exactly this.
- The hit rate is real, and the performance shortfall is elsewhere — a shared column path, timing, or one bank absorbing everything.
Evidence to collect. The absolute counts, not the ratio. The total request count from an independent source — the requester side, not the classifier. Per-bank breakdown of the counts. The counter widths and whether any is at its maximum. And for a sample of transactions, the (req_bank, req_row) alongside the (state_in, row_in) that were actually read.
Discriminator — the counts do most of the work.
- Does
hits + misses + conflicts + busy + bad_bankequal the independently known request count? If it exceeds it, mechanism 4 — cycles are being counted, not requests. If it falls short, a class is being dropped or a counter has saturated. - Is any counter at its maximum value? Mechanism 5, and the ratio is meaningless. This is a one-glance check and it is the first thing to look at, because it invalidates everything else.
- Look at the per-bank breakdown. Mechanism 1 has a distinctive signature: hits attributed to banks that were never activated. A bank with hits and no activates cannot be right, and that comparison localises the bug immediately.
- Sample transactions and recompute the class by hand from the recorded
(state_in, row_in). If the recomputation disagrees with the recorded class, the fault is in the classifier — mechanism 1 or 2, distinguished by whether the disagreements cluster on transitional states. - Check whether hits are being recorded for requests whose bank was in
BANK_OPENING. Mechanism 2, and it is worth a dedicated check because P1 would have caught it had it been enabled — its absence is the finding. - Compare against a request-to-request locality metric computed separately. If the two numbers are identical, mechanism 3: the classifier is measuring the workload rather than the controller. If they differ, that difference is the controller's contribution, and it is a genuinely useful quantity to have.
- If the counts reconcile, the per-bank breakdown is sane, and hand-recomputation agrees, the hit rate is real — mechanism 6, and the search moves to Chapter 5.3's shared path, Modules 13 and 14' timing, or Chapter 9.6 §8's serialisation case.
Responsible layer. Mechanisms 1 and 2 are the classifier — level C, this chapter. Mechanisms 3, 4 and 5 are the instrumentation, not the design at all, and they are the majority: a telemetry bug is far more common than a classifier bug, and it presents identically. Mechanism 6 is a real performance question owned elsewhere. None is a physical fault.
Fix. Enable P1 to P4 — between them they forbid mechanisms 1, 2 and 4. For 5, saturate rather than wrap, which is Chapter 9.6 §5's argument. For 3, keep the locality metric and the hit rate as separate reported numbers, because the pair is more informative than either.
10. Common Misconceptions
"Same row number means a row hit."
Why it is tempting: the row number is the obvious thing to compare, and it is one of the three conditions.
Concrete failure: row 100 of bank 0 is compared against a request for row 100 of bank 1 and reported as a hit. They are unrelated locations. The hit rate climbs and the classifier measures nothing.
Correct model: row identifiers are per bank. A hit needs the same bank, the same row, and a usable state. §1 and §3.
Prevention: P1, and the per-bank count check in §9 — hits in a bank with no activates cannot be real.
"A row hit has zero latency."
Why it is tempting: it is the best case, and "hit" borrows from caches where a hit is nearly free.
Concrete failure: a performance model in which hits cost nothing. It over-predicts throughput for locality-heavy workloads and cannot explain why a 95% hit rate does not give 95% of peak.
Correct model: a hit is the absence of a required row-state transition and nothing more. The column access, its pipeline depth, and the shared column path all remain. §4.
Prevention: state the claim as the absence of a transition. It stays true and composes with the timing modules.
"A row hit bypasses DDR timing rules."
Why it is tempting: the state is ready, so the command feels permitted.
Concrete failure: a controller issues a column command on a hit without checking timing, and violates a constraint the state model knows nothing about.
Correct model: state legality is necessary and not sufficient — Chapter 7.3 §4. A hit is state permission; timing permission is separate and is Modules 13 and 14'.
Prevention: keep the two checks as two checks. This chapter's classifier deliberately cannot answer the timing question, and that is a feature.
"A row miss means the data is missing."
Why it is tempting: borrowed cache vocabulary, where a miss means the data is at another level.
Concrete failure: an engineer looks for the backing store a DRAM miss fetches from. There is none, and the search wastes real time.
Correct model: every row is always present in the array. A non-hit means the row is not currently held, so a transition is needed. Chapter 9.2 §3 and Chapter 9.4.
Prevention: say "not currently open" rather than "not present."
"Any non-hit is the same kind of miss."
Why it is tempting: both are failures to hit, and one word is simpler than two.
Concrete failure: a report gives a single "miss rate" of 40%. Whether that is mostly closed banks or mostly wrong rows changes the required response — one activate versus a precharge and an activate — and the two have different fixes. The report cannot distinguish a workload problem from a page-policy problem.
Correct model: four classes, §2. The non-hit cases need different responses in different amounts.
Prevention: count them separately all the way to the report. Chapter 9.6.
"A closed-bank access is a row conflict."
Why it is tempting: both are non-hits requiring an activate, and "conflict" sounds like a general word for trouble.
Concrete failure: a planner classifying a closed bank as a conflict emits a precharge first, for a bank with nothing to close. Chapter 9.1 rejects it with reason 2, and a controller that ignores rejections stalls.
Correct model: a conflict requires a bank that is open with a different row. A closed bank needs only an activate. §2, and P2's conflict clause.
Prevention: P2. It is the property that makes this structurally impossible.
"A valid command encoding means the command is state-legal."
Why it is tempting: Module 7 spent a whole module on encoding, and a well-formed command feels complete.
Concrete failure: a well-formed activate is issued to a bank that already holds a row. The encoding is perfect and the transition is illegal.
Correct model: Chapter 7.1 §3's four separate questions — encoding, semantics, state legality, timing legality. This module is the third.
Prevention: keep the four apart. A command passing an encoding check has passed one of four.
11. Interview Reasoning
"Why is a row hit defined per bank?"
Because each bank owns its own row decoder and sense amplifiers, so each numbers its rows independently and holds its own row. Row 100 exists in every bank and those are unrelated locations. So "the requested row" has no meaning until a bank is fixed — a row operand is half a coordinate. The practical consequence is a measurement one: a classifier that compares rows without comparing banks reports hits across unrelated banks, and the resulting hit rate is high, stable, and meaningless. The check that catches it is that a bank showing hits but no activates cannot be right.
"Can row 100 in bank 0 and row 100 in bank 1 form a row hit?"
No, and the reason is worth being precise about: they are not the same row in any sense. They are different physical rows in different arrays, reached through different row decoders, holding unrelated data. The shared number is an artefact of per-bank numbering. A hit requires one bank to be holding, right now, the row being asked for.
"Why isn't a row hit free?"
Because it removes one thing only — the row-state transition. The column command still has to be issued, the column path still has a real pipeline depth from command to data, banks in a group still share that path, and the timing rules still apply. A hit is state permission, not timing permission. The precise claim is that a hit is the absence of a required row-state transition, and stating it that way keeps it true across generations and speed bins instead of relying on a number.
"A request arrives for the exact row a bank is currently opening. Is that a hit?"
No — and this is the case that separates a correct classifier from a plausible one. The bank is in a transitional state: the row is selected but the values are not yet resolved and held, so a column access has no defined meaning. The row register already matches, which is precisely why a classifier that compares bank and row without testing the state will call it a hit and licence an access into the opening interval. The right classification is busy, and the right response is to wait, not to issue.
"Your hit rate is 96% on random traffic. What do you do?"
Distrust it, and check the counts rather than the ratio. First, is any counter saturated — that alone invalidates the number. Second, does the sum of all classes equal the request count from an independent source; if it exceeds it, idle cycles are being counted. Third, the per-bank breakdown: hits in a bank that was never activated cannot be real, and that localises a bank-ignored comparison immediately. Fourth, whether the classification is being computed against bank state or against the previous request to the same bank — the latter measures workload locality, not the controller, and always reads high.
12. Engineering Exercise
NUM_BANKS = 4, ROW_W = 16. Context: b0 CLOSED; b1 OPEN(0x0020); b2 OPENING target 0x0055; b3 CLOSING, row_in[3] = 0x0077.
1. Classify: (a) b1, 0x0020 · (b) b1, 0x0021 · (c) b2, 0x0055 · (d) b3, 0x0077 · (e) b0, 0x0020.
2. Which two of those five would a classifier that compares only bank and row get wrong, and in which direction?
3. Bank 3 completes its precharge. Reclassify (d). Then bank 3 is activated to 0x0077 and completes. Reclassify (d) again.
4. A colleague reorders the unique case so the row comparison is tested before the state. Give the failing request and the consequence.
5. Write the property that catches a conflict reported on a closed bank, and say what the bug would cause downstream.
6. A report shows 100% hits over 10,000 requests with busy never asserting. Give two independent explanations and the one check that separates them.
13. Summary
A row hit requires three things: the same bank, the same row, and a bank in BANK_OPEN. The usual definition keeps only the middle one, and each omission produces a classifier that reports an excellent hit rate while measuring nothing.
Row identifiers are per bank. Row 100 exists in every bank and those are unrelated locations, so a comparison that ignores the bank is not a hit test. The signature of that bug is hits in a bank that was never activated.
A matching row register is not a hit. A bank that is opening already carries its target row, and a bank that is closing still carries the row it is releasing. Test the state before comparing the row — that ordering is what makes the third condition structural.
Four classes, kept separate: hit, miss on a closed bank, conflict on an open bank holding a different row, and busy during a transition. Busy is not a kind of miss — nothing is wrong, a transition is simply in flight, and the answer is to wait.
A hit is the absence of a required row-state transition, and nothing more. Not zero latency, not timing permission, not bandwidth. Stated that way the claim survives every generation and composes with the timing rules that arrive later.
And a hit rate that looks too good usually is. Check the counts against an independent request total, check for a saturated counter, and check the per-bank breakdown before believing a ratio.
14. What Comes Next
The best case is settled. Chapter 9.4 — Row Misses takes the simpler of the two non-hit cases: the bank holds nothing at all.
It is simpler in its response — one activate, no ordering obligation — and more interesting than it first appears, because a closed bank is not merely an absence. It got that way somehow: through reset, through an explicit precharge, through an auto-precharge nobody saw on the wire, or by never having been opened. Those provenances debug differently, and a model that records only closed has thrown away the information that would tell you which.
That chapter also settles the vocabulary question this one raised: why "miss" is a borrowed word that fits badly, and what it should be understood to mean when nothing is ever actually missing.
Return to Row Opening for the state this chapter classifies against, The Row Buffer for what an open row is, Banks for per-bank independence, Row Address for why a row operand is half a coordinate, CAS# for the column-to-data pipeline a hit does not remove, and Write for state legality versus timing legality.
Continue learning
Related tutorials
- Related topic
Memory Matrices and Hierarchy
Three independent pressures all say the same thing: a single flat array forces a choice that partitioning avoids. Why DRAM is built from many small local arrays with local sensing, how that produces hierarchical addressing, and how physical structure becomes controller-visible state.
- Related topic
Banks
A bank is the scope of row state, and its value is isolation: an operation on one bank cannot disturb another. That guarantee has exactly one deliberate exception, and one silent failure mode that only the controller's own model can prevent.
- Related topic
Row Opening
ACTIVATE transfers no data. It changes a bank from closed to holding one row, and it does so over an interval rather than instantly — which is why a bank has transitional states and why a controller must track them.
- Related topic
Why Timing Parameters Exist
A DDR command can be perfectly meaningful and target a bank in exactly the right state and still be illegal right now. That third refusal is what timing parameters are, and it is a different question from the first two.
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.
