Skip to content
VLSI Mentor

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

Nine levels of a DDR memory subsystem from the requester down to the cells. A requester issues a memory request to the memory controller, which holds the scheduling state and per-bank model. The PHY handles data capture and trained timing. A channel provides an independent command and data path. A module packages devices and any buffering layer. A rank is the set of devices supplying the data width and owning the shared bus. A bank group shares an internal column data path. A bank owns row state. The row buffer holds one sensed row, above the cells that store the bits.RequesterCores and accelerators · issues addresses, not structureCores and accelerators · issues addresses, not structureMemory controllerScheduling · per-bank model · address mapping · Module 17Scheduling · per-bank model · address mapping · Module 17PHYData capture · trained and calibrated timing · Modules 19–21Data capture · trained and calibrated timing · Modules 19–21ChannelOwn CA bus, own DQ bus, own scheduling · shares nothingOwn CA bus, own DQ bus, own scheduling · shares nothingModulePhysical packaging · any buffering layer · not addressedPhysical packaging · any buffering layer · not addressedRankDevices supplying the width · owns the shared data busDevices supplying the width · owns the shared data busBank groupShares an internal column data path · DDR4 and DDR5Shares an internal column data path · DDR4 and DDR5BankOwns row state · one open row at a timeOwns row state · one open row at a timeRow buffer and cellsSense amplifiers hold one row · Modules 2 and 3Sense amplifiers hold one row · Modules 2 and 3
Figure 1 — from requester to cell; each band owns a resource, a selection and a conflict.

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:

LevelReplicatesStill sharesSelected byController tracksConflict
Channelthe whole interfacenothingchannel fieldall state, per channelimbalance — not contention
Rankthe devicesthe data buschip selectwho owns the busevery transfer + handoff
Bank groupthe column pathdevice data pathgroup fieldthe previous groupconsecutive same-group access
Bankrow statecolumn path + interfacebank fieldopen, and which rowdifferent rows, same bank
Rowthe row bufferrow field
Columnthe row buffercolumn 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 lockstepChapter 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// 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

endmodule

Cycle 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:

CycleObstacle presentblock_levelneeds_prechargeneeds_activate
0channel not readyCHANNEL00
1rank 1 owns the busRANK00
2bank 0 closedBANK01
4row 5 openNONE00
5now wants row 7BANK11
9row 7 openNONE00

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 cycles
Ten cycles. A request is first blocked at the channel level, then at the rank level once the channel is ready, then at the bank level because the bank is closed and needs an activate. After the row opens the request is ready. A subsequent request for a different row in the same bank is blocked at the bank level again, this time needing a precharge before the activate, and becomes ready only after the close and open complete.descending the hierarchydescending the hierarchyconflict: close then openconflict: close then openblocked at rank levelblocked at rank levelrow hit — readyrow hit — readynew row — conflictnew row — conflictclkreq_validreq_row5555577777block_levelCHRANKBANKBANKNONEBANKBANKBANKBANKNONEreq_readyneeds_prechargeneeds_activatebank_opent0t1t2t3t4t5t6t7t8t9
Figure 2 — one request clearing each level in turn, then hitting a row conflict.

block_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 raisedOwned by
What the signals physically areModule 6 — DDR Signals
The actual command encodingsModule 7 — DDR Commands
How addresses form the fieldsModules 8, 18 — Addressing, Address Mapping
What activate and precharge really doModule 9 — Activate and Precharge
Reads, writes, burstsModules 10–12
How long anything takesModules 13, 14 — Timing
Refresh, which closes banksModule 15
Banks and bank groups in depthModule 16
Choosing among pending requestsModule 17 — Controller Architecture
Capturing data at speedModules 19–21 — PHY, DQS, Training
Termination and signal integrityModule 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

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.