DDR · Module 3
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.
Three chapters have now reached the same conclusion from different directions, and none of them resolved it.
Chapter 3.3 §5 found that bitline length trades density against signal: longer amortises the sensing circuitry and shrinks the signal. Chapter 3.4 §2 found that row width trades density against selection speed: wider amortises the decoder and driver and slows the conductor. Chapter 3.5 §5 found that sense-amplifier amortisation trades die area against both.
Each presented a knob with a good direction and a bad one, and each ended by pointing here. So this chapter asks the question they were all circling:
Why isn't a DRAM bank simply one enormous rectangular grid — and what does the answer do to addressing and to the state a controller must track?
The answer closes Module 3. Partition the array. Build many small local structures, each with short conductors and its own local sensing, and connect them with shared global structures. That escapes the choice all three chapters presented, because it amortises the periphery at two levels instead of one. And it has a consequence that reaches all the way to the top of the curriculum: hierarchical physical structure produces hierarchical addressing, and hierarchical addressing plus row state produces the classification every memory controller is built around.
1. Price the Flat Array First
The partitioned structure is much easier to accept once the alternative has been costed, so start there.
Imagine one rectangular grid holding a bank's worth of cells: one wordline spanning the full width, one bitline spanning the full height, one sense amplifier per column at the bottom. Every chapter in this module has already told you what goes wrong.
The bitlines would be enormous. A bitline spanning the full height carries every cell in its column (Chapter 3.3 §1), so its capacitance would be vast relative to a cell's. The charge-sharing signal would be correspondingly minuscule — and Chapter 3.5 §2 established that a smaller initial difference takes longer to regenerate and is more easily overtaken by noise. Beyond some length the signal stops being reliably resolvable at all.
The wordlines would be enormous. A wordline spanning the full width gates every cell in its row (Chapter 3.4 §2), so bringing the far end to a switching level would take a long time — and the whole row's selection is gated by its worst cell.
And a single access would engage the entire bank. One wordline assertion would connect every cell in a full-width row, requiring every column's sense amplifier to resolve and every cell to be restored. There would be exactly one row's worth of activity possible at a time across the whole structure.
So the flat array fails on three independent axes at once, and — this is the important part — it fails at large size specifically. At small size a flat array is perfectly good. What the flat design lacks is a way to grow without lengthening its conductors, and that is precisely what partitioning provides.
2. Partitioning: Escaping the Choice
Here is the move, and it is the most important structural idea in Module 3.
Instead of one large array, build many small local arrays. Each has short bitlines, short wordlines, and its own local sensing circuitry. Then add a level of structure above them: logic that selects which local array participates, and shared global paths that carry data to and from whichever one was selected.
Why this escapes the choice all three chapters presented. In the flat design there was one amortisation level, so making conductors shorter necessarily meant amortising the sensing across fewer cells. In the partitioned design there are two: local sensing is amortised across the cells of its own local array, and the global structures — the selection logic, the global data paths, the interface — are amortised across every local array in the bank.
So the design gets short conductors (good signal, fast selection) and affordable periphery (because the expensive global machinery is shared very widely). That is not a compromise between the two columns of Chapter 3.3 §5's table; it is a structure in which the table's trade applies only to the local array, which is deliberately kept small.
Two further benefits fall out, and they are not small.
Activity becomes localised. An access engages one local array and its sensing, not the whole bank. Everything else is idle — which matters for power, and which is the structural precondition for any form of parallelism between regions.
And the structure is repeatable. A bank is built by replicating a local array many times and adding one selection and distribution layer. Designing and characterising one small structure carefully, then replicating it, is a far better engineering proposition than designing one enormous unique structure — and it is why memory dies are as regular as they are, which in turn is why the redundancy and repair of Chapter 1.6 §2 are practical.
3. What the Levels Accomplish
With the terminology caveat in force, here are the levels this curriculum can honestly describe, each identified by what it does rather than by what it is called.
The cell holds one bit (Chapter 2.4).
The row is the set of cells one selection conductor connects — the unit of participation (Chapter 3.1).
The local array is the smallest structure with its own sensing circuitry. It bounds conductor length, and therefore it is what actually determines signal quality and selection speed. This is the level the trade-offs of 3.3, 3.4 and 3.5 apply to, and keeping it small is the whole point of partitioning.
The bank is a collection of local arrays sharing selection and distribution structures, and — critically for everything above — the level at which row state exists. One row open per bank is the rule the rest of the curriculum works with, because the bank is the scope within which the "one row at a time" exclusivity of Chapter 3.5 §5 is arbitrated.
The device contains multiple banks plus the interface, and is what a controller talks to.
Two honest qualifications about this figure.
The middle levels' names vary, as the opening callout said. What does not vary is that some level bounds conductor length and owns local sensing, and that some level is the scope of row-state exclusivity. Real devices may have more levels than five, and a level may be subdivided further.
And the bank is where this chapter stops. How many banks a device has, how they are grouped, how they are addressed and what parallelism they provide are Module 5's subject. This chapter establishes only that the bank is the scope of row state — because that is what the RTL in §5 needs and what later modules build on.
4. Hierarchical Structure Produces Hierarchical Addressing
Now the consequence that carries into every later module.
A flat array needs two coordinates: row and column (Chapter 3.2 §3). A partitioned structure needs more, because before a row can be selected the structure must know which local array and which bank are involved. Each level of physical partitioning adds a level of selection, and each level of selection needs an index.
So a device-internal address decomposes into a field per level:
| Field | Selects | Established in |
|---|---|---|
| bank | which bank participates | this chapter (Module 5 for detail) |
| local array | which local structure within the bank | §2, §3 |
| row | which selection conductor within it | 3.1 |
| column | which of the resolved values leaves | 3.2 |
The important claim is the correspondence, not the list. Every field exists because a physical selection exists, and the fields' widths follow from the structure's dimensions. Addressing is not an abstraction layered on top of the array — it is the array's shape, expressed as numbers.
And the same warning as Chapter 3.2 §5, now larger. Which bits of a system address map onto which of these fields is not determined by anything in this chapter. It is a policy with very large performance consequences — a map that sends consecutive addresses to different banks behaves completely differently from one that keeps them in a single row — and Module 18 owns it. The decomposition in §5 is deliberately educational and its field order is chosen for legibility, not correctness.
5. RTL — The Module's Synthesis Model
The problem being solved. Everything Module 3 built now has to work together. A request arrives with an address. Something must decompose it into the fields the hierarchy needs, compare the requested row against whatever row is currently open in that bank, and classify the request so a controller can act on it. That classification is the interface between the array's physics and every scheduler in the rest of the curriculum.
Abstraction level. Digital control, combining 3.1's row state, 3.2's decomposition and 3.5's readiness into one model.
What it models. Hierarchical address decomposition; per-bank open-row tracking; and the three-way classification of a request against the bank's current state.
What it deliberately does NOT model. No cell, conductor, sensing, charge or timing — all physics, all handled in prose and diagrams. No commands, no timing parameters, no scheduling policy: the model classifies and does not decide what to do, because deciding is a later module's subject and the distinction matters. No data path. No refresh integration. No bank parallelism beyond independent state per bank — what parallelism is usable is Module 5's question.
The three classifications, defined before any code:
ROW_CLOSED — no row is open in the target bank. The requested row must be opened before the access can proceed.
ROW_HIT — the requested row is the one already open in that bank. The access can use the existing row state.
ROW_CONFLICT — a different row is open in that bank. It must be closed, and the array returned to its ready condition, before the requested row can be opened.
Interface. A request with a device-internal address and a valid flag. Per-bank state updated by open and close events. Outputs: the decomposed fields, the classification, and a validity flag.
How to simulate it. As in 3.1 §5: vlog dram_array_frontend.sv tb_dram_array_frontend.sv then vsim -c tb_dram_array_frontend -do "run -all".
// ─────────────────────────────────────────────────────────────────────────
// DRAM ARRAY FRONT END. Classification: EDUCATIONAL DIGITAL MODEL.
//
// Module 3's synthesis model. Combines hierarchical address decomposition
// (§4), per-bank row state (3.1) and the three-way classification a
// controller needs.
//
// It is NOT a DRAM device, NOT a memory controller, and NOT a scheduler.
// It CLASSIFIES requests; it does not decide what to do about them, because
// that is scheduling POLICY and belongs to later modules. It models no
// cell, conductor, sensing, charge, command or timing.
//
// THE FIELD ORDER BELOW IS EDUCATIONAL. Which system address bits map to
// which field is Module 18's subject and a major performance decision --
// see §4 and the callout there.
// ─────────────────────────────────────────────────────────────────────────
module dram_array_frontend #(
parameter int BANKS = 4,
parameter int LOCAL_ARRAYS = 4,
parameter int ROWS = 16,
parameter int COLS = 8,
// DERIVED widths. Every guard keeps a dimension of 1 legal rather than
// producing a zero-width field, which would be an illegal part-select.
parameter int BANK_W = (BANKS <= 1) ? 1 : $clog2(BANKS),
parameter int LA_W = (LOCAL_ARRAYS <= 1) ? 1 : $clog2(LOCAL_ARRAYS),
parameter int ROW_W = (ROWS <= 1) ? 1 : $clog2(ROWS),
parameter int COL_W = (COLS <= 1) ? 1 : $clog2(COLS),
parameter int ADDR_W = BANK_W + LA_W + ROW_W + COL_W
) (
input logic clk,
input logic rst_n,
// ── Request ───────────────────────────────────────────────────────────
input logic req_valid,
input logic [ADDR_W-1:0] req_addr,
// ── Row-state events. Driven by whatever sequences the array (3.1, 3.5).
// The front end TRACKS state; it does not cause transitions.
input logic open_done, // a row finished opening
input logic [BANK_W-1:0] open_bank,
input logic [ROW_W-1:0] open_row,
input logic close_done, // a row finished closing
input logic [BANK_W-1:0] close_bank,
// ── Decomposed fields (§4) ────────────────────────────────────────────
output logic [BANK_W-1:0] bank_index,
output logic [LA_W-1:0] local_array_index,
output logic [ROW_W-1:0] row_index,
output logic [COL_W-1:0] col_index,
output logic addr_out_of_range,
// ── Classification ────────────────────────────────────────────────────
output logic [1:0] req_class,
output logic req_class_valid
);
// Classification encoding. Declared as a package-style localparam set so
// a consumer can reference the names rather than the numbers.
localparam logic [1:0] ROW_CLOSED = 2'd0;
localparam logic [1:0] ROW_HIT = 2'd1;
localparam logic [1:0] ROW_CONFLICT = 2'd2;
// ── COMPILE-TIME legality ─────────────────────────────────────────────
if (BANKS < 1 || LOCAL_ARRAYS < 1 || ROWS < 1 || COLS < 1) begin : g_bad
initial $fatal(1, "dram_array_frontend: all dimensions must be >= 1");
end
// ── Decomposition (§4). Field order is EDUCATIONAL: column lowest so
// consecutive addresses stay in one row, then row, then local array,
// then bank. A real map distributes system bits deliberately.
localparam int COL_LSB = 0;
localparam int ROW_LSB = COL_LSB + COL_W;
localparam int LA_LSB = ROW_LSB + ROW_W;
localparam int BNK_LSB = LA_LSB + LA_W;
assign col_index = req_addr[COL_LSB +: COL_W];
assign row_index = req_addr[ROW_LSB +: ROW_W];
assign local_array_index = req_addr[LA_LSB +: LA_W];
assign bank_index = req_addr[BNK_LSB +: BANK_W];
// Range checking per field. Each comparison is constant-folded away when
// its dimension is a power of two (3.2 §5), so this is free in the common
// case and the only protection against aliasing when it is not.
logic bank_bad, la_bad, row_bad, col_bad;
assign bank_bad = (BANKS < (1 << BANK_W)) && (bank_index >= BANK_W'(BANKS));
assign la_bad = (LOCAL_ARRAYS < (1 << LA_W)) && (local_array_index >= LA_W'(LOCAL_ARRAYS));
assign row_bad = (ROWS < (1 << ROW_W)) && (row_index >= ROW_W'(ROWS));
assign col_bad = (COLS < (1 << COL_W)) && (col_index >= COL_W'(COLS));
assign addr_out_of_range = bank_bad || la_bad || row_bad || col_bad;
// ── Per-bank row state (3.1), replicated. Row state is per BANK because
// the bank is the scope of the one-row-at-a-time exclusivity (§3).
logic bank_row_open [BANKS];
logic [ROW_W-1:0] bank_open_row [BANKS];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int b = 0; b < BANKS; b++) begin
bank_row_open[b] <= 1'b0;
// bank_open_row is NOT reset: it is meaningful only under
// bank_row_open, exactly as in 3.1 §6, and resetting it would cost
// a wide reset net for values nothing may read (1.2 §9).
end
end else begin
// Opening completes: record which row is now open in that bank.
if (open_done) begin
bank_row_open[open_bank] <= 1'b1;
bank_open_row[open_bank] <= open_row;
end
// Closing completes: that bank has no open row. Applied AFTER the
// open case so that a simultaneous open and close to DIFFERENT banks
// both take effect -- they index different array elements, so there
// is no conflict. A simultaneous open and close to the SAME bank is
// an illegal input combination; §7's properties make it visible
// rather than letting this module pick a winner silently.
if (close_done) begin
bank_row_open[close_bank] <= 1'b0;
end
end
end
// ── Classification. Pure function of the request and the target bank's
// state -- no policy, no decision, no side effects.
always_comb begin
req_class = ROW_CLOSED;
req_class_valid = 1'b0;
if (req_valid && !addr_out_of_range) begin
req_class_valid = 1'b1;
if (!bank_row_open[bank_index]) begin
req_class = ROW_CLOSED;
end else if (bank_open_row[bank_index] == row_index) begin
req_class = ROW_HIT;
end else begin
req_class = ROW_CONFLICT;
end
end
end
endmoduleCombinational decisions. Four part-selects, four range comparisons, and the three-way classification. The classification is deliberately a pure function of the request and the target bank's state — it reads state and produces a label, with no side effects and no policy.
Sequential updates. Per-bank open-row tracking, updated only by completion events. Note the module tracks state rather than causing transitions: opening and closing are driven by whatever sequences the array (3.1, 3.5), which keeps this model's responsibility single.
Cycle-by-cycle example. With the default parameters, COL_W is 3, ROW_W is 4, LA_W is 2, BANK_W is 2, so ADDR_W is 11. From reset, every bank reports ROW_CLOSED. Assert open_done with open_bank = 1, open_row = 5. Now a request whose bank field is 1 and row field is 5 classifies as ROW_HIT; one with bank 1 and row 9 classifies as ROW_CONFLICT; and one with bank 2, any row, still classifies as ROW_CLOSED — because state is per bank.
Simulation expectations. A directed test should confirm all three classifications for the same bank, independence of banks from one another, req_class_valid low whenever the request is invalid or out of range, and — for non-power-of-two dimensions — addr_out_of_range asserting for each field independently.
Synthesis implications. Part-selects are wiring. The range comparators vanish for power-of-two dimensions. The per-bank state is BANKS flags plus BANKS × ROW_W bits, and the classification is one comparison against the selected bank's stored row — so the row comparison's width scales with ROW_W and the bank state is indexed, which synthesises to a small multiplexer. All modest and fully synthesizable.
Corner cases, and why each guard exists. Any dimension of 1 gives a field width of 1 through its guard rather than a zero-width part-select. Any dimension of 0 is an elaboration error. Non-power-of-two dimensions make some field values name nothing, reported per field rather than aggregated — deliberately, because knowing which field is out of range is what makes a debug tractable. open_done and close_done to different banks in one cycle both apply, because they index different elements. open_done and close_done to the same bank in one cycle is an illegal input combination, and §7's properties surface it rather than this module silently picking an order.
Debugging observations. If a request classifies as ROW_HIT when it should conflict, compare bank_open_row[bank_index] against the request's row field — and check the field widths first, because a boundary error makes both look plausible (3.2 §9). If every request classifies ROW_CLOSED, check that open_done is reaching the module and that open_bank is the bank being requested. If a bank's state seems to follow another bank's activity, the bank field is almost certainly being taken from the wrong address bits.
Limitations. Classification without policy, deliberately. No timing, no commands, no data. One open row per bank, with no expression of what a real device permits. No refresh. And the field order is educational, as §4 and the code header both say.
6. Row Classification Is the Module's Payoff
The classification deserves its own section, because it is the conceptual bridge out of Module 3.
Access cost depends on state, and now the states have names:
| Classification | What must happen first | Relative cost |
|---|---|---|
ROW_HIT | nothing — the row is already open and resolved | cheapest |
ROW_CLOSED | open the requested row | intermediate |
ROW_CONFLICT | close the open row, ready the array, then open the requested one | most expensive |
Three things about this table matter more than the table itself.
It is derived, not asserted. Every row of it follows from mechanisms this curriculum has built: ROW_HIT is cheap because the sense amplifiers still hold the resolved values (Chapter 3.5 §4); ROW_CONFLICT is expensive because those amplifiers must be returned to their equalised condition before a new row's small signal can be measured against them (Chapter 3.3 §4) and because the open row must first be restored (Chapter 2.6). Nothing in the table is a convention.
The classification is per bank, because the bank is the scope of row-state exclusivity (§3). A conflict in one bank says nothing about another, which is exactly why having several banks is valuable — and Module 5 is where that value is developed.
And this is where Module 3 hands over to the controller modules. The classification is information, not a decision. What to do about a ROW_CONFLICT — serve it immediately, reorder other requests ahead of it, or leave rows open in the hope of future hits — is scheduling policy, and the whole of Modules 16 to 18 and 23 is about it. This chapter's contribution is that the policy has something well-defined to reason about.
dram_array_frontend — hit, conflict and closed against one bank
10 cyclesRead the figure as one request stream meeting three different states. The row field is 5 in cycles 0, 3, 4 and 8 — the same request — and it classifies as ROW_CLOSED, then ROW_HIT twice, then ROW_CLOSED again, purely because the bank's state changed underneath it. Cycle 5's request for row 12 is a ROW_CONFLICT against the very row that made cycles 3 and 4 hits.
That is the whole of "access cost depends on state", made concrete. Nothing about the requests changed. The classification — and therefore the work required — is a property of the pair of request and state, never of the request alone. It is also why a requester cannot predict its own memory latency, and why the counters §12 asks for exist.
7. Four Assertions Worth Writing
// VERIFICATION-ONLY, inside dram_array_frontend.
// P1 -- the decomposition is LOSSLESS across all four fields. As in 3.2 §6,
// this is the property that catches a field boundary off by one bit, which
// is otherwise invisible: every field looks plausible and one is wrong.
// With four fields there are three boundaries to get wrong.
property p_decomposition_lossless;
@(posedge clk) disable iff (!rst_n)
req_addr == {bank_index, local_array_index, row_index, col_index};
endproperty
assert property (p_decomposition_lossless);
// P2 -- the three classifications are MUTUALLY EXCLUSIVE and exhaustive
// when valid. A classification that could be ambiguous would make every
// downstream scheduling decision unsound, and nothing would report it.
property p_classification_well_formed;
@(posedge clk) disable iff (!rst_n)
req_class_valid |-> (req_class inside {ROW_CLOSED, ROW_HIT, ROW_CONFLICT});
endproperty
assert property (p_classification_well_formed);
// P3 -- ROW_HIT is claimed ONLY when the target bank genuinely holds the
// requested row. This is the property whose violation is most damaging: a
// false hit means a controller skips opening a row and accesses whatever
// the array actually holds.
property p_hit_requires_matching_open_row;
@(posedge clk) disable iff (!rst_n)
(req_class_valid && (req_class == ROW_HIT))
|-> (bank_row_open[bank_index] && (bank_open_row[bank_index] == row_index));
endproperty
assert property (p_hit_requires_matching_open_row);
// P4 -- bank state changes ONLY on a completion event for THAT bank.
// Catches a mis-decoded bank index on an open or close, which would corrupt
// one bank's state from another bank's activity and then produce false hits
// and false conflicts indefinitely.
//
// NOTE ON FORM: this MUST be a generate loop with one instance per bank. A
// single property cannot quantify over an array, and the tempting
// single-instance version -- checking only the bank the CURRENT request
// happens to target -- covers one element while LOOKING like it covers all
// of them. That is a genuinely common and genuinely dangerous assertion
// bug, so the correct form is used here and the trap is named in the prose.
for (genvar gb = 0; gb < BANKS; gb++) begin : g_bank_state_props
property p_bank_changes_only_on_own_event;
@(posedge clk) disable iff (!rst_n)
!$stable(bank_row_open[gb])
|-> $past((open_done && (open_bank == BANK_W'(gb)))
|| (close_done && (close_bank == BANK_W'(gb))));
endproperty
assert property (p_bank_changes_only_on_own_event);
endWhat these prove. P1 catches the three field boundaries — and with four fields there are three chances to get one wrong, which makes losslessness more valuable here than in 3.2's two-field case. P2 makes the classification well-formed so downstream logic can rely on it. P3 is the most consequential: a false ROW_HIT makes a controller skip opening a row, so it accesses whatever the array actually holds, with no error anywhere. The generate-wrapped property forbids cross-bank state corruption, which would otherwise produce false hits and conflicts indefinitely.
And note the deliberate lesson in P4's form. A single property cannot quantify over an array, so the tempting version — checking only the bank the current request happens to target — would cover one element while looking like it covered all of them. That is a common and dangerous assertion bug: a property that appears comprehensive and is not. The generate loop with one instance per bank is the correct form, and the code names the trap rather than silently avoiding it.
What they do not prove. Nothing here proves the array is in the state the model believes. The array has no digital observables (Chapter 2.4 §5), so these properties establish internal self-consistency — and a front end whose open_done inputs were wrong would satisfy all of them while being comprehensively desynchronised. That gap closes only against a device model that independently rejects illegal sequences. Nor does anything here say the classification is acted on correctly, or that the address map is a good one — the first is the controller's verification and the second is measured, not asserted.
8. Verification Perspective
The classification matrix is the core of the plan. Every classification, for every bank, with independence between banks confirmed: a hit in bank 0 while bank 1 conflicts, and so on. A regression that only ever exercises one bank has not tested the per-bank structure at all.
Field boundaries, as ever. With four fields there are three boundaries. Sweep addresses across each one and confirm P1 holds — a boundary error is the most likely integration fault in the whole model.
Simultaneous events. open_done and close_done to different banks in the same cycle, which must both apply. And open_done and close_done to the same bank in one cycle, which is the illegal combination §5 names — the test should confirm the generate-wrapped property surfaces it rather than the model silently choosing.
Non-power-of-two dimensions, per field. Each of the four can be non-power-of-two independently, and each must report its own out-of-range condition. This is four separate tests, not one.
Parameter configurations. Minimum dimensions of 1 for each field in turn; powers of two; non-powers of two; and an asymmetric shape. A parameterised module is several designs.
Reset, and the contract. Reset clears every bank's open flag and makes no claim about the array — so a design coming out of reset must establish the array's condition rather than assume it, exactly as 3.1 §8 argued. A testbench that assumes otherwise is testing a system that does not exist.
Coverage targets. All three classifications in every bank; each field at minimum and maximum; each field boundary crossed; simultaneous events both legal and illegal; addr_out_of_range per field; every parameter configuration elaborated.
9. Common Misconceptions
"One giant array would be simpler and therefore better." Wrong model: partitioning is added complexity that a cleaner design would avoid. Engineering action: the engineer reasons about a bank as a single grid, and treats the hierarchy as an implementation detail with no consequences. Resulting bug: no model for why access cost depends on which region is involved, why activity is localised, or why conductor length bounds anything. Performance reasoning has no place for locality below the row level. Correct model: a flat array fails on three independent axes at large size — bitline capacitance, wordline load, and whole-bank activity per access. Partitioning escapes a choice rather than adding complexity: it amortises periphery at two levels instead of one. Prevention: when a shared resource's cost grows with what it serves, ask whether adding a level beats choosing a size.
"A mat and a subarray are standard, well-defined terms." Wrong model: the hierarchy has an industry-standard vocabulary. Engineering action: reading one vendor's naming as universal, and interpreting another's documentation through it. Resulting bug: genuine misunderstanding of a device's organisation — two documents using the same word for different levels, or different words for the same level, and the engineer silently reconciling them wrongly. Correct model: the structure is invariant and the names are not. Some level bounds conductor length and owns local sensing; some level is the scope of row-state exclusivity. What they are called varies by vendor, literature and generation. Prevention: when reading any device documentation, identify each level by what it does before trusting what it is called.
"Addressing is an abstraction on top of the array." Wrong model: address fields are a software or protocol convention. Engineering action: treating field structure as arbitrary, and assuming any bit could be in any field with no consequence. Resulting bug: an address map chosen without analysis — which is among the most expensive arbitrary choices available, since the same workload can differ enormously in achieved bandwidth depending on how often it produces conflicts. Correct model: every field exists because a physical selection exists. Addressing is the array's shape expressed as numbers. Which system bits land in which field then determines the conflict rate. Prevention: ask what physical selection a field drives, then ask what the map does to the conflict rate.
"A row conflict is a kind of error." Wrong model: conflict is a fault or a failure to optimise. Engineering action: treating conflicts as something to eliminate entirely, or as evidence of a bug. Resulting bug: misdirected effort, and a failure to recognise that some access patterns must produce conflicts because the working set spans more rows than there are banks. Correct model: it is a classification, describing a legitimate state of affairs: the bank holds a different row, which must be closed first. It is expensive, not erroneous. Prevention: use the three names as descriptions of state, never as verdicts.
"The classification tells the controller what to do." Wrong model: classification and decision are the same step. Engineering action: building policy into the classifier, or assuming the classification implies an action. Resulting bug: a scheduler with policy baked in at the wrong level and no access to the information real policy needs — timing constraints, pending-request ages, the refresh obligation. And a teaching artefact that presents one page policy as a fact. Correct model: classification is information; policy is a separate decision requiring information this level does not have. §6's callout is the argument. Prevention: keep "what is true" and "what to do" in different modules, in designs and in explanations.
10. Debugging — False Hits and Phantom Conflicts
Symptom. A controller's row classification is wrong. Sometimes it reports a hit when the array actually holds a different row — and the resulting access returns data from the wrong row with no error. Sometimes it reports a conflict when the requested row is genuinely open, costing performance for no reason.
The two directions have different consequences and partly different causes, so establish which is happening first: a false hit corrupts data; a false conflict only costs time. A design exhibiting both is likely to have one root cause; one exhibiting only false conflicts is likely to have a different one.
Mechanism 1 — a field boundary is off by one bit. Inspect: reassemble all four fields and compare against the address, per P1. Expected evidence: misclassification with a consistent power-of-two relationship between the intended and actual row, appearing above a magnitude threshold. Discriminator: P1 fails immediately in simulation. In a lab, the arithmetic signature is diagnostic — and with four fields, note which boundary by seeing which field is wrong.
Mechanism 2 — the bank field is taken from the wrong bits. Inspect: whether one bank's state appears to follow another bank's activity. Expected evidence: classifications for bank A changing when only bank B was opened or closed — which produces both false hits and false conflicts, in a pattern correlated with cross-bank traffic. Discriminator: the cross-bank correlation. The generate-wrapped property in §7 catches it; the naive single-instance form would not, which is exactly why §7 makes that point.
Mechanism 3 — the open-row identifier was captured at the wrong moment. Inspect: whether open_row is sampled at the open request or at open_done. Expected evidence: false hits that correlate with back-to-back requests, because the identifier captured was a later request's row rather than the one that actually opened. Discriminator: hold the request stream steady during opens; if the misclassification stops, this is it — 3.1 §11's mechanism 1 at this level.
Mechanism 4 — a completion event was missed. Inspect: counts of open_done and close_done against the number of opens and closes actually issued. Expected evidence: a bank stuck reporting ROW_CLOSED forever (a missed open) or stuck reporting conflicts against a row that is no longer open (a missed close). Discriminator: the failure is persistent from an identifiable moment rather than intermittent, and only that bank is affected.
Mechanism 5 — the model is right and the array is not. Inspect: every path that can leave the sequencing logic — error handling, an aborted sequence, refresh, reset — and whether each re-establishes the array. Expected evidence: self-consistent classification that disagrees with physical reality, starting at an exceptional event and persisting. Discriminator: the model passes all its own assertions. This is the mechanism §7 explicitly says the assertions cannot catch, and it is why controllers are verified against device models rather than against their own bookkeeping.
Discrimination, cheapest first. False hits only, or conflicts too? Both, correlated with cross-bank traffic, points at mechanism 2. Is there a power-of-two relationship and a magnitude threshold? Yes points at mechanism 1. Did it start at an identifiable moment and persist? Yes points at mechanism 4 or 5, separated by whether event counts balance. Does it correlate with back-to-back requests? Yes points at mechanism 3.
The reasoning lesson. Classification faults are diagnosable because the two error directions have different costs and different signatures — and because the fault is in bookkeeping, which means event counting is often more informative than waveform inspection. Count opens, closes and completions per bank; if they do not balance, the arithmetic has already identified the problem before anything is opened in a viewer.
11. Interview Reasoning
"Why isn't a DRAM bank one large array?" Because a flat array fails on three independent axes at large size. The bitlines would be so long that the charge-sharing signal became unresolvable (3.3); the wordlines would be so loaded that selection became very slow, gated by the worst cell (3.4); and a single access would engage the entire bank, since one wordline assertion connects a full-width row. Partitioning into many small local arrays with local sensing escapes all three — and the key insight is that it amortises periphery at two levels, so short conductors and affordable periphery stop being alternatives.
"What determines how large a local array should be?" The trade-offs of the three preceding chapters, all of which apply at that level: bitline length against signal quality, wordline length against selection speed, and cells per sense amplifier against die area. Keeping the local array small is what makes the first two good; the global structures being shared across all local arrays is what keeps the third affordable. Specific dimensions are product decisions and are not general facts.
"Why do DRAM addresses have more than a row and a column field?" Because a partitioned structure has more selections to make. Before a row can be selected, the structure must know which bank and which local structure within it are involved, and each level of physical partitioning adds a level of selection that needs an index. Every field exists because a physical selection exists — addressing is the array's shape expressed as numbers, not an abstraction layered on top of it.
"What are the three outcomes of a request against a bank's row state, and why do their costs differ?" A hit — the requested row is already open — is cheapest, because the sense amplifiers still hold the resolved values, so nothing in the sensing sequence is repeated. Closed — no row open — costs opening the row. Conflict — a different row is open — is most expensive, because the open row must first be restored and released, and the sense amplifiers must be returned to their equalised condition before a new row's small signal can be measured against them. Every one of those costs is derived from the mechanism, not conventional.
"Why is row state per bank rather than per device or per local array?" Because the bank is the scope within which the one-row-at-a-time exclusivity is arbitrated — it is the collection of local arrays sharing the selection and distribution structures that enforce it. That is precisely why having several banks is valuable: a conflict in one says nothing about another, so independent state per bank is the precondition for any parallelism between them. What parallelism is actually usable is a later module's subject.
"Should the classification logic also decide what to do?" No, and the reason is substantive rather than stylistic. Policy needs information the classifier does not have — the timing constraints between operations, how many requests are pending and how old they are, and the refresh obligation. And the policy question is genuinely contested: whether to leave a row open after serving it depends on the workload's locality, which is why real controllers implement different page policies. Classification is information; deciding is a separate concern that deserves the information it needs.
12. Engineering Check
A front end like §5's is configured with
BANKS = 4andROWS = 16. Bank 1 currently has row 5 open; bank 2 has row 9 open; banks 0 and 3 have no open row. Four requests arrive: (a) bank 1, row 5; (b) bank 1, row 12; (c) bank 3, row 7; (d) bank 2, row 9.
1. Classify each request. (a) ROW_HIT — bank 1 holds exactly row 5. (b) ROW_CONFLICT — bank 1 holds a different row. (c) ROW_CLOSED — bank 3 holds nothing. (d) ROW_HIT — bank 2 holds exactly row 9.
2. Rank them by the work each requires, and say why. (a) and (d) require nothing beyond a column access, because the sense amplifiers in those banks still hold the resolved rows (3.5 §4). (c) requires opening row 7 — the full sensing sequence of 3.5 §3. (b) is worst: row 5 must be restored and released and bank 1's amplifiers returned to their equalised condition before row 12's small signal can be measured, then the whole sequence run.
3. Which of these can proceed independently of the others, and what does that depend on? They target four different banks, so their row state is independent — a conflict in bank 1 does not affect bank 3. Whether the operations can genuinely overlap depends on what the device permits and on shared resources between banks, which is Module 5's question. Independent state is a precondition for parallelism, not a guarantee of it — and conflating the two is a common error.
4. A scheduler could serve (a), (c) and (d) before (b). Is that the right thing to do? It is a plausible thing to do and the model deliberately does not say. Serving the cheap requests first improves immediate throughput; doing it repeatedly can starve (b) indefinitely, and if (b) is on a latency-critical path that is a bad outcome. This is exactly the line §6's callout draws: the classification is information, and the decision needs pending-request ages, timing constraints and the refresh obligation — none of which this level has.
5. Suppose (b) is misclassified as ROW_HIT. What happens? The controller skips closing row 5 and opening row 12, and issues a column access against bank 1 — which still holds row 5. Data comes back from the wrong row, well-formed, with no error reported anywhere, because nothing in the array can detect it. That is why P3 in §7 is the most consequential property in the model: a false hit is silent data corruption, whereas a false conflict merely wastes time.
6. Which single measurement would tell you whether the address map serving this device is any good? The conflict rate — the fraction of requests classifying as ROW_CONFLICT — alongside the number of column accesses served per row opening. Those two say directly how well the map and the traffic fit each other. A correct, lossless map with a high conflict rate is a bad map, which is precisely why 3.2 §6 insisted that map quality is measured, never asserted.
13. Summary
A flat array fails at scale on three independent axes: bitlines so long the charge-sharing signal becomes unresolvable, wordlines so loaded that selection is slow and gated by its worst cell, and a single access engaging the entire bank. Those are the unresolved trade-offs of 3.3, 3.4 and 3.5, and they all point the same way.
Partitioning escapes the choice rather than compromising on it. Many small local arrays, each with short conductors and its own local sensing, plus shared global selection and distribution. Periphery is then amortised at two levels: local sensing across its own local array, and the expensive global machinery across every local array. Short conductors and affordable periphery stop being alternatives. Activity becomes localised, and the structure becomes repeatable — which is why memory dies are so regular and why redundancy and repair are practical.
The levels are best identified by what each accomplishes, because their names are not standardised: the cell holds a bit; the row is what one selection conductor connects; the local array bounds conductor length and owns local sensing; the bank is the scope of row-state exclusivity; the device holds banks and the interface. An engineer who learns one vendor's vocabulary as universal will misread another's documentation.
Hierarchical structure produces hierarchical addressing — a field per level, each existing because a physical selection exists. Addressing is the array's shape expressed as numbers, not an abstraction on top of it. Which system bits map onto which field remains a policy with large performance consequences, owned by Module 18.
And the module's payoff is the classification: against a bank's state, a request is a hit (cheapest, because the amplifiers still hold the row), closed (open the row), or a conflict (restore and release the open row, equalise, then open). Every cost is derived from mechanism. The classification is per bank, which is why several banks are valuable. And it is information, not a decision — because policy needs timing constraints, request ages and the refresh obligation, and because the page-policy question is genuinely contested.
14. Module 3 in One Chain
Module 3 began with one cell and billions of copies of it. The argument, end to end:
Cells must share their selection, because a conductor and decoder output per cell would be paid at every bit. The set of cells one conductor reaches is a row — so row granularity is a property of the wiring, every cell in a row participates every time, and because selecting a row is a real operation whose result persists, the array acquires state that lives only in the controller.
A selected row makes far more available than was asked for, so a second, orthogonal selection chooses the subset: columns. Row selection acts on cells, costs substantially and leaves state; column selection acts on already-available values, costs little and leaves none. Two physically distinct selections, two address fields, and an external data path deliberately far narrower than a row.
The vertical conductor is the electrical heart of the technology. A bitline is long because sharing is what makes the array affordable, so its capacitance dwarfs a cell's — which makes the read signal small, forces sensing to compare against a matched reference rather than measure, and makes conductor length an architectural variable.
The horizontal conductor is a load, not a signal. A wordline gates every access transistor in a row, so asserting it takes real time and the far end settles last. And exclusivity is physical: two asserted conductors destroy two rows silently, which makes row decoding a safety function needing three properties, not one.
Sensing is where the weak evidence becomes a strong value. A sense amplifier is a cross-coupled pair whose positive feedback develops a small difference into a decision — the same bistable mechanism as an SRAM cell, which is why it has gain, why it holds its result, and why it can drive the restore. One operation, two outcomes: the cell is repaired and the row is held. The row buffer is the sense amplifiers in their settled state — and a row that is selected is not yet usable.
And a flat array cannot scale, so the structure is partitioned — local arrays with local sensing under shared global structures, amortising periphery at two levels, producing hierarchical addressing, and producing the hit / closed / conflict classification that every controller in the rest of this curriculum reasons about.
Read the chain backwards and the shape of a memory controller falls out. It tracks row state per bank because selection is shared and persists. It decomposes addresses into fields because each field drives a physical selection. It waits between operations because each phase of sensing is a physical process. It classifies requests because access cost depends on state. None of it is arbitrary. All of it is the array's structure, seen from outside.
15. What Comes Next
Module 3 has built the array and derived the state a controller must track. It has been careful throughout to stop at one boundary: it established that the bank is the scope of row-state exclusivity and said almost nothing else about banks.
That is deliberate, because the next question is large. If row state is per bank, and a conflict in one bank says nothing about another, then how many banks are there, how are they addressed, and what can actually happen at the same time? Independent state is a precondition for parallelism, not a guarantee of it — and the difference between the two is where much of a real memory system's performance lives.
Module 4 takes a different step first, placing everything built so far in its historical and generational context — how the interface evolved, what each generation added, and why. Module 5 then opens the device organisation properly: banks, bank groups, ranks and channels, and the parallelism they provide.
You now know why every structure in a DRAM array exists and what job each one does. That is the right position from which to learn how devices are organised and how the interface to them developed.
Return to Sense Amplifiers for the circuitry this hierarchy replicates, Bitlines and Wordlines for the conductors partitioning keeps short, or Rows for where the module began. For the system-level consequence of state-dependent access cost, see DRAM and The Memory Wall Problem. The full path is on the DDR tutorials index.
Continue learning
Related tutorials
- 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
The Refresh Requirement
Leakage produces a rule about the passage of time rather than about any operation. What the maintenance operation actually does, why it costs device availability, and how a digital design tracks a deadline, arbitrates it against traffic, and proves it never silently drops the obligation.
- Related topic
Restore Operations
Sensing consumed the stored state, so something must put it back. What restoration drives, why it covers a whole row, why a restored row is then cheap to access again, and an educational control model that cannot skip a prerequisite the array is unable to enforce.
- Related topic
Rows
A DRAM row is not an address range. It is the group of cells one shared selection conductor connects at the same instant — and that physical fact is where row granularity, controller-visible row state and state-dependent access cost all come from.
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.
