DDR · Module 9
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.
Module 7 established what operation a command requests. Module 8 established what location its operands identify. Both stopped at the interface.
This module asks what happens on the other side of it:
When a request arrives at a bank, what does the bank's current row state do to the outcome?
That question only has meaning because a DRAM bank is not a function from address to data. It holds state. A request for row 500 is cheap, expensive, or impossible depending entirely on what the bank was doing when the request arrived — and nothing in the address or the command says which.
So the module starts where that state comes from, and this chapter's question is:
What does it actually mean to open a row?
Not "ACTIVATE opens a row," which is a restatement. What changes, where, and over what interval.
1. What "Open" Actually Names
A bank is open when one row's contents are resolved and held in the bank's sense-amplifier structure, and the bank knows which row.
Three parts, and each rules something out.
"Resolved and held" — not "being read." Chapter 2.4 established that sensing is amplification of a tiny difference, and Chapter 2.5 that the amplified values must be driven back into the cells. When both have happened, the row's values exist at full logic levels in circuitry the bank owns. That persistence is the whole point.
"One row" — a bank has one sense-amplifier structure per bitline, so it can hold exactly one row's worth of values at a time. Chapter 5.2 derived this as the reason banks exist at all: N banks hold N open rows.
"And the bank knows which row" — the row address is no longer a parameter of anything. It has become state. Chapter 8.2 built an entire chapter on the consequence: a column command carries no row, because the row is already selected.
2. The Causal Chain
The physics is owned elsewhere; what this chapter needs is the chain of causation from a command to a state, so that the state is understood as a consequence rather than a convention.
Read the fourth arrow. The sense amplifiers drive values back into the cells. That is restoration, and it is the step that makes the whole scheme viable: reading destroyed the stored charge, and restoration puts it back. A row is not open until restoration has happened, because a bank whose cells have been disturbed but not restored is not in a state anything may rely on.
And read the last arrow carefully, because it is the one this module is about. The bank reports nothing. There is no signal that says OPEN(R). The controller knows the bank is open because it issued the activate and tracked the interval — which is §8's subject and the single most consequential fact in the module.
Three levels, kept apart for the rest of the module:
| Level | Example | Where it lives |
|---|---|---|
| A — Physical mechanism | charge sharing, amplification, restore, bitline equalisation | prose and diagrams, owned by Modules 2 and 3 |
| B — Architectural state | closed / open(R), one row per bank | prose, diagrams and RTL as state |
| C — Controller model | tracked open row, request classification, transition planning | RTL, this module |
No RTL in this module models level A. Not one register represents a charge, a voltage or an amplifier. That boundary is stated in every block's header and is the difference between a model that teaches and a model that misleads.
3. Opening Is Not Instantaneous
Here is the step that a two-state model cannot express, and it is why this module needs its own state object.
Chapter 5.2's state table has two states: a bank is open or it is not. That is exactly right for its purpose — it answers which row does this bank hold — and Module 8 leaned on it throughout.
But between "closed" and "open(R)" there is an interval. The wordline has been asserted, the cells are connected, the amplifiers have not yet resolved. Chapter 3.5 §6 named this precisely: selected is not usable. During that interval the bank is neither closed nor usefully open, and a column command issued into it has no defined meaning.
The same is true in reverse. A precharge does not instantaneously return a bank to closed; §4 covers what it does.
So the module uses four states:
BANK_CLOSED no row held; the bank will accept an activate
BANK_OPENING an activate has been accepted; the row is not yet usable
BANK_OPEN one row is resolved and held; column access is meaningful
BANK_CLOSING a precharge has been accepted; the row is being released4. Precharge, at the Right Level
Closing deserves its own paragraph because it is the most misunderstood operation in DRAM.
Architecturally, a precharge relinquishes the bank's current open-row state and returns the bank toward the condition another activate requires.
Physically, and at the level of intuition rather than circuit detail: the wordline is deasserted so the row's cells are disconnected from the bitlines, and the bitline pair is returned toward the balanced condition that sensing needs. Chapter 2.4 §4 explained why that balance matters — the signal a sense amplifier resolves is a small difference, so the starting point has to be a known, equal one. A bank cannot sense a new row against bitlines still carrying the last one.
What precharge is not, and this is Chapter 7.4 §2's point: it is not a write-back. Nothing is saved anywhere. The row's values were already restored into their cells during the activate, by the mechanism in §2. A precharge discards no data and preserves none — the data was never in danger. The cache analogy that makes people expect an eviction is exactly wrong here, and Chapter 9.2 takes that analogy apart properly.
So the cost of a precharge is not data movement. It is time, and the loss of the open row — which means the next access to a different row in that bank must pay for an activate again. That is the whole economics of Chapters 9.3 to 9.6.
5. RTL — The Multi-Bank Transition Model
The engineering problem
Hold, for every bank independently, which of the four states it is in and which row it concerns — and reject commands the current state does not permit, per bank, without letting one bank's activity disturb another's.
Why hardware needs it
A controller cannot ask a device what row is open. Its model is the only thing it has, so the model must be correct, must be per bank, and must reject illegal transitions rather than silently absorbing them.
Classification
SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL. It models level B and C only.
What it models
Per-bank four-state row context; acceptance or rejection of ACTIVATE and PRECHARGE against that state; the row identifier associated with the current state; and bank isolation.
What it does NOT model
Timing of any kind (Modules 13, 14). Column commands and data (Modules 10 to 12). Scheduling, queueing or arbitration (Module 17). Command encoding (Chapter 7.1) — commands arrive already decoded and qualified. Refresh (Module 15). All-bank precharge scope (Chapter 7.4 §3) — this block takes one bank at a time deliberately, so that bank isolation is provable.
Interface and parameter contract
// ─────────────────────────────────────────────────────────────────────────
// bank_row_fsm
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
//
// MODELS: per-bank ROW CONTEXT as four architectural states, and the
// legality of ACTIVATE / PRECHARGE against that state.
//
// DOES NOT MODEL ANY PHYSICAL OR ANALOG BEHAVIOUR. No signal here
// represents a charge, a bitline voltage, a sense amplifier or a
// transistor. Modules 2 and 3 own the physics in prose and diagrams.
//
// activate_done / precharge_done are ABSTRACT COMPLETION EVENTS. They are
// NOT DDR pins and NOT a JEDEC handshake. A real controller infers
// completion by counting against timing parameters, which Modules 13 and
// 14 own. They are inputs here so the transitional states can be modelled
// WITHOUT inventing a timing number.
//
// RELATION TO EXISTING BLOCKS: Chapter 3.2's row_state_tracker is one bank
// with four states and counted waits. Chapter 5.2's ddr_bank_state_table
// is many banks with two states. This is many banks with four states --
// the combination neither provides, and the one Chapter 9.5 requires.
//
// GENERATION: GENERATION-NEUTRAL ARCHITECTURAL MODEL.
// ─────────────────────────────────────────────────────────────────────────
// Four architectural states. Level B of Chapter 9.1 Section 2.
typedef enum logic [1:0] {
BANK_CLOSED = 2'd0, // no row held; an activate may be accepted
BANK_OPENING = 2'd1, // activate accepted; row NOT yet usable (3.5 §6)
BANK_OPEN = 2'd2, // one row resolved and held; column access valid
BANK_CLOSING = 2'd3 // precharge accepted; row being released
} bank_row_state_e;
module bank_row_fsm #(
parameter int NUM_BANKS = 4,
parameter int ROW_W = 16,
// Guarded so NUM_BANKS == 1 gives a 1-bit index rather than the illegal
// zero-width one $clog2(1) would produce.
parameter int BA_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS)
) (
input logic clk,
input logic rst_n,
// ── Semantic commands, already decoded and qualified (Module 7). One at
// a time: a device receives one command per command event, and
// accepting two here would model something no interface can present.
input logic act_req,
input logic pre_req,
input logic [BA_W-1:0] cmd_bank,
input logic [ROW_W-1:0] act_row,
// ── Abstract completion events. See the header.
input logic activate_done,
input logic precharge_done,
input logic [BA_W-1:0] done_bank,
// ── The state this block exists to hold.
output bank_row_state_e state_q [NUM_BANKS],
// The row this bank's state CONCERNS: a target while opening, the held
// row while open. Deliberately NOT called open_row: it is only an OPEN
// row when the state says so, and row_valid says when.
output logic [ROW_W-1:0] row_q [NUM_BANKS],
output logic [NUM_BANKS-1:0] row_valid,
output logic [NUM_BANKS-1:0] bank_open,
// In a transitional state. Chapter 9.5's subject is what arrives here.
output logic [NUM_BANKS-1:0] bank_busy,
// ── Rejections, reported per command rather than absorbed. A controller
// that silently drops an illegal transition has a model that diverges
// from the device with nothing to show for it.
output logic cmd_rejected,
// 0 none, 1 ACT to a bank not closed, 2 PRE to a bank not open,
// 3 invalid bank index, 4 completion event with no matching transition.
output logic [2:0] reject_reason,
output logic bank_index_invalid
);
// ── Elaboration legality.
if (NUM_BANKS < 1) begin : g_nb
initial $fatal(1, "bank_row_fsm: NUM_BANKS must be >= 1");
end
if (ROW_W < 1) begin : g_rw
initial $fatal(1, "bank_row_fsm: ROW_W must be >= 1");
end
// ── Index legality. NUM_BANKS need not be a power of two, so cmd_bank
// can name a bank that does not exist.
//
// NOTE THE SHAPE OF THIS CHECK. Writing (cmd_bank >= BA_W'(NUM_BANKS))
// is the bug it looks like a fix for: for a power-of-two NUM_BANKS the
// cast truncates to zero and the comparison is permanently false, so
// every index would pass. The generate arm removes the comparison
// entirely in the case where it cannot fire.
logic cmd_idx_bad, done_idx_bad;
if (NUM_BANKS >= (1 << BA_W)) begin : g_idx_full
// The index space is exactly covered; no index can be invalid.
assign cmd_idx_bad = 1'b0;
assign done_idx_bad = 1'b0;
end else begin : g_idx_partial
assign cmd_idx_bad = ({1'b0, cmd_bank} >= (BA_W+1)'(NUM_BANKS));
assign done_idx_bad = ({1'b0, done_bank} >= (BA_W+1)'(NUM_BANKS));
end
assign bank_index_invalid = ((act_req || pre_req) && cmd_idx_bad)
|| ((activate_done || precharge_done) && done_idx_bad);
// ── Per-command legality against the addressed bank's state.
bank_row_state_e cmd_state, done_state;
always_comb begin
// Default so that an invalid index never indexes the arrays.
cmd_state = BANK_CLOSED;
done_state = BANK_CLOSED;
if (!cmd_idx_bad) cmd_state = state_q[cmd_bank];
if (!done_idx_bad) done_state = state_q[done_bank];
end
logic act_ok, pre_ok, act_done_ok, pre_done_ok;
// An activate is admissible ONLY from closed. Not from OPEN -- a bank
// already holding a row must be closed first, which is Chapter 9.5's
// whole subject. Not from a transitional state -- there is already a
// transition in flight, which is Chapter 9.5's other subject.
assign act_ok = act_req && !cmd_idx_bad && (cmd_state == BANK_CLOSED);
assign pre_ok = pre_req && !cmd_idx_bad && (cmd_state == BANK_OPEN);
assign act_done_ok = activate_done && !done_idx_bad && (done_state == BANK_OPENING);
assign pre_done_ok = precharge_done && !done_idx_bad && (done_state == BANK_CLOSING);
always_comb begin
cmd_rejected = 1'b0;
reject_reason = 3'd0;
if ((act_req || pre_req) && cmd_idx_bad) begin
cmd_rejected = 1'b1;
reject_reason = 3'd3;
end else if (act_req && !act_ok) begin
cmd_rejected = 1'b1;
reject_reason = 3'd1;
end else if (pre_req && !pre_ok) begin
cmd_rejected = 1'b1;
reject_reason = 3'd2;
end else if ((activate_done && !act_done_ok)
|| (precharge_done && !pre_done_ok)) begin
cmd_rejected = 1'b1;
reject_reason = 3'd4;
end
end
// ── State. One independent element per bank: the loop is what makes
// bank isolation structural rather than a property to be hoped for.
always_ff @(posedge clk) begin
if (!rst_n) begin
for (int b = 0; b < NUM_BANKS; b++) begin
state_q[b] <= BANK_CLOSED;
// Reset to zero and, crucially, row_valid is derived from the
// state -- so this zero is never mistaken for an open row.
row_q[b] <= '0;
end
end else begin
// Command acceptance. The row is latched HERE, at acceptance, but
// the state does not become OPEN until completion below. That gap
// is the chapter.
if (act_ok) begin
state_q[cmd_bank] <= BANK_OPENING;
row_q[cmd_bank] <= act_row;
end
if (pre_ok) begin
state_q[cmd_bank] <= BANK_CLOSING;
// row_q is deliberately NOT cleared: it stays as evidence of what
// was open, which Chapter 9.2's monitor and Section 9's debugging
// both use. row_valid already says it is not an open row.
end
// Completion. THE TRANSITION HAPPENS HERE, not at the request.
if (act_done_ok) state_q[done_bank] <= BANK_OPEN;
if (pre_done_ok) state_q[done_bank] <= BANK_CLOSED;
end
end
// ── Derived views.
always_comb begin
for (int b = 0; b < NUM_BANKS; b++) begin
bank_open[b] = (state_q[b] == BANK_OPEN);
bank_busy[b] = (state_q[b] == BANK_OPENING) || (state_q[b] == BANK_CLOSING);
row_valid[b] = (state_q[b] == BANK_OPEN);
end
end
endmoduleState representation and transitions
One enum and one row register per bank. The transition graph is deliberately minimal:
CLOSED --act_ok--> OPENING
OPENING --activate_done--> OPEN
OPEN --pre_ok--> CLOSING
CLOSING --precharge_done-> CLOSEDThere are no other edges, and the absences are the design. There is no OPEN --act--> OPENING, because a bank holding a row cannot open another without closing first. There is no OPENING --act--> OPENING, because a transition is already in flight. Both attempts are rejected with a reason rather than queued — queueing is Module 17's.
Combinational behaviour
Index legality, a state lookup per addressed bank, four admissibility terms, a priority chain producing one reject reason, and three derived per-bank views.
The always_comb that reads state_q[cmd_bank] assigns a default first. That is not stylistic: with a non-power-of-two NUM_BANKS, cmd_bank can exceed the array, and indexing it would be undefined. Defaulting and then guarding the read makes the illegal index produce a rejection instead of a lookup.
Sequential behaviour
Nonblocking assignments only. Acceptance and completion are separate if statements in the same clocked block, so a completion for one bank and an acceptance for another commit in the same cycle without interacting — which is bank independence, and P4 proves it.
Note the ordering subtlety: if act_ok and act_done_ok ever addressed the same bank in the same cycle, the later assignment would win. They cannot: act_ok requires that bank to be CLOSED and act_done_ok requires it to be OPENING, and a bank is in one state.
Reset behaviour
Every bank resets to BANK_CLOSED, with row_q zeroed and row_valid low.
The zero in row_q is safe only because row_valid is derived from the state rather than stored. A model that reset row_q to zero and left a separate valid flag set would claim, after reset, that every bank holds row 0 — which is Chapter 7.4 §4's hazard, and §9's second debugging case.
A controller may legitimately reset to CLOSED because it knows it has issued nothing. A monitor may not, and Chapter 9.2 explains why the same state means different things to the two components.
Cycle-by-cycle example
NUM_BANKS = 4, ROW_W = 16. Bank 1 opens row 0x0104, then a second activate is attempted while it is still opening:
| Cycle | Input | state_q[1] after | row_valid[1] | Reject |
|---|---|---|---|---|
| 0 | — | CLOSED | 0 | — |
| 1 | act_req, bank 1, row 0x0104 | OPENING | 0 | — |
| 2 | act_req, bank 1, row 0x0105 | OPENING | 0 | reason 1 |
| 3 | activate_done, bank 1 | OPEN | 1 | — |
| 4 | act_req, bank 1, row 0x0105 | OPEN | 1 | reason 1 |
| 5 | pre_req, bank 1 | CLOSING | 0 | — |
| 6 | precharge_done, bank 1 | CLOSED | 0 | — |
Cycles 2 and 4 are both reason 1, and they are different bugs. Cycle 2 is an activate into an in-flight transition; cycle 4 is an activate into a bank that already holds a row. Chapter 9.5 separates them, and the fact that one reason code covers both is a stated limitation below.
How to simulate, and expected output
Drive the sequence above and check state_q[1], row_valid[1] and the reject outputs each cycle. Then the cases that matter more than the happy path:
A completion event with no matching transition — activate_done while the bank is CLOSED — must produce reject_reason == 4 and must not change the state. This is the one that catches a controller counting the wrong interval.
A command to bank 2 while bank 1 is opening must be accepted, and bank 1's state must not move. That is P4.
An invalid bank index with NUM_BANKS = 3 and cmd_bank = 3 must produce reason 3 and mutate nothing.
Reset asserted mid-transition, from OPENING, must give CLOSED with row_valid low.
Expected waveform
§6. The shape to look for is row_valid rising one cycle after activate_done, not with the activate — and staying low for the whole of OPENING.
Synthesis implications
NUM_BANKS state elements of 2 bits plus NUM_BANKS × ROW_W row bits, a NUM_BANKS-to-1 multiplexer per state lookup, and a small priority chain. The row registers dominate the area, which is the honest reason a real controller stores the open row once per bank and nothing more: at 16 banks and a 17-bit row that is under 300 flops, and it is the cheapest useful thing a memory controller owns.
Corner cases
NUM_BANKS == 1 gives BA_W == 1 through the guard, and since 1 < (1 << 1), the g_idx_partial arm is selected and index 1 is correctly reported invalid. Non-power-of-two NUM_BANKS selects the same arm and is the only configuration where reason 3 can fire — a regression elaborating only powers of two never exercises it. NUM_BANKS == 4 selects g_idx_full, so the comparison is removed rather than being permanently false. ROW_W == 1 is legal. Simultaneous act_req and pre_req to the same bank cannot both be admissible, since they require different states; if both are asserted the priority chain reports the activate's rejection first, which is a stated simplification.
Failure modes and debugging clues
reject_reason == 4 recurring means the completion events are not aligned with the transitions — usually a controller counting from the wrong event. row_valid never rising means completions are not arriving at all. row_q changing while a bank is OPEN means something is writing the row outside an accepted activate, which P3 catches. A bank's state moving when a command named a different bank is an indexing fault, which P4 catches.
Limitations
One command per cycle. One reject reason at a time, and reason 1 conflates "already open" with "busy" — deliberately, because Chapter 9.5 is where that distinction earns its own outputs. No all-bank precharge. No timing, so the model cannot say whether a completion arrived legally, only whether it arrived coherently. And it is a model: it proves the controller self-consistent, never that it agrees with a device. That comparison is Chapter 9.2's.
6. Opening, in Cycles
bank_row_fsm — acceptance, interval, completion
10 cyclesCycles 2 to 3 are the interval this chapter exists to make visible. The bank has accepted an activate. row_q[1] already holds 0x0104 — the target is known. And row_valid[1] is low, because the row is not yet resolved and held. A column command issued here has no defined meaning, which is Chapter 3.5 §6's selected is not usable as controller state.
Cycle 3's rejection is the second activate. The bank is already opening; a second activate is not queued, not merged, and not silently dropped — it is rejected with a reason.
Cycle 4 is the transition. activate_done arrived at cycle 3, and the state is OPEN at cycle 4 with row_valid asserting. The state changed on completion, not on the command — which is §8's rule.
Cycles 6 onward show row_q[1] retaining 0x0104 after the precharge. That is deliberate. The row register is not cleared, because what was last open is useful evidence for a monitor and for debugging, and row_valid already says it is not an open row. A model that cleared it would destroy the only record of what the bank had been doing.
REPRESENTATIVE EDUCATIONAL STATE TRANSITIONS. The one-cycle spacing between a request and its completion event is a reading convenience. It corresponds to no DDR timing parameter, and real minimum intervals are Modules 13 and 14'.
7. Four Assertions Worth Writing
// ─────────────────────────────────────────────────────────────────────────
// NOTE THE GENERATE WRAPPER. A property is an expression, not a procedural
// block, so a `for` loop cannot appear inside one. Per-bank properties are
// ELABORATED per bank with a genvar, which also means a failure names the
// bank in its instance path instead of hiding it inside one property.
// ─────────────────────────────────────────────────────────────────────────
for (genvar b = 0; b < NUM_BANKS; b++) begin : g_bank_asrt
// P1 -- a bank cannot arrive at a valid open row without having been in
// a transition. This is the stale-state hazard as a contract: a model
// that resets row_q to zero and keeps a STORED valid flag would claim
// row 0 is open in every bank, and would fail here on the first cycle.
property p_valid_row_needs_a_transition;
@(posedge clk) disable iff (!rst_n)
row_valid[b] |-> ($past(state_q[b]) == BANK_OPENING)
|| ($past(state_q[b]) == BANK_OPEN);
endproperty
assert property (p_valid_row_needs_a_transition);
// P2 -- the transition happens on COMPLETION, not on the request. Stated
// as the direction that can actually fail: a bank cannot ENTER OPEN
// unless the previous cycle was OPENING with a matching completion
// event. Deliberately NOT "a request leads to OPEN", which would import
// a timing assumption this module refuses to make.
property p_open_requires_completion;
@(posedge clk) disable iff (!rst_n)
(state_q[b] == BANK_OPEN) && ($past(state_q[b]) != BANK_OPEN)
|-> ($past(state_q[b]) == BANK_OPENING)
&& $past(activate_done)
&& ($past(done_bank) == BA_W'(b));
endproperty
assert property (p_open_requires_completion);
// P3 -- the held row is stable while the bank stays OPEN. A row that
// changes under an open bank means state is being written outside an
// accepted activate, and every column access after that point resolves
// against a row the device does not hold.
property p_open_row_is_stable;
@(posedge clk) disable iff (!rst_n)
(state_q[b] == BANK_OPEN) && ($past(state_q[b]) == BANK_OPEN)
|-> (row_q[b] == $past(row_q[b]));
endproperty
assert property (p_open_row_is_stable);
// P4 -- BANK ISOLATION. A bank named by neither the command nor the
// completion event cannot change state. This catches the indexing fault
// in Section 9, and it is Chapter 5.2's architectural claim made
// checkable.
property p_unaddressed_bank_untouched;
@(posedge clk) disable iff (!rst_n)
(!((act_req || pre_req) && (cmd_bank == BA_W'(b)))
&& !((activate_done || precharge_done) && (done_bank == BA_W'(b))))
|=> (state_q[b] == $past(state_q[b]))
&& (row_q[b] == $past(row_q[b]));
endproperty
assert property (p_unaddressed_bank_untouched);
endWhat these prove. P1 makes row_q safe to consume: it forbids a bank from claiming a valid open row without having passed through a transition, which is exactly what a stored validity flag gets wrong after reset. P2 is the chapter's central rule as a checkable contract, and note how it is written: not "a request leads to OPEN," which would import timing, but "arriving in OPEN requires a completion from OPENING" — which is timing-free and still catches a model that transitions early. P3 catches out-of-band state writes. P4 is the most valuable of the four, because a bank-indexing fault corrupts a bank nobody was looking at, and it is the only property here that would catch it.
What they do not prove. Nothing here says the model agrees with a device. Every property is about internal coherence; activate_done is an input, so P2 proves the model responds correctly to completion events, not that those events were generated at the right moment — that requires the timing parameters in Modules 13 and 14, and no assertion in this module can substitute for them. Nothing proves any physical claim: no property here is about charge, sensing, restoration or bitline state, and the block contains no representation of them. And nothing proves a column access issued during BANK_OPEN is legal — state legality is necessary and not sufficient, which Chapter 7.3 established and Modules 13 and 14 complete.
8. DV — Transition on Completion, Not on Request
This section is short and it is the most important paragraph in the module for a verification engineer.
A monitor observing the command interface sees requests. It does not see completions.
An ACT bank 1, row 0x0104 crosses the interface. Nothing crosses it later to say the row is now held. So a monitor building a row-context model has to choose when to apply the transition, and the two available choices are both wrong in different ways:
Apply it at the request. The model then claims the bank is open during the interval when it is not, and a column command issued too early looks legal to the checker. The bug the monitor exists to find is the bug it cannot see.
Apply it after a fixed delay. The model is now a timing model, and it is only as right as the number in it — which is a Module 13/14 figure that varies by device and speed bin.
The resolution used throughout this module is to model the transitional state explicitly and treat completion as a separate event. A monitor then has three honest answers instead of two dishonest ones: closed, open with row R, and "in transition — I do not yet know." That third answer is what Chapter 7.4 §4's known bits were protecting, and Chapter 9.2 builds the component that maintains it.
Which gives the rule: a checker that gates column accesses on bank_open is only as sound as the moment it decides bank_open became true. Get that moment from a modelled transition, never from the command that requested it.
9. Debugging — The Row That Never Opened
Symptom. A column access returns data that matches no recent write. The command trace looks correct: an activate for the right bank and the right row precedes the read, and the operands are right.
Candidate mechanisms.
- The column access was issued during
OPENING— the state transition had not completed when the controller believed it had. - The activate was rejected and the controller did not notice, so no row was ever opened.
- The completion event fired for the wrong bank, so a different bank moved to
OPENand the addressed one never did. row_qwas latched from a staleact_row, so the wrong row opened — which is Chapter 7.1 §9's case, reached from a different direction.- The model is right and the device disagrees with it — Chapter 9.2's divergence.
Evidence to collect. For the failing access: the bank's state_q and row_valid at the cycle the column command was issued, the preceding activate and its acceptance, cmd_rejected and reject_reason for every cycle since that activate, and the done_bank of every completion event in the window.
Discriminator.
- Was
row_validlow at issue? If yes, mechanism 1, and you are done — the controller's notion of when opening finished is wrong. This is the single most useful check and it needs one signal at one cycle. - Did
cmd_rejectedassert withreason 1at the activate? Mechanism 2. The bank was not closed, so the activate never took effect — and a controller ignoring rejections will do this repeatedly and never recover. - Compare
done_bankagainst the activate'scmd_bank. A mismatch is mechanism 3, and its signature is distinctive: a bank nobody addressed is inOPEN, which P4 catches automatically if enabled. - Compare
row_qagainst the row the address map produced. A mismatch with everything else correct is mechanism 4, and the fault is upstream in Module 8. - If the state, the row and the timing all check out, it is mechanism 5 and the next step is the two-model comparison in Chapter 9.2.
Responsible layer. Mechanism 1 is the controller's completion tracking — level C, and ultimately a Module 13/14 parameter applied wrongly. Mechanism 2 is the controller ignoring its own error output. Mechanism 3 is an indexing fault, level C. Mechanism 4 is Module 8's address map. None of them is a physical DRAM fault, and that matters: the temptation with data corruption is to suspect the array, and in every case above the array did exactly what it was told.
Fix. For 1, correct the completion interval and add P2. For 2, make rejections fatal in simulation rather than merely reported. For 3, enable P4. For 4, Chapter 8.6's round trip.
10. Common Misconceptions
"ACTIVATE transfers data."
Why it is tempting: it is the first command in every read sequence, and it names a row, so it feels like the read.
Concrete failure: an engineer expects data on the bus after an activate, or writes a monitor that attributes returned data to the activate rather than to the column command that actually requested it.
Correct model: an activate changes state and moves nothing. §1. The data bus is not involved at all.
Prevention: describe activate only as a transition. If a sentence about activate mentions data, it is wrong.
"ACTIVATE reads one column."
Why it is tempting: a column is what an access eventually wants, so activate feels like it should fetch one.
Concrete failure: a model that opens a row and captures one column's worth of data cannot explain why a second column access to the same row needs no new activate.
Correct model: an activate resolves the whole row into the sense amplifiers. Chapter 2.5 §2 covered why the whole row participates whether you wanted it or not.
Prevention: Chapter 9.2, which is about what the whole row being present makes possible.
"A row opens instantly."
Why it is tempting: in a two-state model it does, and Chapter 5.2's table — correctly, for its purpose — transitions in one cycle.
Concrete failure: a column command issued in the interval between the activate and the row actually being held. The command is well-formed, the controller believes the bank is open, and the result is undefined.
Correct model: four states. OPENING is a real state in which the bank is neither closed nor usable.
Prevention: gate column access on a modelled transition, never on the activate. §8.
"A row miss means the data is missing."
Why it is tempting: "miss" is borrowed from caches, where a miss means the data is elsewhere.
Concrete failure: an engineer looks for a backing store that a DRAM row miss fetches from, and cannot find one — because there is none.
Correct model: every row is always present in the array. A miss means the row is not currently resolved and held, so an activate is needed. Nothing is fetched from anywhere else. Chapter 9.4 develops this.
Prevention: say "not currently open" rather than "not present."
"Precharge writes the row back, like a cache eviction."
Why it is tempting: the cache analogy is running, and evictions write back.
Concrete failure: a performance 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 written.
Correct model: the row was restored into its cells during the activate, by the mechanism in §2. A precharge moves no data and saves nothing. Chapter 7.4 §2.
Prevention: remember that restoration is part of opening, not of closing. That single fact removes the whole misconception.
"The controller can ask the DRAM which row is open."
Why it is tempting: every other subsystem an engineer has met can be queried.
Concrete failure: a debug plan that assumes the open row can be read back, and a verification environment with no row-context model because it expected to observe one.
Correct model: there is no such query and no such signal. The controller knows because it tracked what it issued. Its model is the only source of truth it has, and when the model is wrong the controller is confidently wrong.
Prevention: treat the row-context model as a first-class design object with its own assertions — which is what this chapter's P1 to P4 are.
"The RTL bank-state model is the physical DRAM."
Why it is tempting: it has the right state names and behaves plausibly.
Concrete failure: an engineer expects the model to catch a physical or timing violation, and it cannot — it has no representation of either.
Correct model: it is level B and C only. No signal in it represents a charge, a voltage, or an amplifier, and its header says so.
Prevention: read the classification block before assigning a block a capability.
11. Interview Reasoning
"What exactly changes when ACTIVATE occurs?"
The bank's state, and nothing else. Before, the bank holds no row; after, one row's values are resolved and held in its sense-amplifier structure and the bank is associated with that row identifier. No data crosses the interface, no column is named, and nothing is returned. The row address has become state — which is why a later column command carries no row, and the reason a read and an activate are separate commands at all.
"A colleague says a row opens the moment the activate is accepted. What is wrong with that?"
It collapses a real interval. Sensing amplifies a small difference and the amplified values must be restored into the cells; until that has happened the row is selected but not usable. So there is a state in which the bank is neither closed nor open, and a column command issued into it is undefined. The practical consequence is a verification one: a checker that marks the bank open at the activate will pass exactly the bug it was written to catch, because the too-early column access looks legal to it.
"How would a controller know which row is open?"
Because it issued the activate and tracked the interval. There is no query and no status signal — the controller's own model is its only source of truth. That is why the model deserves assertions of its own: when it is wrong, nothing contradicts it, and the controller issues legal-looking commands against a state the device is not in. It is also why the model must be per bank and must transition on a modelled completion rather than on the command.
"Why is a precharge not a write-back?"
Because the data was never at risk. Reading a DRAM cell is destructive, so the sense amplifiers drive the resolved values back into the cells as part of the activate — restoration is a step of opening, not of closing. By the time a precharge happens, the cells already hold correct values. A precharge therefore moves no data; it relinquishes the open-row state and returns the bitlines toward the balanced condition the next sensing needs. The cost is time and the loss of the open row, not data movement.
"What bug would make a bank nobody addressed change state?"
A bank-indexing fault — a command or a completion event applied to the wrong array element. It is worth naming because of how it presents: the corruption appears in a bank the failing transaction never mentioned, so a trace filtered to the failing address contains no evidence at all. The property that catches it says that a bank named by neither the command nor the completion this cycle cannot change, and it is cheap to write and easy to forget.
12. Engineering Exercise
NUM_BANKS = 4, ROW_W = 16. All banks start CLOSED.
1. Trace state_q and row_valid for all four banks through this sequence, one event per cycle: ACT b1 r100 · ACT b2 r200 · activate_done b1 · activate_done b2 · PRE b1 · ACT b1 r300 · precharge_done b1 · ACT b1 r300.
2. At which cycles above is a column access to bank 1 meaningful?
3. Which event in the sequence is rejected, and with what reason?
4. A colleague changes row_valid[b] to be a register set at act_ok instead of derived from the state. Give the exact failing scenario.
5. Write the property that catches a completion event applied to the wrong bank.
6. Explain why row_q is not cleared on precharge, and name one thing that would break if it were.
13. Summary
A bank is open when one row's values are resolved and held in its sense-amplifier structure and the bank is associated with that row. Not being read — held. That persistence is what everything in this module trades on.
ACTIVATE transfers no data. Its entire effect is a state change, which is why it names no column and returns nothing, and why the row address becomes state rather than remaining a parameter.
Opening is an interval, not an event. Between closed and open there is a state in which the row is selected but not usable, and the same is true in reverse for closing. A two-state model cannot express it — which is why this module uses four, and why Chapter 9.5 is possible at all.
The transition commits on completion, not on request. A monitor that applies it at the command is open during the interval when the device is not, and it will pass the too-early column access it was written to catch.
Precharge is not a write-back. Restoration happened during the activate; a precharge moves no data and saves nothing. Its cost is time and the loss of the open row.
And there is no query. No signal reports the open row, so a controller's model is its only source of truth — which makes the model a first-class design object, per bank, with assertions of its own. Bank isolation is the one most likely to be omitted and the only one that catches a fault in a bank nobody addressed.
14. What Comes Next
This chapter established that after an activate the bank holds a row. Chapter 9.2 — The Row Buffer asks what that held row actually is.
It is commonly called a row buffer, and the name invites an analogy to a cache that is useful for about one sentence and misleading after that. There is no separate memory there — no SRAM, no tags, no backing store to miss to. There is a sense-amplifier structure holding one row, and a controller holding metadata about it.
The chapter separates the two, and then builds the component this chapter's §8 argued for: a monitor that maintains a row-context model from observed commands and detects when it has diverged from the controller's. Because the controller's model being the only source of truth is fine right up until it is wrong.
Return to Activate for the command and its operands, Banks for the bank and its two-state table, Rows for what a row physically is, Sense Amplifiers for selected is not usable, Restore Operations for the step that makes opening safe, and Precharge for the command that closes a bank.
Continue learning
Related tutorials
- Related topic
Activate (ACT)
ACT opens a row, and it is the right place to establish what a DDR command actually is: a sampled encoding, qualified for a target, carrying operands, interpreted against device state — four separate questions that must not be collapsed.
- Related topic
Write (WR / WRA)
A write's data arrives after its command, which makes three events impossible to conflate: observed, accepted, and completed. A monitor, a protocol checker and a scoreboard each attach to a different one.
- 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
Physical Mapping
Five chapters of fields assembled into one map, and then the two questions none of them could ask alone: is the decomposition lossless, and can a monitor invert it from what it actually observed?
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.
