DDR · Module 9
Row Conflicts
A bank holding the wrong row cannot just open the right one — there is nowhere for a second row to go. That produces a two-command response with an order inside it, and an entirely new kind of contention.
Chapter 9.3 handled the bank that holds the row you want. Chapter 9.4 handled the bank that holds nothing. This chapter takes the remaining case, and it is the expensive one:
The bank is open, and it is holding the wrong row.
Every engineer's first question here is the right one, and it has a structural answer rather than a procedural one:
Why can the controller not simply activate the row it wants?
The answer produces the only action in this module with a dependency inside it — two commands that must happen in a required order, with the second unable to begin until the first has completed. And that ordering, in turn, produces a kind of contention the earlier chapters could not express: a request arriving while a two-step transition is still in flight.
1. Why Not Just Open the New Row
Because there is nowhere for it to go.
Chapter 3.5 established the structure: one sense amplifier per bitline pair, and a row spans all of them. Chapter 9.2 §1 established what "open" means in terms of it — the amplifiers are in a resolved state holding that row's values.
So the amplifiers are the resource, and they are singular. Resolving a second row would require them to hold two sets of values at once, which is not a capacity limitation that a bigger design could relax — it is what the circuit is. A sense amplifier holds one resolved value.
And there is a second, sharper reason that is easy to miss. Chapter 2.4 §4 established that sensing amplifies a small difference between a bitline pair, which requires those bitlines to start from a known balanced condition. A bitline pair still carrying the previous row's resolved values is not balanced — it is driven hard to full levels. Connecting a new row's cells to it would not produce a small readable difference; it would overwrite those cells with whatever the bitlines are holding.
Therefore the bank must first be returned to the condition sensing requires — wordline deasserted, cells disconnected, bitlines equalised — which is exactly what Chapter 9.1 §4 described a precharge as doing. The precharge is not a courtesy or an accounting step. It is the physical precondition for the next activate.
2. The Two-Command Response, and the Order Inside It
bank OPEN(row A), request wants row B, A != B
→ PRECHARGE bank ... releases row A
→ (the bank becomes CLOSED)
→ ACTIVATE bank, row B ... resolves row B
Two commands. And the second CANNOT begin until the first
has completed, because it needs the state the first produces.This is the first action in the module with a dependency inside it, and the dependency is worth characterising precisely, because it is easy to file it under the wrong heading.
It is a causal dependency, not a timing constraint. The activate needs the bank to be in BANK_CLOSED; the precharge is what produces that state. Even in a hypothetical device with no timing rules at all, the order would still be forced — because the second command requires a precondition only the first can establish.
Timing constraints exist too, and they are separate. Modules 13 and 14 define minimum intervals, and those add duration to an ordering that already exists for structural reasons. Conflating them is the mistake: an engineer who believes the order exists because of a timing parameter will expect a fast-enough device to relax it, and no device ever will.
3. A Request's Class Changes While It Waits
Here is where this chapter's second subject appears, and it follows directly from §2's two-step response.
The transition takes time, and during it the bank is in BANK_CLOSING and then BANK_OPENING. Chapter 9.1 introduced those states and Chapter 9.3 §2 gave requests arriving during them a class: BUSY.
So a single unchanging request walks through the taxonomy as its bank transitions underneath it:
bank OPEN(A), request B → CONFLICT (2 steps remain)
precharge issued
bank CLOSING, request B → BUSY (wait)
precharge completes
bank CLOSED, request B → MISS (1 step remains)
activate issued
bank OPENING, request B → BUSY (wait)
activate completes
bank OPEN(B), request B → HIT (0 steps remain)The request never changed. Its class changed four times. That is the strongest demonstration in the module that these classes are properties of a moment, not of an address — and §6 puts it on a waveform.
It also explains what the BUSY class is for. A request meeting BANK_CLOSING is not a miss and not a conflict; the bank is mid-transition and the only correct response is to wait. Issuing anything would be wrong: a precharge would be rejected (the bank is not open), and an activate would be rejected (the bank is not closed). BUSY is the class whose correct action is no action, which is why a three-class taxonomy has to misfile it.
4. The Full Transition
Read the two step arrows and the two replies between them. The activate cannot be sent when the precharge is sent; it has to wait for the reply that says the bank reached CLOSED. That gap is the serialisation, and it is why a conflict is not simply "two commands' worth" of cost but two commands that cannot overlap.
And note what the planner does between them: nothing. It re-derives the plan from the state each cycle and reports WAIT while the transition is in flight. It holds no plan and remembers no obligation — §5 explains why that is a design decision rather than a simplification.
5. RTL — The Transition Planner
The engineering problem
Turn a classified request into the action its bank's state actually permits, including how many row-state transitions remain before a column access becomes possible — without deciding anything about ordering, priority, or when.
Why hardware needs it
Every request needs to know what it is waiting for. A controller that knows only hit or not cannot tell a request that needs one command from one that needs two, and therefore cannot reason about its own work.
Classification
SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL. Level B and C only.
What it models
The action a request requires given its class and its bank's state; the number of row-state transitions remaining before a column access is possible; and the separation of the two conflict steps in the correct order.
What it does NOT model
Classification (Chapter 9.3, consumed). State (Chapter 9.1, consumed). Timing (Modules 13, 14) — may_access_column means the state permits it, never that timing does. Command encoding (Chapter 7.1). Scheduling of any kind. Data (Modules 10 to 12). Rank (Chapter 8.5) — one rank at a time.
Interface and parameter contract
// ─────────────────────────────────────────────────────────────────────────
// conflict_transition_planner
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
//
// MODELS: a function from (classified request, bank state) to the action
// that request needs next, plus the number of row-state transitions
// remaining before a column access is possible.
//
// STATELESS BY DESIGN. The bank state IS the memory (Chapter 9.1), so the
// plan is re-derived every cycle. A planner that stored pending
// obligations would be one step from a queue, and queues are Module 17's.
//
// NOT A SCHEDULER. No queue, no arbitration, no reordering, no policy, no
// QoS, no starvation handling, no timing counters, no command issue. It
// answers WHAT a request needs -- never WHO goes first or WHEN.
//
// NOT A TIMING CHECK. may_access_column means the STATE permits a column
// access, never that timing does (Modules 13, 14).
//
// MODELS NO PHYSICAL OR ANALOG BEHAVIOUR.
// ─────────────────────────────────────────────────────────────────────────
// What this request needs next. Chapter 9.5 Section 5.
typedef enum logic [2:0] {
PLAN_NONE = 3'd0, // no valid request
PLAN_DIRECT_COLUMN = 3'd1, // hit: state permits a column access
PLAN_ACTIVATE = 3'd2, // miss: one activate, no ordering obligation
PLAN_PRECHARGE = 3'd3, // conflict: precharge FIRST, activate after
PLAN_WAIT = 3'd4, // a transition is in flight; issue nothing
PLAN_INVALID = 3'd5 // the bank index names no bank
} plan_action_e;
module conflict_transition_planner #(
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 req_class_e req_class,
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],
output plan_action_e plan_action,
// ── Row-state transitions remaining before a column access is possible.
// 0 hit, 1 miss, 2 conflict. During a transition the count is the
// number still OWED, which is why it is derived from the state rather
// than latched when the plan was first made.
output logic [1:0] steps_remaining,
output logic steps_known,
// ── Broken-out actions. At most one is ever high.
output logic may_access_column,
output logic issue_precharge,
output logic issue_activate,
output logic must_wait,
output logic [BA_W-1:0] target_bank,
output logic [ROW_W-1:0] target_row
);
if (NUM_BANKS < 1) begin : g_nb
initial $fatal(1, "conflict_transition_planner: NUM_BANKS must be >= 1");
end
if (ROW_W < 1) begin : g_rw
initial $fatal(1, "conflict_transition_planner: ROW_W must be >= 1");
end
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
bank_row_state_e sel_state;
always_comb begin
sel_state = BANK_CLOSED;
if (!idx_bad) sel_state = state_in[req_bank];
end
// ── The plan. One case over the class, because Chapter 9.3 already did
// the work of reducing (state, row) to a class -- re-deriving it here
// would be a second classifier that could disagree with the first.
//
// THE STATE IS STILL CONSULTED, for one thing only: distinguishing
// which half of a conflict's two-step transition is in flight, which
// the class BUSY does not say.
always_comb begin
plan_action = PLAN_NONE;
steps_remaining = 2'd0;
steps_known = 1'b0;
if (req_valid) begin
unique case (req_class)
REQ_HIT: begin
plan_action = PLAN_DIRECT_COLUMN;
steps_remaining = 2'd0;
steps_known = 1'b1;
end
REQ_MISS: begin
plan_action = PLAN_ACTIVATE;
steps_remaining = 2'd1;
steps_known = 1'b1;
end
REQ_CONFLICT: begin
// PRECHARGE, not activate. The activate is step two and cannot
// be issued now -- Section 2's causal dependency.
plan_action = PLAN_PRECHARGE;
steps_remaining = 2'd2;
steps_known = 1'b1;
end
REQ_BUSY: begin
plan_action = PLAN_WAIT;
// How far there is still to go depends on WHICH transition is
// in flight, and that is the one thing the class does not carry.
// CLOSING means a close is under way and an activate is still
// owed: two. OPENING means only the activate remains: one.
if (sel_state == BANK_CLOSING) begin
steps_remaining = 2'd2;
steps_known = 1'b1;
end else if (sel_state == BANK_OPENING) begin
steps_remaining = 2'd1;
steps_known = 1'b1;
end else begin
// BUSY with a non-transitional state is incoherent input.
// Reported as UNKNOWN rather than guessed: a fabricated step
// count would flow straight into Chapter 9.6's work model.
steps_remaining = 2'd0;
steps_known = 1'b0;
end
end
REQ_BAD_BANK: begin
plan_action = PLAN_INVALID;
steps_known = 1'b0;
end
default: begin
plan_action = PLAN_INVALID;
steps_known = 1'b0;
end
endcase
end
end
// ── Views. Note that issue_activate is NOT asserted for a conflict:
// the conflict's activate is step two, and it will be issued when the
// request is re-presented against a CLOSED bank and classifies as a
// miss. That is what makes the ordering structural here rather than
// something a consumer has to remember.
assign may_access_column = (plan_action == PLAN_DIRECT_COLUMN);
assign issue_precharge = (plan_action == PLAN_PRECHARGE);
assign issue_activate = (plan_action == PLAN_ACTIVATE);
assign must_wait = (plan_action == PLAN_WAIT);
assign target_bank = req_bank;
assign target_row = req_row;
endmoduleState representation
None, and §5's callout explains why that is the architectural claim rather than a shortcut. The bank state carries everything the plan depends on, so the plan is a pure function of it.
One consequence is worth drawing out. A conflict's activate is never issued by the conflict plan. The conflict plan issues a precharge; when the bank reaches CLOSED the same request re-presents, classifies as a miss, and gets PLAN_ACTIVATE. The two-step sequence emerges from re-planning against a changed state rather than from a stored plan — which means the ordering cannot be violated by a consumer that forgets step two, because there is no step two to forget.
Combinational behaviour
An index check, a guarded state read, a six-way case over the class, and four mutually exclusive views. The state is consulted for exactly one purpose: splitting REQ_BUSY into "two steps still owed" and "one step still owed," which the class alone cannot say.
Sequential behaviour and reset
Neither. A registered plan would be a plan for a state that may have moved — and in this chapter's sequence the state moves constantly, so a stale plan would be wrong within a cycle or two.
Bit-level derivation
steps_remaining maps as follows, and this is the table Chapter 9.6 consumes:
| Class | Bank state | plan_action | steps_remaining | steps_known |
|---|---|---|---|---|
| HIT | OPEN, rows match | DIRECT_COLUMN | 0 | 1 |
| MISS | CLOSED | ACTIVATE | 1 | 1 |
| CONFLICT | OPEN, rows differ | PRECHARGE | 2 | 1 |
| BUSY | CLOSING | WAIT | 2 | 1 |
| BUSY | OPENING | WAIT | 1 | 1 |
| BUSY | non-transitional | WAIT | 0 | 0 |
| BAD_BANK | — | INVALID | 0 | 0 |
The two BUSY rows are the reason the state is an input. A request waiting on a close still owes both transitions; one waiting on an open owes only the activate. A planner that reported a single number for BUSY would double-count or under-count every conflict in flight, and Chapter 9.6's work model would inherit the error.
Cycle-by-cycle example
Bank 2 holds row 0x0300; a request wants row 0x0400. Following the same request through:
| Cycle | Bank state | Class | Plan | Steps |
|---|---|---|---|---|
| 0 | OPEN(0300) | CONFLICT | PRECHARGE | 2 |
| 1 | CLOSING | BUSY | WAIT | 2 |
| 2 | CLOSING | BUSY | WAIT | 2 |
| 3 | CLOSED | MISS | ACTIVATE | 1 |
| 4 | OPENING | BUSY | WAIT | 1 |
| 5 | OPEN(0400) | HIT | DIRECT_COLUMN | 0 |
One request, four classes, three plans, and steps_remaining counting down monotonically. Nothing was stored to make that happen.
How to simulate, and expected output
Drive the table and check every output each cycle. Then:
issue_activate must be low at cycle 0. A conflict's plan is a precharge; a planner asserting both would license an ordering violation, and §7's P2 forbids it.
steps_remaining must never increase across the sequence for an unchanging request. A rise means the bank moved backwards — which can only happen if something else is using it, and is a genuine finding rather than a planner bug.
BUSY with BANK_OPEN — incoherent input, constructed by forcing the class — must give steps_known low, not a plausible number.
req_valid low must give PLAN_NONE with all four views low.
NUM_BANKS = 3 with req_bank = 3 must give PLAN_INVALID and steps_known low.
Expected waveform
§6, which is this table with the commands and completions that drive it.
Synthesis implications
A multiplexer on the state, a six-way case, and four decoders. Smaller than Chapter 9.3's classifier, because the hard work — reducing a state and a row to a class — was already done upstream. That is the argument for the split: two small combinational blocks in sequence, each with its own assertions, rather than one block doing both jobs and being checkable as neither.
Corner cases
NUM_BANKS == 1 works through the guard. Non-power-of-two counts are the only configuration where PLAN_INVALID is reachable. REQ_BUSY with a non-transitional state cannot arise from Chapter 9.3's classifier — it is incoherent input, handled by reporting steps_known low rather than by guessing, because a fabricated step count would flow directly into a work model and quietly corrupt it. The unique case keeps a default for an out-of-range class encoding, and it maps to PLAN_INVALID rather than to any plausible action.
Failure modes and debugging clues
issue_activate asserting on a conflict means the class and the plan have been wired together wrongly — P2 catches it. steps_remaining of 2 while the bank is OPENING means the two BUSY arms are swapped, which under-reports work for closes and over-reports for opens. must_wait asserting permanently for one bank means the transition it is waiting on never completes — check Chapter 9.1's completion events reach the FSM.
Limitations
One request per cycle, one rank. It plans for one request against one bank and has no view of any other request — which is exactly the scheduler boundary, and the reason it cannot tell you whether waiting is the right choice, only that it is the required one. It says nothing about timing. And steps_remaining counts row-state transitions only: refresh, calibration and everything else a bank may owe are invisible to it.
6. The Conflict Transition, in Cycles
conflict_transition_planner — the plan follows the state
10 cyclesreq_row is 0x0400 at every cycle. The request does not change. req_class changes four times — conflict, busy, miss, busy, hit — and plan_action follows it.
Cycle 0 is the conflict, and note what issue_activate is doing: nothing. The plan is a precharge. The activate that this request ultimately needs is not issued here and is not remembered anywhere — it appears at cycle 3, when the bank has reached CLOSED and the very same request classifies as a miss. §5's point about the ordering being structural rather than remembered is visible in exactly those two signals.
steps_remaining counts 2, 2, 2, 1, 1, 1, 1, 0 — monotonically down, never stored, re-derived each cycle from the bank state. And it is 2 throughout the closing phase and 1 throughout the opening phase, which is the distinction the two BUSY arms exist to make.
The WAIT cycles are the contention. During them the bank is unavailable to this request and to every other one. What a controller does with those cycles — serve a different bank, hold, or reorder — is Module 17's question, and this block deliberately has no opinion.
REPRESENTATIVE EDUCATIONAL STATE TRANSITIONS. The one-cycle and two-cycle transition intervals are reading conveniences and correspond to no DDR timing parameter whatsoever.
7. Five Assertions Worth Writing
// P1 -- a conflict plans a PRECHARGE and never an activate. The ordering
// made checkable at the point a consumer reads it: this is the property
// that fails if someone "optimises" the conflict path by issuing the
// activate the request ultimately needs.
property p_conflict_precharges_first;
@(posedge clk)
(req_valid && (req_class == REQ_CONFLICT))
|-> issue_precharge && !issue_activate && !may_access_column;
endproperty
assert property (p_conflict_precharges_first);
// P2 -- and the converse for the other two classes, so no class can
// borrow another's action. A miss must never precharge (Chapter 9.4's
// claim, checked here at the planner) and a hit must never do either.
property p_actions_match_their_classes;
@(posedge clk)
((req_valid && (req_class == REQ_MISS))
|-> issue_activate && !issue_precharge)
and ((req_valid && (req_class == REQ_HIT))
|-> may_access_column && !issue_activate && !issue_precharge);
endproperty
assert property (p_actions_match_their_classes);
// P3 -- a column access is never permitted while a transition is in
// flight. THE safety property of the chapter: the access it forbids is
// the one Chapter 9.1's four-state model was introduced to exclude, and
// it is the bug a three-class taxonomy cannot even express.
property p_no_column_access_during_transition;
@(posedge clk)
((state_in[req_bank] == BANK_OPENING)
|| (state_in[req_bank] == BANK_CLOSING))
|-> !may_access_column;
endproperty
assert property (p_no_column_access_during_transition);
// P4 -- at most one action at a time. The four views drive four different
// consumers, and two of them agreeing to act in the same cycle would put
// two commands on an interface that carries one.
property p_at_most_one_action;
@(posedge clk)
$onehot0({may_access_column, issue_precharge, issue_activate, must_wait});
endproperty
assert property (p_at_most_one_action);
// P5 -- a step count is reported only when it is known, and a conflict
// always reports two. Stops a fabricated count reaching Chapter 9.6's
// work model, where it would be indistinguishable from real work.
property p_steps_are_known_or_absent;
@(posedge clk)
(!steps_known |-> (steps_remaining == 2'd0))
and ((req_valid && (req_class == REQ_CONFLICT))
|-> steps_known && (steps_remaining == 2'd2));
endproperty
assert property (p_steps_are_known_or_absent);What these prove. P1 is the chapter's ordering claim at the point it can be violated. P2 stops the classes borrowing each other's actions — and its miss clause is Chapter 9.4's structural claim re-checked one layer down, which is worth doing because that is where a consumer actually reads it. P3 is the module's most important safety property: it forbids a column access during a transition, which is the access the whole four-state model exists to exclude. P4 keeps the four views usable by four independent consumers. P5 protects the work model downstream.
What they do not prove. Nothing here says an action may be taken now. Every output is about what the state requires; timing legality is Modules 13 and 14' and no property in this module substitutes for it. Nothing says the plan is a good one — waiting may be correct and may be a starvation bug, and distinguishing those needs a view of other requests, which is Module 17's by construction. Nothing proves the inputs are right: req_class and state_in are inputs, so these prove the planner responded correctly to what it was given, with Chapter 9.3's and Chapter 9.1's properties establishing those. And nothing proves anything physical — no property here concerns charge, sensing, restoration or bitline equalisation, and §1's destructive-activation argument is a reason for the design, not something this RTL models or could check.
8. DV — Verifying an Ordering That Is Never Stored
The verification problem in this chapter is unusual: the ordering obligation is real, and it exists nowhere as a variable.
There is no "pending activate" register to inspect, no plan object, no transaction that spans both commands. The two-step sequence is an emergent property of re-planning against a changing state — which means a checker cannot verify it by reading a field. It has to verify it from the command stream.
The check that works: for every conflict, confirm that the next row-state command to that bank is a precharge, and that the activate for the new row comes after that precharge has completed. Stated as a sequence rather than as a state:
classify CONFLICT on bank B
→ next row-state command to B is PRE, not ACT
→ precharge completes
→ then ACT to B may appearThree related requirements:
Cover BUSY deliberately, because a naive testbench never produces it. A test that issues one request, waits for it to complete, and issues the next will never present a request during a transition. Every BUSY path, both step counts, and P3 are then untested — and P3 is the module's central safety property. Producing BUSY requires either a second requester or a testbench that re-presents a request every cycle, and it should be a deliberate coverage goal rather than an accident.
Count commands against classes. Over a run, every conflict should produce one precharge and one activate; every miss, one activate; every hit, neither. So activates == misses + conflicts and precharges >= conflicts, with the excess attributable to page policy. A mismatch localises quickly: more activates than that means requests are being retried and recounted; fewer means they are being dropped.
Check that steps_remaining never increases for a request in flight. A rise means the bank went backwards — it was taken by something else, closed by a refresh, or auto-precharged mid-sequence. That is not a planner bug and it is worth knowing, because a request whose step count keeps resetting is a request that may never complete, which is the starvation case Module 17 has to handle.
9. Debugging — Only Alternating Rows Fail
Symptom. A test passes for repeated access to one row and passes for scattered access across banks. It fails only when the pattern alternates between two rows in the same bank — A, B, A, B — and the failure is data corruption rather than a protocol error.
This pattern is diagnostic on its own, and recognising it is worth more than any single check: alternating rows in one bank is the only access pattern that exercises the conflict path repeatedly, so a bug confined to it is almost certainly in the two-step transition.
Candidate mechanisms.
- The activate for the new row is being issued before the precharge has completed — the ordering is being violated, and the device is asked to open a row into a bank that has not been released.
- The precharge is being issued but the controller is not waiting for its completion before re-planning, so the request re-presents while the bank is still
CLOSINGand something acts on a stale plan. - A column access is being permitted during the transition — P3's case — so data is read from a bank mid-release.
- The conflict is being classified as a miss, so only an activate is issued, with no precharge at all. The activate is rejected and the access proceeds against the old row, which is still open.
- The row comparison is correct but the bank comparison is not, so a conflict in one bank is being resolved in another — and the original bank keeps its old row.
- Everything is correct and the corruption is in the address map: A and B are being computed to the same bank when they should not be, or to different rows when they should be the same.
Evidence to collect. For a failing pair: the full command sequence to that bank, with each command's acceptance and any reject_reason from Chapter 9.1. The plan_action and steps_remaining each cycle. The bank state at the cycle each command was issued. And whether the two addresses genuinely map to the same bank and different rows — Chapter 8.6's decomposition.
Discriminator.
- Is there a precharge in the sequence at all? If not, mechanism 4 — and Chapter 9.1's
reject_reason == 1on the activate confirms it immediately. This is the fastest check and it eliminates the most. - Is there a precharge, and does the activate follow its completion or its issue? Following the issue is mechanism 1. The signature is a
reject_reason == 1on the activate, because the bank is stillCLOSING. - Was
may_access_columnhigh while the state was transitional? Mechanism 3, and P3 would have caught it — its absence from the assertion set is itself the finding. - Does
steps_remainingreset upward mid-sequence? Mechanism 2, or interference from another requester. Check whether any other command to that bank appears in the window. - Compare the bank operand on the precharge against the one on the activate. A mismatch is mechanism 5, and its signature is distinctive: a third bank's state changes during a two-bank test. Chapter 9.1's P4 catches it.
- Decompose both addresses and confirm same bank, different rows. If they are not, mechanism 6 and the fault is in Module 8, not here. Do this check early — it is cheap and it eliminates an entire layer.
Responsible layer. Mechanisms 1 to 4 are the controller's planning and issue logic, level C, this chapter. Mechanism 5 is an indexing fault. Mechanism 6 is the address map. None is a physical fault — though mechanism 1 is the one case in this module where the physical consequence matters, because §1 established that activating into an unreleased bank is destructive rather than merely illegal.
Fix. For 1 and 2, gate the activate on the modelled completion, per Chapter 9.1 §8. For 3, enable P3. For 4, enable Chapter 9.3's P2. For 5, Chapter 9.1's P4. And add the alternating-row pattern to the regression permanently — it is one line of stimulus and it is the only pattern that exercises this path.
10. Common Misconceptions
"A conflict can simply activate the new row immediately."
Why it is tempting: the bank is available, the command is well-formed, and the old row is not wanted.
Concrete failure: the activate is rejected by a correct controller — and on a device, connecting a new row's cells to bitlines still driven to the previous row's values overwrites those cells. This is the one misconception in the module whose consequence is data destruction rather than a stall.
Correct model: the bank must be returned to the balanced condition sensing requires before another row can be resolved. §1.
Prevention: P1, and understanding the reason rather than the rule — a prohibition understood as a convention gets optimised away.
"Precharge writes the row back before closing it."
Why it is tempting: the cache-eviction analogy, and the intuition that something must be preserved.
Concrete failure: a work model that charges a precharge for data movement, and an engineer who cannot explain why closing a row nobody wrote costs the same as closing one that was.
Correct model: restoration happened during the activate — Chapter 9.1 §2. The cells are already correct before the precharge starts, so it moves nothing. Chapter 7.4 §2.
Prevention: remember that restoration is a step of opening. The misconception dissolves immediately.
"The precharge-then-activate order is a timing rule."
Why it is tempting: timing rules are everywhere in DDR, and the order does have timing attached to it.
Concrete failure: an engineer expects a faster device or a relaxed speed bin to allow the two to overlap. No device ever will, and time is spent looking for a parameter that would permit it.
Correct model: it is a causal dependency — the activate requires the state the precharge produces. Timing adds duration to an order that already exists. §2.
Prevention: ask what state each command requires. The order falls out with no timing knowledge at all.
"A conflict is just a slower miss."
Why it is tempting: both are non-hits ending in an activate, and both feel like the same kind of failure.
Concrete failure: a work model that treats them identically under-counts the required row-state work by a factor of two on conflict-heavy traffic, and the two have different fixes — a miss is often addressed by page policy, a conflict by address mapping.
Correct model: one transition versus two, serialised. §2.
Prevention: count them separately. Chapter 9.6.
"A request that is waiting has a stored plan somewhere."
Why it is tempting: the request clearly has an obligation, so it feels like there must be a record of it.
Concrete failure: an engineer looks for a pending-activate field to inspect during debug and cannot find one — or worse, adds one, creating a second source of truth that can disagree with the bank state.
Correct model: the plan is re-derived from the state each cycle. The bank state is the memory. §5.
Prevention: verify the ordering from the command sequence, not from a field. §8.
"A row conflict means two requests are conflicting with each other."
Why it is tempting: the word "conflict" strongly suggests two parties.
Concrete failure: an engineer looks for a second requester and concludes there is no conflict when only one requester exists — and mis-files a real conflict as something else.
Correct model: a conflict is between a request and the state a bank is already in. One requester alternating between two rows in one bank produces conflicts continuously with nobody to conflict with. In-flight contention, §3, is the case that genuinely involves two parties — and it is a different thing with a different class.
Prevention: define the class from the state, never from the number of requesters.
11. Interview Reasoning
"Why must a conflicting row be closed before another row can be opened?"
Because the bank has one sense-amplifier structure and it is currently holding a row's values. There is nowhere for a second row to go — that is not a capacity limit, it is what the circuit is. And there is a destructive consequence, which is the part worth knowing: sensing works by amplifying a small difference between a bitline pair, which requires those bitlines to start balanced. A pair still driven to the previous row's full logic levels is not balanced, so connecting a new row's cells to it would overwrite them with the old row's values. So the prohibition is structural and physical, not a convention — which matters, because a rule understood as a convention eventually gets optimised away.
"Is the precharge-then-activate order a timing constraint?"
No — it is a causal dependency, and the distinction has practical consequences. The activate requires the bank to be in the closed state, and the precharge is what produces that state; even with no timing rules at all the order would still be forced. Timing constraints exist separately and add duration to an ordering that already exists for structural reasons. An engineer who believes the order comes from a timing parameter will expect a fast enough part to relax it, and will spend real time looking for the parameter that permits the overlap.
"Why can two request streams with the same request count do very different amounts of DRAM work?"
Because the work is in the row-state transitions, not in the requests. A stream with good locality produces hits, which need zero transitions. A stream that finds banks closed produces misses at one transition each. A stream that alternates rows within a bank produces conflicts at two serialised transitions each. So the same number of requests can demand anywhere from zero to twice the request count in transitions — and since a conflict's two transitions cannot overlap, the difference is worse than the count suggests. That is the argument Chapter 9.6 builds on, and it needs no timing numbers to be decisive.
"Where does a controller store the pending activate for a conflict?"
It does not have to, and a good design does not. The bank state is the memory: the conflict plan issues a precharge, and when the bank reaches closed the same request re-presents, classifies as a miss, and gets an activate. The two-step sequence emerges from re-planning against a changed state. That is worth preferring because a stored obligation is a second source of truth that can disagree with the bank state, and because it means the ordering cannot be violated by a consumer that forgets step two — there is no step two to forget. The trade is that a checker cannot verify the ordering by reading a field; it has to verify it from the command sequence.
"A test fails only when alternating between two rows in one bank. Where do you look?"
At the conflict path, immediately — that pattern is the only one that exercises it repeatedly, so a bug confined to it is almost certainly there. The first question is whether a precharge appears in the sequence at all; if not, the conflict is being classified as a miss and the activate is being rejected while the old row stays open. If a precharge is there, the next question is whether the activate follows the precharge's completion or merely its issue — following the issue is the ordering violation, and it shows up as a rejected activate against a bank still closing. Before any of that, confirm the two addresses really do map to the same bank and different rows, because an address-map fault produces the same symptom one layer up.
12. Engineering Exercise
NUM_BANKS = 4. Bank 1 holds row 0x0050. A single requester issues, in order: b1 r0x0050 · b1 r0x0060 · b1 r0x0050 · b2 r0x0060.
1. Classify each request and give its plan and steps_remaining, assuming each transition completes before the next request arrives.
2. Total the row-state transitions the four requests require.
3. Reorder the four requests to minimise total transitions. What is the minimum, and what does the reordering cost you?
4. During the second request's precharge, a third request for b1 r0x0060 arrives. Classify it and give its steps_remaining. Then classify it again after the precharge completes.
5. A planner asserts issue_activate for a conflict. Write the property that catches it and describe the device-level consequence.
6. A controller stores a pending-activate field per bank. Give one concrete way it can disagree with the bank state, and the symptom.
13. Summary
A bank holding the wrong row cannot simply open the right one, because there is one sense-amplifier structure and it is already holding a row. Attempting it is not merely illegal — the new row's cells would be overwritten by bitlines still driven to the old row's values.
So the response is two commands with an order inside it: precharge to release the current row, then activate to resolve the new one. The order is causal, not a timing rule — the activate requires the state the precharge produces, and no device will ever relax that.
A hit needs zero row-state transitions, a miss one, and a conflict two, serialised. No number is involved, the claim holds across every generation, and it is the whole basis of the work model in Chapter 9.6.
A request's class changes while it waits. Conflict, then busy, then miss, then busy, then hit — one unchanging request, four classes, as the bank transitions underneath it. The classes are properties of a moment, not of an address.
The ordering obligation is stored nowhere. The plan is re-derived from the bank state each cycle, so step two cannot be forgotten because there is no step two to forget — and a checker must therefore verify the order from the command sequence rather than from a field.
And the alternating-row pattern is the only stimulus that exercises any of this. One line of it in a regression covers the conflict path, both BUSY step counts, and the safety property that forbids a column access mid-transition.
14. What Comes Next
All three classes are now defined, with their required responses and their transition counts: zero, one, and two-serialised.
Chapter 9.6 — Performance Impact asks the question those numbers make possible: how does the mix of classes in a request stream change the work the memory system has to do?
Not in nanoseconds — this module names no timing parameter and will not start now. In transitions, which is a quantity that stays correct across generations and speed bins and that this chapter's steps_remaining already produces. Two streams with identical request counts can demand very different amounts of row-state work, and the chapter derives that difference from the class distribution alone.
It also builds the instrumentation that measures it, and spends real effort on the ways such instrumentation lies — because a counter that has wrapped reports a flattering ratio, and a hit rate computed from the wrong thing reports a flattering number.
Return to Row Hits for the taxonomy and classifier, Row Misses for the one-command case, Row Opening for the state model and its transitional states, Sense Amplifiers for the structure that holds one row, Destructive Read for why bitlines must start balanced, and Precharge for the command that releases a row.
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
Precharge (PRE / PREA)
Precharge closes a bank, and its scope is decided by an operand: one bank or all of them. It is also the command that exposes why a command stream does not fully describe device state — which is the hardest problem a DV monitor faces.
- Related topic
Write Recovery (tWR)
The bus is free, the controller owes nothing, and the bank still cannot be closed. A precharge issued too early does not delay the write — it interferes with data still being driven into cells.
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.
