DDR · Module 5
The Memory-Subsystem View
Six levels, six shared resources, six conflicts. Assembling them into one picture and tracing a single request through it produces the question that organises all memory debugging: which level is blocking this, and how often?
Six chapters have each introduced one level of structure, and each followed the same pattern: something was shared, replicating it created parallelism, and the replication brought a new selection mechanism and a new conflict.
This chapter assembles them. Not as a summary — as one picture with a request moving through it, because the picture's whole value is that it turns a vague question into a precise one.
"Why is memory slow?" is not answerable. "Which level is blocking this request?" is — and it has exactly one answer at any moment, from a list of six. That reframing is what this module has been building toward, and it is what every later DDR module assumes you carry.
1. The Complete Picture
Read the variants. Blue bands are levels that replicate something and therefore create parallelism. Amber bands are the two shared resources inside a device that create conflicts — the column path and the row buffer. Grey bands are neither: the requester supplies addresses, the PHY supplies timing, and the module supplies packaging.
And notice the module's position. It sits between the channel and the rank because that is where it physically is — but Chapter 5.6 §9 established it is not addressed at all. It is the one band in the figure with no selection field, which is precisely what makes it a packaging boundary rather than a level of the hierarchy.
2. Every Level, in One Table
The six structural levels, answered with this module's questions:
| Level | Replicates | Still shares | Selected by | Controller tracks | Conflict |
|---|---|---|---|---|---|
| Channel | the whole interface | nothing | channel field | all state, per channel | imbalance — not contention |
| Rank | the devices | the data bus | chip select | who owns the bus | every transfer + handoff |
| Bank group | the column path | device data path | group field | the previous group | consecutive same-group access |
| Bank | row state | column path + interface | bank field | open, and which row | different rows, same bank |
| Row | — | the row buffer | row field | — | — |
| Column | — | the row buffer | column field | — | — |
Three readings of that table are worth more than the table.
The "still shares" column shrinks upward and the parallelism grows with it. A bank shares almost everything; a channel shares nothing. That ordering is not a coincidence — it is the definition, since Chapter 5.1 §1 established that parallelism is created by replication and destroyed by sharing.
The "controller tracks" column is the memory controller's entire state. Per-channel everything, rank bus ownership, previous bank group, per-bank open row. That is a surprisingly short list for a structure this elaborate, and it is short because the controller tracks structure and availability, never data.
And the bank group's entry is unique. Every other level requires tracking state — what is open, who owns what. A bank group requires tracking history. That is why its conflict is pairwise and why Chapter 5.3 §11 insisted a per-request histogram cannot see it.
3. Tracing One Request
Follow a single read from a requester to the data coming back.
A requester issues an address. It knows nothing about banks or channels — Chapter 5.1 §3 established that the mapping from address to structure is the controller's, not the requester's.
The controller maps the address into fields — channel, rank, bank group, bank, row, column. This is where most performance is decided, because the mapping determines whether consecutive requests spread across the hierarchy or concentrate. Module 18 owns it.
The channel field routes the request to one channel's queue. From here the request is inside one independent memory subsystem, and nothing about the other channels affects it.
The controller checks the bank's state. Row open and matching — a row hit, and the request can proceed to a column access. Row closed — an activate is needed first. Row open and different — a precharge then an activate, the row conflict that costs the most.
The rank must own the data bus when the data moves. If another rank owns it, the transfer waits and a handoff is paid.
The bank group classification against the previous access determines how soon the column access may follow its predecessor.
Commands cross the channel's command bus, through any module buffering, to the devices. Every device in the rank participates in lockstep — Chapter 5.4 §2 — each contributing its slice.
The array does the physical work that Modules 2 and 3 described: a wordline activates, sense amplifiers resolve, the row is held and must later be restored.
And data returns through the column path, the device interface, any data buffering, the channel's data bus, and the PHY's trained capture logic to the controller.
4. One Level Is Different
Three of the four blocking levels are genuine blocks: the resource is unavailable and the request cannot proceed.
The bank group is not. Two accesses in the same group can both proceed — they simply must be separated in time. Nothing is unavailable; the second access is merely delayed.
That distinction matters structurally, and it is why §5's RTL treats it separately. Turning "these two accesses share a column path" into "therefore wait N cycles" requires timing, and this module has deliberately carried none. Chapter 5.3 §4 explained why: the classification is generation-independent and the separation is not.
So the architecture layer produces a classification, and the timing layer converts it into a delay. Modules 13 and 14 do the converting. Keeping that boundary clean is what lets one classifier serve DDR4 and DDR5 unchanged, and it is the clearest example in this module of layering paying off.
5. RTL — The Architecture State Model
Engineering problem
Given a request already decomposed into fields, answer in one place: which level of the hierarchy prevents it from being served right now, what preparatory work it needs, and how it classifies against its predecessor.
This composes 5.2's bank state, 5.3's classification and 5.4's ownership into the single output that matters for diagnosis: block_level.
Classification
SYNTHESIZABLE RTL — an ARCHITECTURE STATE MODEL, not a controller.
The distinction is important enough to be explicit about what is deliberately absent:
No commands. It reports that an activate is needed; it does not issue one. Module 7 owns commands. No address decoding. Fields arrive already separated. Modules 8 and 18 own addressing. No timing counters. Nothing here counts cycles or enforces a separation. Modules 13 and 14 own timing. No scheduling. It evaluates the one request it is given; it does not choose among several, which is where a real controller's value lies. Module 17 owns that.
What it does own is the structural question: given the state of every level, which one is in the way.
Interface
req_* presents a decomposed request. ch_ready and bus_owner come from levels this block does not model internally. open_evt and close_evt report completed row transitions. Outputs are block_level, req_ready, needs_precharge, needs_activate, group_hint, and per-level counters.
State
Per-rank, per-bank open bits and open rows; the previously issued group and bank. Nothing else — and the smallness of that state relative to the structure it describes is the point.
Combinational logic
The level evaluation, in priority order, and the preparatory-work decisions.
Sequential logic
Row state updates on completion events, and the predecessor update on issue.
Simulation
vlog ddr_arch_request_frontend.sv tb_ddr_arch_request_frontend.sv then vsim -c tb_ddr_arch_request_frontend -do "run -all"; VCS vcs -sverilog ddr_arch_request_frontend.sv tb_ddr_arch_request_frontend.sv && ./simv; Xcelium xrun -sv ddr_arch_request_frontend.sv tb_ddr_arch_request_frontend.sv.
Expected output: §7's sequence walks block_level down the hierarchy — channel, then rank, then bank — reaching NONE on a row hit, then returning to the bank level when a different row is wanted.
// ─────────────────────────────────────────────────────────────────────────
// DDR ARCHITECTURE REQUEST FRONTEND.
// Classification: SYNTHESIZABLE RTL -- AN ARCHITECTURE STATE MODEL.
//
// THIS IS NOT A MEMORY CONTROLLER. Deliberately absent:
// * commands -- it reports that an activate is NEEDED, never issues
// one (Module 7)
// * address decode -- fields arrive already separated (Modules 8, 18)
// * timing counters -- nothing counts cycles or enforces a separation
// (Modules 13, 14)
// * scheduling -- it evaluates the ONE request it is given and does
// not choose among several (Module 17)
//
// What it owns is the structural question this whole module was for:
// GIVEN THE STATE OF EVERY LEVEL, WHICH ONE IS IN THE WAY.
//
// NOTE ON THE BANK GROUP. It is reported as a HINT, not a block level,
// because same-group accesses are DELAYED rather than prevented -- and
// converting "shares a column path" into "wait N cycles" requires timing,
// which this layer deliberately has none of. The classification is
// generation-independent; the separation is not.
// ─────────────────────────────────────────────────────────────────────────
module ddr_arch_request_frontend #(
parameter int NUM_RANKS = 2,
parameter int BANK_GROUPS = 2,
parameter int BANKS_PER_GROUP = 2,
parameter int ROW_W = 8,
parameter int ACC_W = 16,
parameter int TOTAL_BANKS = BANK_GROUPS * BANKS_PER_GROUP,
parameter int RK_W = (NUM_RANKS <= 1) ? 1 : $clog2(NUM_RANKS),
parameter int BG_W = (BANK_GROUPS <= 1) ? 1 : $clog2(BANK_GROUPS),
parameter int BA_W = (BANKS_PER_GROUP <= 1) ? 1 : $clog2(BANKS_PER_GROUP),
parameter int TB_W = (TOTAL_BANKS <= 1) ? 1 : $clog2(TOTAL_BANKS)
) (
input logic clk,
input logic rst_n,
// ── Request, already decomposed. How it was decomposed is Module 18's.
input logic req_valid,
input logic [RK_W-1:0] req_rank,
input logic [BG_W-1:0] req_bg,
input logic [BA_W-1:0] req_ba,
input logic [ROW_W-1:0] req_row,
// The request is being issued, so it becomes the classification
// predecessor. Separate from req_valid for Chapter 5.3's reason: a
// request that is evaluated and not issued contended with nothing.
input logic issue,
// ── State from levels this block does not model internally.
// The channel's queue depth is the channel's business (Chapter 5.5);
// bus ownership is rank_bus_owner's (Chapter 5.4). Taking them as
// inputs keeps this block a COMPOSER rather than a reimplementation.
input logic ch_ready,
input logic bus_valid,
input logic [RK_W-1:0] bus_owner,
// ── Completed row transitions, from whatever sequences the array.
// This block TRACKS state; it does not cause transitions.
input logic open_evt,
input logic [RK_W-1:0] open_rank,
input logic [TB_W-1:0] open_bank,
input logic [ROW_W-1:0] open_row,
input logic close_evt,
input logic [RK_W-1:0] close_rank,
input logic [TB_W-1:0] close_bank,
// ── The answer. 0 NONE, 1 CHANNEL, 2 RANK, 3 BANK.
output logic [1:0] block_level,
output logic req_ready,
output logic needs_precharge,
output logic needs_activate,
// Chapter 5.3's classification: 0 SAME_BANK, 1 SAME_GROUP, 2 DIFF_GROUP.
output logic [1:0] group_hint,
output logic group_hint_valid,
output logic index_invalid,
// Per-level telemetry. The module's recurring argument: a controller that
// cannot say WHICH level blocked it cannot be tuned.
output logic [ACC_W-1:0] cnt_blk_channel,
output logic [ACC_W-1:0] cnt_blk_rank,
output logic [ACC_W-1:0] cnt_blk_bank,
output logic [ACC_W-1:0] cnt_ready
);
localparam logic [1:0] BLK_NONE = 2'd0;
localparam logic [1:0] BLK_CHANNEL = 2'd1;
localparam logic [1:0] BLK_RANK = 2'd2;
localparam logic [1:0] BLK_BANK = 2'd3;
localparam logic [1:0] CLS_SAME_BANK = 2'd0;
localparam logic [1:0] CLS_SAME_GROUP = 2'd1;
localparam logic [1:0] CLS_DIFF_GROUP = 2'd2;
// ── COMPILE-TIME legality.
if (NUM_RANKS < 1) begin : g_nr
initial $fatal(1, "ddr_arch_request_frontend: NUM_RANKS must be >= 1");
end
if (BANK_GROUPS < 1 || BANKS_PER_GROUP < 1) begin : g_bank
initial $fatal(1, "ddr_arch_request_frontend: bank geometry must be >= 1");
end
if (ROW_W < 1) begin : g_row
initial $fatal(1, "ddr_arch_request_frontend: ROW_W must be >= 1");
end
// ── Flattened bank index within a rank. The group and bank fields
// concatenate: a bank is identified by BOTH, which is the structural
// fact Chapter 5.3's assertion P3 protects.
logic [TB_W-1:0] req_bank_flat;
if (BANK_GROUPS == 1) begin : g_flat_one_group
assign req_bank_flat = TB_W'(req_ba);
end else if (BANKS_PER_GROUP == 1) begin : g_flat_one_bank
assign req_bank_flat = TB_W'(req_bg);
end else begin : g_flat_concat
assign req_bank_flat = TB_W'({req_bg, req_ba});
end
// ── Index range checks, Chapter 5.1's pattern throughout.
logic rk_bad, bg_bad, ba_bad;
if (NUM_RANKS >= (1 << RK_W)) begin : g_rk_full
assign rk_bad = 1'b0;
end else begin : g_rk_chk
assign rk_bad = ({1'b0, req_rank} >= (RK_W+1)'(NUM_RANKS));
end
if (BANK_GROUPS >= (1 << BG_W)) begin : g_bg_full
assign bg_bad = 1'b0;
end else begin : g_bg_chk
assign bg_bad = ({1'b0, req_bg} >= (BG_W+1)'(BANK_GROUPS));
end
if (BANKS_PER_GROUP >= (1 << BA_W)) begin : g_ba_full
assign ba_bad = 1'b0;
end else begin : g_ba_chk
assign ba_bad = ({1'b0, req_ba} >= (BA_W+1)'(BANKS_PER_GROUP));
end
assign index_invalid = req_valid && (rk_bad || bg_bad || ba_bad);
// ── State. Per rank, per bank: is a row open, and which.
logic bank_open_q [NUM_RANKS][TOTAL_BANKS];
logic [ROW_W-1:0] open_row_q [NUM_RANKS][TOTAL_BANKS];
// Chapter 5.3's predecessor.
logic [BG_W-1:0] prev_bg_q;
logic [BA_W-1:0] prev_ba_q;
logic prev_valid_q;
logic sel_open;
logic [ROW_W-1:0] sel_row;
assign sel_open = bank_open_q[req_rank][req_bank_flat];
assign sel_row = open_row_q [req_rank][req_bank_flat];
logic row_match, ok;
assign row_match = (sel_row == req_row);
assign ok = req_valid && !index_invalid;
// ── THE LEVEL EVALUATION, in hierarchy order.
//
// Priority is top-down deliberately: a request blocked at the channel
// is blocked regardless of what any lower level's state is, so
// reporting the LOWEST unsatisfied level would be misleading. The
// controller wants the FIRST obstacle, because that is the one to act
// on -- clearing a bank conflict achieves nothing while the channel
// queue is full.
logic rank_owns;
assign rank_owns = !bus_valid || (bus_owner == req_rank);
always_comb begin
if (!ok) block_level = BLK_NONE;
else if (!ch_ready) block_level = BLK_CHANNEL;
else if (!rank_owns) block_level = BLK_RANK;
else if (!sel_open) block_level = BLK_BANK; // needs activate
else if (!row_match) block_level = BLK_BANK; // row conflict
else block_level = BLK_NONE; // row hit
end
assign req_ready = ok && ch_ready && rank_owns && sel_open && row_match;
// ── Preparatory work. Reported, never performed -- this block has no
// commands. A closed bank needs an activate; an open bank holding the
// wrong row needs a precharge FIRST and then an activate, which is the
// row conflict and the most expensive case in the hierarchy.
assign needs_activate = ok && ch_ready && rank_owns
&& (!sel_open || !row_match);
assign needs_precharge = ok && ch_ready && rank_owns
&& sel_open && !row_match;
// ── Chapter 5.3's classification, carried through as a HINT.
logic bg_eq, ba_eq;
assign bg_eq = (req_bg == prev_bg_q);
assign ba_eq = (req_ba == prev_ba_q);
assign group_hint_valid = ok && prev_valid_q;
assign group_hint = (bg_eq && ba_eq) ? CLS_SAME_BANK
: bg_eq ? CLS_SAME_GROUP
: CLS_DIFF_GROUP;
// ── Saturating telemetry.
logic [ACC_W:0] c_ch, c_rk, c_bk, c_rd;
always_comb begin
c_ch = {1'b0, cnt_blk_channel} + (ACC_W+1)'(1);
c_rk = {1'b0, cnt_blk_rank} + (ACC_W+1)'(1);
c_bk = {1'b0, cnt_blk_bank} + (ACC_W+1)'(1);
c_rd = {1'b0, cnt_ready} + (ACC_W+1)'(1);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int r = 0; r < NUM_RANKS; r++) begin
for (int b = 0; b < TOTAL_BANKS; b++) begin
bank_open_q[r][b] <= 1'b0;
open_row_q[r][b] <= '0;
end
end
prev_bg_q <= '0;
prev_ba_q <= '0;
prev_valid_q <= 1'b0;
cnt_blk_channel <= '0;
cnt_blk_rank <= '0;
cnt_blk_bank <= '0;
cnt_ready <= '0;
end else begin
// Row state follows COMPLETED transitions. Isolation holds by
// construction: each event names exactly one rank and one bank, and
// exactly one array element is written.
if (open_evt) begin
bank_open_q[open_rank][open_bank] <= 1'b1;
open_row_q[open_rank][open_bank] <= open_row;
end
if (close_evt) begin
bank_open_q[close_rank][close_bank] <= 1'b0;
end
if (ok) begin
unique case (block_level)
BLK_CHANNEL: cnt_blk_channel <= c_ch[ACC_W] ? {ACC_W{1'b1}} : c_ch[ACC_W-1:0];
BLK_RANK: cnt_blk_rank <= c_rk[ACC_W] ? {ACC_W{1'b1}} : c_rk[ACC_W-1:0];
BLK_BANK: cnt_blk_bank <= c_bk[ACC_W] ? {ACC_W{1'b1}} : c_bk[ACC_W-1:0];
default: cnt_ready <= c_rd[ACC_W] ? {ACC_W{1'b1}} : c_rd[ACC_W-1:0];
endcase
end
if (issue && ok) begin
prev_bg_q <= req_bg;
prev_ba_q <= req_ba;
prev_valid_q <= 1'b1;
end
end
end
endmoduleCycle trace
NUM_RANKS = 2, BANK_GROUPS = 2, BANKS_PER_GROUP = 2. A request to rank 0, bank 0, row 5, meeting each obstacle in turn:
| Cycle | Obstacle present | block_level | needs_precharge | needs_activate |
|---|---|---|---|---|
| 0 | channel not ready | CHANNEL | 0 | 0 |
| 1 | rank 1 owns the bus | RANK | 0 | 0 |
| 2 | bank 0 closed | BANK | 0 | 1 |
| 4 | row 5 open | NONE | 0 | 0 |
| 5 | now wants row 7 | BANK | 1 | 1 |
| 9 | row 7 open | NONE | 0 | 0 |
The request descends the hierarchy, and the order is the figure's order. Note cycles 0 and 1: needs_activate is low even though the bank is closed — because the block does not report preparatory work for a request that cannot get past a higher level. Reporting "you need an activate" to a request blocked at the channel would be noise, and it is the kind of noise that makes telemetry unusable.
Waveform expectation
§7. Watch block_level step down one level at a time as each obstacle clears, and return to the bank level when a different row is wanted.
Synthesis implication
NUM_RANKS × TOTAL_BANKS open bits plus their rows, a two-dimensional indexed read, comparators and four saturating counters. The 2D indexed read is the structure to watch — it is a multiplexer over NUM_RANKS × TOTAL_BANKS entries and it sits on the path to block_level, which a scheduler wants early in the cycle. A real controller usually keeps the table per rank to shorten it.
Corner cases
BANK_GROUPS == 1 or BANKS_PER_GROUP == 1 change how the flat bank index is formed, which is why the generate has three arms — concatenating a zero-width field would be illegal. NUM_RANKS == 1 makes rank_owns permanently true, so the rank level never blocks, which is correct for a single-rank system. bus_valid low means the bus is idle and any rank may proceed. An invalid index reports index_invalid and forces block_level to NONE without setting req_ready — deliberately, so an invalid request is never counted as blocked at a level it never reached.
Verification
What DV must prove: block_level matches the hierarchy priority exactly — the first unsatisfied level, not any unsatisfied level; req_ready exactly when nothing blocks; preparatory-work outputs only when the request has cleared the higher levels; needs_precharge implying needs_activate and never the reverse; isolation — a row event for one rank and bank leaves every other entry unchanged; the classification hint matching 5.3's definitions; counters summing to the number of evaluated requests; and all three flat-index arms.
SVA
§6.
Debugging
If block_level reports BANK while the channel is not ready, the priority order has been inverted — the evaluation must be top-down. If preparatory work is reported for requests blocked higher up, the ch_ready && rank_owns guards are missing from needs_*, and the resulting telemetry will suggest row conflicts that never occurred. If the counters do not sum to the request count, check that the unique case covers BLK_NONE via its default. If a row event appears to affect the wrong bank, check the flat-index generate arm for the configured geometry — the one-group and one-bank-per-group cases form the index differently and are easy to get wrong because they are rarely exercised.
Limitations
No timing anywhere, so the bank-group hint is a classification and not a delay, and nothing here knows how long any operation takes. No commands, no address decode, no scheduling — the header lists these because they are the boundaries that keep this an architecture model. No refresh, which closes banks and is Module 15's. No read/write direction or turnaround. No queueing: ch_ready is an input, so the block cannot distinguish a channel that is busy from one that is badly mapped. And it evaluates one request, where the real difficulty is choosing among many.
One asymmetry is deliberate and worth naming. The request fields are range-checked and reported through index_invalid; the row-event fields (open_rank, open_bank, close_rank, close_bank) are not. They come from the block that sequences the array rather than from a requester, so they are treated as a trusted internal contract — but that is an assumption, not a guarantee, and an out-of-range event index would write outside the state array. In an integration where those events cross a module boundary you do not control, they need the same generate-guarded range check the request fields get.
6. Five Assertions Worth Writing
// VERIFICATION-ONLY, inside ddr_arch_request_frontend.
// P1 -- THE PARTITION PROPERTY, and the most important assertion in this
// module. Exactly one counter advances per evaluated request. Section 10's
// closing lesson is that per-level telemetry is only meaningful if the
// levels PARTITION the requests -- mutually exclusive and exhaustive -- and
// a category system that overlaps produces confident, detailed, wrong
// pictures. This is that requirement made checkable.
property p_counters_partition;
@(posedge clk) disable iff (!rst_n)
ok |=> $onehot({ cnt_blk_channel != $past(cnt_blk_channel),
cnt_blk_rank != $past(cnt_blk_rank),
cnt_blk_bank != $past(cnt_blk_bank),
cnt_ready != $past(cnt_ready) });
endproperty
assert property (p_counters_partition);
// P2 -- BLOCK LEVEL IS THE FIRST OBSTACLE, NOT ANY OBSTACLE. The priority
// must be top-down: reporting a bank conflict to a request whose channel
// queue is full would send every downstream optimisation at the wrong
// level. Each arm pins one step of the priority chain.
property p_channel_has_priority;
@(posedge clk) disable iff (!rst_n)
(ok && !ch_ready) |-> (block_level == BLK_CHANNEL);
endproperty
assert property (p_channel_has_priority);
property p_rank_before_bank;
@(posedge clk) disable iff (!rst_n)
(ok && ch_ready && !rank_owns) |-> (block_level == BLK_RANK);
endproperty
assert property (p_rank_before_bank);
// P3 -- preparatory work is reported only for a request that actually
// reached the bank level. Reporting "you need an activate" to a request
// blocked at the channel is noise, and noise is what makes telemetry
// unusable rather than merely imprecise.
property p_prep_only_below_higher_levels;
@(posedge clk) disable iff (!rst_n)
(needs_activate || needs_precharge) |-> (ch_ready && rank_owns);
endproperty
assert property (p_prep_only_below_higher_levels);
// A precharge is never needed without an activate after it: closing a row
// you do not intend to replace is not a thing this model can express.
property p_precharge_implies_activate;
@(posedge clk) disable iff (!rst_n)
needs_precharge |-> needs_activate;
endproperty
assert property (p_precharge_implies_activate);
// P4 -- ISOLATION ACROSS RANKS AND BANKS, composed from Chapter 5.2. A row
// event names exactly one rank and one bank, and must leave every other
// entry untouched. Written per pair with genvars because isolation is a
// claim about a RELATIONSHIP between an event and state it did not name.
generate
for (genvar r = 0; r < NUM_RANKS; r++) begin : g_iso_rank
for (genvar b = 0; b < TOTAL_BANKS; b++) begin : g_iso_bank
property p_entry_untouched;
@(posedge clk) disable iff (!rst_n)
(!(open_evt && (open_rank == RK_W'(r)) && (open_bank == TB_W'(b)))
&& !(close_evt && (close_rank == RK_W'(r)) && (close_bank == TB_W'(b))))
|=> ((bank_open_q[r][b] == $past(bank_open_q[r][b]))
&& (open_row_q[r][b] == $past(open_row_q[r][b])));
endproperty
assert property (p_entry_untouched);
end
end
endgenerate
// P5 -- req_ready is exactly "nothing blocks". A status output that can
// disagree with the level report is worse than no status output, because
// two consumers would draw opposite conclusions from one evaluation.
property p_ready_iff_unblocked;
@(posedge clk) disable iff (!rst_n)
ok |-> (req_ready == (block_level == BLK_NONE));
endproperty
assert property (p_ready_iff_unblocked);P1 is the assertion this whole module has been building toward. Every RTL block in Module 5 produced counters, and §10 argued that a per-level profile whose counts do not partition the requests is not a measurement — it is several overlapping measurements that resemble one, with every ratio drawn from it unreliable in an unknown direction.
P1 makes the partition a verified property rather than an intention. And notice its shape: $onehot over four change-detectors, not over the counters themselves. The property is about how the instrument behaves, not about what it counts — which is the right level for an instrument, and a formulation worth reusing anywhere a system is categorised.
P2's two arms pin the priority chain, and the reason priority matters is diagnostic rather than functional. A request blocked at the channel is blocked whatever its bank state is, so reporting the lowest unsatisfied level would be accurate and useless: it would name a level that clearing would not help. The first obstacle is the actionable one.
P4 composes Chapter 5.2's isolation across two dimensions — rank and bank — and it is the property most likely to be omitted, for the same reason it was there: it is about what does not happen to state nobody mentioned. The genvar pair is unavoidable; isolation is a relation, and relations need properties per pair.
What none of them prove. Nothing about timing, so the bank-group hint remains a classification and is never checked against any separation — §4's boundary, deliberately. Nothing about data, which this block never sees. Nothing about whether the fields were decomposed correctly, which is Module 18's. And nothing about ch_ready or bus_owner being right — they are inputs, and this block composes levels it does not model, so their correctness is the responsibility of 5.5's and 5.4's blocks.
7. Descending the Hierarchy
ddr_arch_request_frontend — block_level walks down the hierarchy
10 cyclesblock_level is the only signal that matters here, and its trajectory is the chapter.
Cycles 0 to 3 descend. CHANNEL, then RANK, then BANK — each level clearing in turn, with the request unable to see past the current obstacle. At cycles 0 and 1, needs_activate is low even though the bank is closed, because the block does not report work for a request that has not yet reached that level. That restraint is what keeps the telemetry meaningful.
Cycle 4 is the hit. Everything satisfied, req_ready high.
Cycles 5 to 8 are the expensive case. The same bank, a different row: needs_precharge and needs_activate together, the close completing at cycle 7 and the open at cycle 9. Four cycles of preparation in this model and considerably more in reality — and Chapter 5.2 §13 showed this is exactly what a workload alternating two rows in one bank pays on every single access.
What the figure does not claim. No real durations — the events here happen when the stimulus says, and actual timing is Modules 13 and 14. No bank-group delay, since §4 explained that requires timing. And nothing about data.
8. Where Every Later Module Attaches
This module deliberately stopped at structure. The map of what comes next:
| Question this module raised | Owned by |
|---|---|
| What the signals physically are | Module 6 — DDR Signals |
| The actual command encodings | Module 7 — DDR Commands |
| How addresses form the fields | Modules 8, 18 — Addressing, Address Mapping |
| What activate and precharge really do | Module 9 — Activate and Precharge |
| Reads, writes, bursts | Modules 10–12 |
| How long anything takes | Modules 13, 14 — Timing |
| Refresh, which closes banks | Module 15 |
| Banks and bank groups in depth | Module 16 |
| Choosing among pending requests | Module 17 — Controller Architecture |
| Capturing data at speed | Modules 19–21 — PHY, DQS, Training |
| Termination and signal integrity | Module 22 |
Every one of those modules assumes the structure this one built. That is why Module 5 comes before all of them, and why it teaches vocabulary as consequences rather than definitions: a definition you memorised will not tell you which level is blocking a request, and a consequence you derived will.
9. Common Misconceptions
"Memory performance is a single number." Wrong mental model: a memory subsystem has a latency and a bandwidth that characterise it. Engineering action: sizing systems from peak bandwidth; predicting performance from a datasheet figure; comparing configurations on one metric. Observable failure / bad conclusion: predictions wrong by large factors in either direction, because the achieved figure depends on which levels the access pattern conflicts at — and Chapter 4.8 §12 showed two independent losses can multiply, giving 37% of a rated figure with nothing malfunctioning. Correct model: a request can be blocked at any of several levels, each with its own conflict and its own fix. Performance is a distribution over which level blocked, not a number. Prevention: ask "blocked where, how often" rather than "how fast". If the instrumentation cannot answer, that is the first thing to build.
"Any level's conflict can be fixed by better address mapping." Wrong mental model: mapping is a universal lever. Engineering action: remapping to fix one conflict without checking its effect on the others; assuming an interleaving that helps one level helps all. Observable failure / bad conclusion: fixing bank-group contention while worsening rank alternation, because bank groups want the channel and group fields on rapidly changing bits while ranks want the opposite — alternating ranks per access costs a handoff on every access and gains nothing. Correct model: mapping affects which level conflicts, and the levels want incompatible things. It is a trade across levels, informed by which conflict actually dominates the workload. Prevention: measure the per-level block distribution before remapping, and re-measure after. A remapping justified by reasoning alone is as likely to hurt as help.
"The controller tracks what is in memory." Wrong mental model: a memory controller has some view of the data. Engineering action: expecting the controller to detect corrupted data; assuming it could validate a read; imagining data-dependent behaviour. Observable failure / bad conclusion: misunderstanding why Chapter 5.2 §4's silent row mismatch is undetectable — the controller cannot notice wrong data because it never looks at data — and mis-scoping verification, since data correctness needs a scoreboard and structure needs assertions. Correct model: the controller tracks structure and availability: which row is open, who owns the bus, what the previous group was, what is queued. It never tracks contents. §2's table is the whole of its state. Prevention: look at the state list. Nothing in it is a datum.
"Every level's conflict is a block." Wrong mental model: all conflicts prevent a request from proceeding. Engineering action: modelling bank-group contention as a resource being unavailable; building a scheduler that waits for a group to be free. Observable failure / bad conclusion: a model that cannot express the actual constraint, which is a minimum separation rather than a busy resource — and a scheduler that serialises accesses which could have overlapped. Correct model: channel, rank and bank conflicts are genuine blocks — a resource is unavailable. A bank-group conflict is a delay: both accesses can proceed, separated in time. Converting the classification into a separation requires timing, which is Modules 13 and 14'. Prevention: ask whether the second access is impossible or merely early. Only the first is a block.
10. Debugging — Performance Is Poor and No Single Level Explains It
Symptom. A memory subsystem underperforms. Per-level instrumentation shows blocks distributed across channel, rank and bank levels, with no single level dominating. No errors.
A distributed block profile is genuinely different from a dominated one, and the mistake is treating it as several independent problems to fix in parallel.
Mechanism 1 — one level dominates and the instrumentation is hiding it. Inspect: whether block counts are gated on requests that actually reached that level. Expected evidence: counts that sum to more than the request count, or preparatory-work counts that exceed bank blocks. Discriminator: do the per-level counts sum to the request count? §5's design reports the first obstacle for exactly this reason. Instrumentation that counts every unsatisfied level makes a dominated profile look distributed, and it is the first thing to rule out because it invalidates the whole picture.
Mechanism 2 — the levels are coupled through the address mapping. Inspect: which address bits feed each field, and whether one mapping choice is producing conflicts at two levels at once. Expected evidence: block counts at two levels moving together as the workload changes. Discriminator: do two levels' counts correlate? A single bad mapping decision can cause both rank alternation and group concentration, and fixing them separately will fail because there is one cause. Correlated counts mean one cause, not two.
Mechanism 3 — the bottleneck moves as each is fixed. Inspect: the block profile before and after each change. Expected evidence: fixing the dominant level promotes another to dominance, with total throughput improving less than expected each time. Discriminator: does the profile shift rather than shrink? This is not a failure — it is what a balanced system looks like, and the correct conclusion is that the system is near a genuine ceiling rather than that each fix failed.
Mechanism 4 — the requester is the limit. Inspect: how many requests are outstanding at once. Expected evidence: few blocks at any level and low utilisation everywhere, because a requester with one access in flight cannot conflict with anything. Discriminator: are the levels blocking at all? If nothing is blocked and throughput is low, the memory system is idle and the problem is upstream — Chapter 1.8 §8's memory-level parallelism, and no memory-side change helps.
Mechanism 5 — refresh and other periodic work. Inspect: the fraction of time unavailable for refresh, and its temperature dependence. Expected evidence: a throughput ceiling with no correlation to any address field, rising with temperature. Discriminator: no address correlation. Chapter 2.3 established retention falls with temperature, so the burden grows — and this is the component that appears in no level's block count because it is not a structural conflict at all.
Discrimination, cheapest first. Check that the per-level counts sum to the request count — one arithmetic check, and it validates or invalidates the entire profile. Then check whether any two levels' counts correlate, which distinguishes one cause from several. Then check outstanding-request depth, which separates "the memory system is the limit" from "the memory system is idle". Then look for temperature dependence.
The reasoning lesson, and it is this module's closing one. Building per-level instrumentation is the easy half; validating that it partitions correctly is the half that gets skipped. A profile whose counts do not sum to the request count is not a measurement — it is several overlapping measurements that resemble one, and every conclusion drawn from it is unreliable in an unknown direction.
The design decision that prevents this is in §5: report the first obstacle, not every obstacle. That makes the counters a genuine partition of the requests, which is what makes their ratios meaningful. Whenever you instrument a system by category, check that the categories partition — that they are mutually exclusive and exhaustive — because a category system that overlaps produces confident, detailed, wrong pictures, and those are far more dangerous than no picture at all.
11. Interview Reasoning
"Walk me through what happens to a memory request." The requester issues an address and knows nothing about structure. The controller maps that address into fields — channel, rank, bank group, bank, row, column — and that mapping is where most performance is decided, because it determines whether consecutive requests spread across the hierarchy or concentrate. The channel field routes it into one independent subsystem. The controller checks its per-bank model: if the wanted row is already open it is a row hit and a column access can proceed; if the bank is closed it needs an activate; if a different row is open it needs a precharge then an activate, which is the most expensive case. The rank must own the shared data bus when data moves, costing a handoff if it does not. The bank-group classification against the previous access determines how soon the column access may follow. Commands cross the channel's command bus through any module buffering to the devices, every device in the rank participates in lockstep, the array does the physical work, and data returns through the column path, the buffers, and the PHY's trained capture logic.
"What does a memory controller actually keep track of?" Structure and availability, never data. Per channel it keeps entirely separate state. Within a channel: which rank owns the data bus, the previously issued bank group for classification, and for each bank whether a row is open and which one. That is a short list for a structure this elaborate, and the open-row table is the important one — a column command carries no row address, so if that model is wrong the device returns the wrong row's data with no error signalled anywhere. What the controller never tracks is contents, which is why it cannot detect corrupt data and why data correctness needs a scoreboard while structural correctness needs assertions.
"Why is 'which level is blocking this' a better question than 'why is memory slow'?" Because it has exactly one answer at any moment, from a short list, and each answer implies a different fix. Blocked at the channel means its queue is full or the mapping concentrated traffic there — fix distribution. Blocked at the rank means another rank owns the bus — batch by rank rather than alternating. Delayed at the bank group means the previous access shared a column path — interleave groups. Blocked at the bank means the wrong row is open — change which rows map together or close rows sooner. Those fixes are not interchangeable and some actively conflict, since bank groups want rapidly changing address bits and ranks want the opposite. So the vague question cannot be acted on and the precise one can, which is why per-level instrumentation matters more than aggregate throughput.
"Is a bank-group conflict the same kind of thing as a rank conflict?" No, and the difference is structural. A rank conflict is a genuine block — another rank owns the data bus, so the transfer cannot happen. A bank-group conflict is a delay: both accesses can proceed, they simply have to be separated in time because they share an internal column path. Nothing is unavailable. That distinction matters because converting "these two accesses share a column path" into "therefore wait N cycles" requires timing, and timing is generation-specific while the classification is not — DDR4 and DDR5 classify identically and attach different separations. Keeping the layers separate is what lets one classifier serve both.
"You have per-level block counters and the profile is spread evenly. What now?" First check the counters sum to the request count, because if they do not, the instrumentation is counting every unsatisfied level rather than the first obstacle, and a dominated profile will look distributed. That single arithmetic check validates or invalidates everything else. Then check whether any two levels' counts move together as the workload changes, because one bad address-mapping decision can cause conflicts at two levels and fixing them separately will fail. Then check how many requests are outstanding at once — if little is blocked anywhere and throughput is still low, the memory system is idle and the limit is upstream. And a genuinely even profile after all that is not a failure: it means the bottleneck moves as each level is fixed, which is what a balanced system near its ceiling looks like.
12. Engineering Check
A system with 2 channels, 2 ranks per channel, 2 bank groups of 2 banks. Reason structurally.
1. How many rows can be open simultaneously across the whole system? 2 channels × 2 ranks × 4 banks = 16. Each bank holds one open row, and banks exist per rank, per channel. Note that the devices within a rank do not multiply this — Chapter 5.4 §2's lockstep means a rank's banks are one device's banks, just wider.
2. How many data transfers can be in flight simultaneously? Two — one per channel. Ranks share their channel's bus, so only one rank per channel transfers at a time. Sixteen open rows and two transfers: the gap between those numbers is the entire reason the hierarchy exists — overlap the slow work, serialise the fast work.
3. A request is blocked and block_level reads RANK. What is true and what is unknown? True: the channel was ready and another rank owns the data bus. Unknown: everything about the bank — whether the row is open, whether it is the right row. The evaluation is top-down and stops at the first obstacle, so lower levels were never examined. A common error is reading "not BANK" as "the bank is fine".
4. The workload alternates ranks every access. Which counters rise, and is remapping the fix? cnt_blk_rank rises, and each access pays a handoff. The fix is not a remapping that spreads differently — it is to stop alternating: batch accesses by rank. Chapter 5.4 §13 showed alternating ranks gains nothing, because both ranks' internal work can overlap without alternating the transfers. This is the level where the instinct that helps bank groups actively hurts.
5. The workload alternates two rows in one bank. Which counters rise, and what does adding ranks or channels do? cnt_blk_bank rises and every access needs needs_precharge and needs_activate. Adding ranks or channels does nothing — the conflict is inside one bank, and the other 15 banks sit idle. The fix is a mapping that puts those two rows in different banks, which converts a conflict into two independent row hits at no hardware cost.
6. cnt_blk_channel + cnt_blk_rank + cnt_blk_bank + cnt_ready exceeds the number of requests evaluated. What does that mean? The instrumentation is broken, and every conclusion from it is unreliable. The counters are meant to partition the requests — each request increments exactly one — and a sum exceeding the request count means some request was counted at more than one level, which happens when the evaluation reports every unsatisfied level rather than the first. Check the partition before trusting any ratio: §10's first mechanism, and the cheapest check in this entire module.
13. Summary
Six levels, each following one pattern: something was shared, replicating it created parallelism, and the replication brought a new selection mechanism and a new conflict.
Channel replicates the whole interface and shares nothing — so its failure is imbalance, not contention. Rank replicates devices but shares the data bus, so every transfer serialises and every change costs a handoff. Bank group replicates the column path partially, so same-group accesses are delayed rather than blocked. Bank replicates row state, so different rows in one bank conflict completely. Below them, row and column are selections within one held row.
The sharing shrinks as you go up and the parallelism grows with it — which is not a coincidence but the definition, since parallelism is created by replication and destroyed by sharing.
The controller's entire state is short: per-channel everything, rank bus ownership, previous bank group, per-bank open row. It tracks structure and availability and never data — which is why it cannot detect a wrong-row read, and why structural correctness needs assertions while data correctness needs a scoreboard.
And the picture's value is one question. "Why is memory slow" is unanswerable; "which level is blocking this request" has exactly one answer at any moment and implies a specific fix. Blocked at the channel means distribution; at the rank means batching; at the bank group means interleaving; at the bank means remapping rows. Those fixes conflict — bank groups want rapidly changing address bits and ranks want the opposite — so the choice must be informed by measurement rather than reasoning.
One level is not like the others. Channel, rank and bank conflicts are genuine blocks; a bank-group conflict is a separation requirement, and converting a classification into a delay needs timing — which this module deliberately never carried, so that one classifier serves every generation unchanged.
And the instrumentation lesson is the closing one. Per-level counters are only meaningful if they partition the requests: each request counted at exactly one level, the first obstacle. Categories that overlap produce confident, detailed, wrong pictures — and checking that the counts sum to the request count is the cheapest and most skipped validation in memory performance work.
14. What Comes Next
Module 5 built the structure. Everything from here fills it in.
Module 6, DDR Signals, starts at the bottom of what this module took for granted: the actual wires. Every chapter here has referred to "the command bus", "the data bus", "the chip select" and "the clock" as abstractions with roles. Module 6 asks what they physically are — how many there are, what each carries, which are shared and which are per-rank or per-device, and why the set is organised as it is.
From there Module 7 gives those signals meaning as commands, Module 8 gives addresses their fields, Module 9 makes activate and precharge concrete, and Modules 13 and 14 finally attach the timing this module has consistently declined to invent.
Everything they add attaches to a level this module named — which was the point.
Return to The DDR Device Structure for the storage/selector/shared-resource sorting, Banks for the row state at the heart of it, or The Memory Wall Problem for why none of this is optional. The full path is on the DDR tutorials index.
Continue learning
Related tutorials
- Related topic
The DDR Device Structure
A DDR device is mostly selectors and shared resources wrapped around one kind of storage. Sorting every structure into those three categories is what makes banks, bank groups, ranks and channels arrive as consequences rather than as vocabulary to memorise.
- Related topic
Performance Impact
Two request streams with identical addresses and counts can demand more than twice the row-state work, decided only by their order — and the instrumentation that measures it lies in specific, recognisable ways.
- Related topic
Data-Transfer Efficiency
Bursting buys command efficiency and can spend payload efficiency to get it. Those are different quantities that trade against each other, and collapsing them into a single percentage is how architectural arguments go wrong.
- Related topic
DDR4 / DDR5 Bank-Group Concepts
The same-group penalty is not one number. It reaches twice for column commands, tracks that for activates on x8 parts, and on one verified x16 configuration disappears entirely.
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.
