Skip to content
VLSI Mentor

DDR · Module 5

Banks

A bank is the scope of row state, and its value is isolation: an operation on one bank cannot disturb another. That guarantee has exactly one deliberate exception, and one silent failure mode that only the controller's own model can prevent.

Chapter 5.1 sorted every structure in a DDR device into storage, selectors and shared resources, and claimed that each level of the hierarchy exists because replicating a shared resource was worth the die area.

The bank is the first such level, and the resource it replicates is row state.

This chapter asks what having many of them actually buys, which resources stay bank-local and which remain shared above them, and — the part Module 3 never reached — what makes an operation on a bank legal given that bank's own state, and what guarantees it cannot disturb any other bank.

That isolation guarantee turns out to have exactly one deliberate exception, and one failure mode that produces wrong data with no error reported anywhere. The second is the reason a memory controller tracks per-bank state at all.

1. The Problem a Bank Solves

Start from a device that has no banks — one array, one set of sense amplifiers.

Chapter 3.5 established that reading DRAM means activating a row into the sense amplifiers, which then hold it. Chapter 2.5 established the read is destructive and Chapter 2.6 that restoration is mandatory. So the sense amplifiers can hold exactly one row at a time, and moving to a different row means closing the current one and opening the new one.

In a monolithic array that means the entire device has one row context. Two requests to different rows cannot overlap in any useful way — the second must wait for the first's row to be closed and its own to be opened, and that sequence is the expensive part of a DRAM access. Every access to a different row serialises against every other, regardless of how unrelated the two requests are.

The response is to partition. Divide the array into independent sections, give each its own sense amplifiers and its own row decoder, and each section can hold its own open row. A bank is one such section, and the number of banks is the number of rows that can be open simultaneously.

A monolithic array has a single row context, so every access to a different row must close the current row and open the new one, and requests serialise. Partitioning the array into banks gives each bank its own sense amplifiers and row decoder, so N banks hold N open rows and requests to different banks can overlap.Monolithic arrayone row contextOne open rowdevice-wideRequests serialiseclose, open, waitPartitionedown sense amps eachN open rowsone per bankRequests overlapdifferent banksgivesgivespartition12
Figure 1 — partitioning replicates row state: N banks means N rows can be open at once.

Note what partitioning does not do. It does not make any single access faster — a row still takes as long to open and the sense amplifiers still take as long to resolve. It creates the opportunity for overlap, which is Chapter 1.8 §7's memory-level parallelism appearing as a device-structural property. Whether that opportunity is taken depends on the access pattern and the controller, which is a theme this whole module keeps returning to.

2. Bank Is Not Four Other Things

Because "bank" gets used loosely, it is worth nailing down what it is not. Each confusion below produces a specific wrong prediction.

A bank is not a row. A row is a set of cells sharing one wordline — Chapter 3.4's subject. A bank contains many thousands of rows and holds one of them open at a time. Confusing them makes "how many banks" and "how many rows" look like the same question; they are different axes, and 5.1 §11 showed two organisations with identical capacity differing four-fold in bank count.

A bank is not a rank. A bank is inside a device. A rank is a set of devices operating together — 5.4's subject. Every device in a rank has its own full set of banks, so a rank does not add banks to a device; it adds devices, each with their own.

A bank is not a channel. A channel is an independent interface path with its own command bus — 5.5's subject. Banks within a device all share one command path and one data interface; channels share nothing. Banks give overlapping internal work; channels give genuinely parallel transfers.

And a bank is not a cache way. This one is worth spelling out because the analogy is tempting and completely wrong in direction. Cache ways are alternative places the same address may live — they exist to give a set more associativity, and a lookup searches them. Banks are a disjoint partition of the address space — an address lives in exactly one bank, determined by its bank field, and nothing searches. Ways multiply the places to look; banks divide the space so you need not look.

3. What a Bank Owns, and What It Still Shares

The isolation a bank provides is real but bounded, and drawing the boundary precisely is the point of this section.

Resources sorted by sharing scope. Bank-local resources are the row state, the sense amplifiers holding the open row, and the row decoder, and these permit full overlap between banks. The column data path is shared at the bank-group level in DDR4 and DDR5 and at the device level before bank groups existed, so it permits only partial overlap. The command path and external data interface are shared by the whole device and permit no overlap at all.Bank-localrow state · sense ampsGroup-sharedcolumn data pathDevice-sharedcommand + DQ pathFull overlapthis chapterPartial overlapchapter 5.3No overlapone at a timegivesgivesgives12
Figure 2 — resources by sharing scope: only the bank-local ones give full overlap.

Bank-local: the row state, the sense amplifiers holding the open row, and the row decoder. These are replicated per bank, which is exactly why a bank costs die area — Chapter 3.5 established that sense amplifiers are large, and a bank needs a full row's worth.

Group-shared: the column data path, and this needs a generation qualification. In DDR4 and DDR5, banks are organised into bank groups and the internal column path is shared at the group level — that is Chapter 4.5's subject and 5.3's. Before bank groups existed, the column path was shared at the device level, so all banks contended for it equally. The structural principle is the same in both cases; only the scope of the sharing changed.

Device-shared: the command path and the external data interface. Every bank's commands arrive on one command bus and every bank's data leaves through one interface. No amount of bank parallelism changes this, which is the honest limit on what banks buy: they let the slow internal work overlap, and the fast external transfer remains strictly serialised.

That is the shape of the whole benefit. Banks overlap the expensive part — activating and restoring a row — and cannot overlap the cheap part. Since the expensive part is much longer, the trade is strongly favourable, and that is why bank counts grew from a handful to 32.

4. The Failure Mode That Reports Nothing

Here is the fact that makes per-bank tracking mandatory rather than merely useful.

A column command does not carry a row address. It names a column within whatever row is currently open in the addressed bank. The device has no idea which row the controller intended — it simply serves the row it is holding.

So if the controller's model of which row is open is wrong, the device returns data from the wrong row, correctly, with no error signalled anywhere.

5. RTL — Per-Bank State, Legality, and Isolation

Engineering problem

A controller must maintain, for every bank, whether a row is open and which one; decide whether each incoming operation is legal against that bank's state; classify column accesses as hits or as the silent mismatch of §4; and guarantee that an operation naming one bank does not alter any other bank's state — with one deliberate exception.

Classification

SYNTHESIZABLE RTL. A per-bank state table with legality checking — genuine logic that every memory controller contains, and the structure Module 17 builds a scheduler on top of.

It models controller-visible bank state, not a physical DRAM bank. There are no cells, no sense amplifiers, no charge, no restoration and no timing here. A real activate takes time and this model treats it as instantaneous, because the timing is Modules 13 and 14' subject and mixing it in would obscure the state machine. The open bit means "the controller believes a row is open", which is precisely the thing §4 says must not drift from reality.

Interface

cmd_valid with cmd_type, cmd_bank and cmd_row presents an operation. The outputs split into classification (row_hit, bank_open), hazards (col_row_mismatch), and legality (illegal_activate, illegal_precharge, illegal_column_closed, bank_invalid). open_vector exposes the whole table for observability.

State

One open bit and one open_row register per bank. That is the entire state, and its smallness relative to its importance is worth noticing.

Combinational logic

Selected-bank lookup, row comparison, and the legality decisions — all derived from the table rather than held separately, so there is one source of truth about each bank.

Sequential logic

State transitions on legal commands only. An illegal command changes nothing, which is the "report, never repair" discipline this curriculum has used throughout.

Simulation

vlog ddr_bank_state_table.sv tb_ddr_bank_state_table.sv then vsim -c tb_ddr_bank_state_table -do "run -all"; VCS vcs -sverilog ddr_bank_state_table.sv tb_ddr_bank_state_table.sv && ./simv; Xcelium xrun -sv ddr_bank_state_table.sv tb_ddr_bank_state_table.sv.

Expected output: the directed sequence of §7 produces two row hits, one col_row_mismatch, one illegal_activate, and an open_vector that returns to zero after the all-bank precharge.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// DDR BANK STATE TABLE.  Classification: SYNTHESIZABLE RTL.
//
// The controller's model of per-bank row state, plus the legality rules
// that model implies. This is the single most important structure in a
// memory controller, and the reason is Section 4: a COLUMN COMMAND CARRIES
// NO ROW ADDRESS, so if this model drifts from the device's real state the
// device returns data from the wrong row with NO ERROR ANYWHERE.
//
// MODELS CONTROLLER-VISIBLE STATE, NOT A PHYSICAL BANK. No cells, no sense
// amplifiers, no charge, no restoration, NO TIMING. A real activate takes
// time; here it is instantaneous, because timing is Modules 13/14 and
// mixing it in would obscure the state machine. `open` means "the
// controller believes a row is open".
//
// ISOLATION IS THE HEADLINE PROPERTY: a command naming one bank must not
// alter any other bank's state. There is exactly ONE deliberate exception,
// and it is CMD_PREA.
// ─────────────────────────────────────────────────────────────────────────
module ddr_bank_state_table #(
  parameter int NUM_BANKS = 16,
  parameter int ROW_W     = 16,
  // Guarded so NUM_BANKS == 1 yields a 1-bit field rather than the illegal
  // zero-width one $clog2(1) would give.
  parameter int BA_W      = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS)
) (
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 cmd_valid,
  // 0 ACT, 1 PRE, 2 COL, 3 PREA. Abstract operation classes, NOT DDR
  // command encodings -- Module 7 owns those, including how a real device
  // distinguishes reads from writes and single-bank from all-bank
  // precharge. This block cares only what each class does to STATE.
  input  logic [1:0]           cmd_type,
  input  logic [BA_W-1:0]      cmd_bank,
  // For ACT: the row to open. For COL: the row the controller BELIEVES is
  // open. A real column command carries no row address -- it is supplied
  // here purely so the model can detect the drift of Section 4.
  input  logic [ROW_W-1:0]     cmd_row,

  // ── Classification of the addressed bank.
  output logic                 bank_open,
  output logic [ROW_W-1:0]     open_row_sel,
  output logic                 row_hit,

  // ── THE SILENT HAZARD. A column access to an open bank whose held row is
  //    not the row the controller intended. On real hardware this returns
  //    wrong data and signals nothing.
  output logic                 col_row_mismatch,

  // ── Legality. Reported, never repaired: an illegal command changes no
  //    state, because silently "fixing" it would hide a controller bug.
  output logic                 illegal_activate,
  output logic                 illegal_precharge,
  output logic                 illegal_column_closed,
  output logic                 bank_invalid,

  // ── Observability: the whole table, for assertions and for debug.
  output logic [NUM_BANKS-1:0] open_vector
);

  localparam logic [1:0] CMD_ACT  = 2'd0;
  localparam logic [1:0] CMD_PRE  = 2'd1;
  localparam logic [1:0] CMD_COL  = 2'd2;
  localparam logic [1:0] CMD_PREA = 2'd3;

  // ── COMPILE-TIME legality.
  if (NUM_BANKS < 1) begin : g_nb_min
    initial $fatal(1, "ddr_bank_state_table: NUM_BANKS must be >= 1");
  end
  if (ROW_W < 1) begin : g_rw_min
    initial $fatal(1, "ddr_bank_state_table: ROW_W must be >= 1");
  end

  logic              bank_open_q [NUM_BANKS];
  logic [ROW_W-1:0]  open_row_q  [NUM_BANKS];

  // ── Bank-index range check, using Chapter 5.1's pattern.
  //    NOT `cmd_bank >= BA_W'(NUM_BANKS)`: that cast truncates to zero
  //    whenever NUM_BANKS is a power of two, making the test always true.
  //    Generated only where it can ever fire.
  if (NUM_BANKS >= (1 << BA_W)) begin : g_ba_full
    assign bank_invalid = 1'b0;
  end else begin : g_ba_check
    assign bank_invalid = cmd_valid && ({1'b0, cmd_bank} >= (BA_W+1)'(NUM_BANKS));
  end

  // ── Selected-bank view. Derived from the table so there is exactly one
  //    source of truth about any bank's state.
  assign bank_open    = !bank_invalid && bank_open_q[cmd_bank];
  assign open_row_sel = open_row_q[cmd_bank];

  logic row_match, cmd_ok;
  assign row_match = (open_row_sel == cmd_row);
  assign cmd_ok    = cmd_valid && !bank_invalid;

  // ── Classification and legality.
  assign row_hit = cmd_ok && (cmd_type == CMD_COL) && bank_open && row_match;

  // The silent hazard: open, but holding a row the controller did not
  // expect. Distinct from illegal_column_closed, because the DEVICE cannot
  // detect this one and can detect that one.
  assign col_row_mismatch = cmd_ok && (cmd_type == CMD_COL)
                         && bank_open && !row_match;

  // Activating an already-open bank. A real device requires a precharge
  // first; issuing ACT here means the controller lost track.
  assign illegal_activate = cmd_ok && (cmd_type == CMD_ACT) && bank_open;

  // Precharging a bank that is already closed. Harmless on some devices and
  // still a modelling error, so it is surfaced rather than absorbed.
  assign illegal_precharge = cmd_ok && (cmd_type == CMD_PRE) && !bank_open;

  // A column access with no row open: there is nothing to read.
  assign illegal_column_closed = cmd_ok && (cmd_type == CMD_COL) && !bank_open;

  always_comb begin
    for (int b = 0; b < NUM_BANKS; b++) open_vector[b] = bank_open_q[b];
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int b = 0; b < NUM_BANKS; b++) begin
        bank_open_q[b] <= 1'b0;
        open_row_q[b]  <= '0;
      end
    end else if (cmd_ok) begin
      unique case (cmd_type)

        CMD_ACT: begin
          // Legal only on a closed bank. Opens it and records the row.
          // ISOLATION: exactly one array element is written.
          if (!bank_open) begin
            bank_open_q[cmd_bank] <= 1'b1;
            open_row_q[cmd_bank]  <= cmd_row;
          end
        end

        CMD_PRE: begin
          // Legal only on an open bank. open_row_q is deliberately LEFT
          // ALONE rather than cleared: with bank_open low it is meaningless,
          // and not clearing it makes a stale-row bug visible in a waveform
          // instead of hiding it behind a plausible zero.
          if (bank_open) begin
            bank_open_q[cmd_bank] <= 1'b0;
          end
        end

        CMD_COL: begin
          // A column access never changes row state -- neither on a hit nor
          // on the silent mismatch. That is exactly why the mismatch is
          // dangerous: nothing about the device's state records that it
          // happened.
        end

        CMD_PREA: begin
          // ── THE DELIBERATE EXCEPTION TO ISOLATION.
          //    All-bank precharge closes every bank at once. It is the only
          //    operation here that touches a bank it did not name, and it
          //    exists because closing banks one at a time before a
          //    device-wide event (refresh, power-state change) would cost a
          //    command per bank. The exception is the reason the isolation
          //    assertion in Section 6 must be written with an explicit
          //    carve-out rather than as an unconditional invariant.
          for (int b = 0; b < NUM_BANKS; b++) bank_open_q[b] <= 1'b0;
        end

        default: ; // unreachable: cmd_type is fully covered above
      endcase
    end
  end

endmodule

Cycle trace

NUM_BANKS = 4. The sequence of §7's waveform:

CycleCommandopen_vector beforeResult
0ACT bank 0, row 50000bank 0 opens with row 5
1ACT bank 1, row 90001bank 1 opens — bank 0 untouched
2COL bank 0, row 50011row_hit
3COL bank 1, row 90011row_hit
4COL bank 0, row 70011col_row_mismatch — bank 0 holds row 5
5PRE bank 00011bank 0 closes
6ACT bank 0, row 70010bank 0 opens with row 7
7COL bank 0, row 70011row_hit
8ACT bank 1, row 30011illegal_activate — bank 1 still open
9PREA0011all banks close

Cycle 1 is the isolation guarantee and cycle 4 is §4's hazard. Cycle 8 shows an illegal command changing nothingopen_vector is unchanged at cycle 9, so bank 1 still holds row 9 and the controller's intent to open row 3 was simply refused.

Waveform expectation

§7. The signal to watch is open_vector: it should change only in the bit the command named, except at the PREA.

Synthesis implication

NUM_BANKS flip-flops for the open bits, NUM_BANKS × ROW_W for the rows, plus a multiplexer indexed by cmd_bank and a ROW_W-bit comparator. For 32 banks and a 17-bit row that is a few hundred flops — genuinely small, and a real controller's version is not much bigger. The indexed read open_row_q[cmd_bank] is the structure to watch: it is a multiplexer that grows with bank count and sits on the path to row_hit, which a scheduler wants early.

Corner cases

NUM_BANKS == 1 gives BA_W == 1 through the guard, and then the range check does materialise — one legal encoding out of two — which is 5.1 §5's degenerate case appearing again. PREA on an all-closed device is a no-op rather than an error, deliberately: it is routinely issued as a "make sure everything is closed" operation and flagging it would produce noise. A cmd_valid with an invalid bank index changes nothing and sets bank_invalid. Reset closes every bank. And open_row_q is not cleared on precharge, so a stale value persists — read the comment before calling that a bug.

Verification

What DV must prove: isolation — every command except PREA leaves all non-addressed banks bit-identical; legality — each illegal condition is reported and changes nothing; classificationrow_hit exactly when open and matching, col_row_mismatch exactly when open and not matching, and the two mutually exclusive; the PREA exception — it closes every bank, including ones already closed; reset from any state; and behaviour at NUM_BANKS == 1.

Debugging

If row_hit is asserting for the wrong bank, check that open_row_q is indexed by cmd_bank and not by a latched or stale index. If a command appears to disturb another bank, look for a loop variable shadowing cmd_bank in the always_ff — the PREA loop uses b, and reusing b elsewhere is the classic way to write to every bank by accident. If col_row_mismatch never fires in a system that is returning wrong data, check that cmd_row is carrying the controller's intent rather than being back-filled from open_row_sel — a model that copies the device's answer into its own expectation can never detect a disagreement, which is a self-fulfilling check and the most dangerous shape of bug in this block.

Limitations

No timing at all — activate and precharge are instantaneous here, and the real minimum intervals are Modules 13 and 14. No refresh, which closes banks on real hardware and is Module 15. No bank groups, so every bank looks equally independent — that is 5.3's correction and it matters. No data. No distinction between reads and writes, since both are column accesses as far as row state is concerned. And no notion of ranks or channels, so this is one device's worth of state.

6. Four Assertions Worth Writing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY, inside ddr_bank_state_table.

// P1 -- THE ISOLATION PROPERTY, and the reason this block exists.
// Every bank except the addressed one is unchanged, EXCEPT under PREA.
// Written per bank with a genvar because isolation is a claim about a
// RELATIONSHIP between a command and a bank it did not name, and a single
// scalar property cannot express it.
generate
  for (genvar b = 0; b < NUM_BANKS; b++) begin : g_isolation
    property p_other_banks_unchanged;
      @(posedge clk) disable iff (!rst_n)
        (cmd_ok && (cmd_type != CMD_PREA) && (cmd_bank != BA_W'(b)))
          |=> (bank_open_q[b] == $past(bank_open_q[b]))
           && (open_row_q[b]  == $past(open_row_q[b]));
    endproperty
    assert property (p_other_banks_unchanged);
  end
endgenerate

// P2 -- THE EXCEPTION, asserted explicitly rather than left as a hole in
// P1. A carve-out that is never itself checked is how an exception quietly
// becomes a bug: P1 would still pass if PREA did nothing at all.
property p_prea_closes_everything;
  @(posedge clk) disable iff (!rst_n)
    (cmd_ok && (cmd_type == CMD_PREA)) |=> (open_vector == '0);
endproperty
assert property (p_prea_closes_everything);

// P3 -- classification is exact and mutually exclusive. row_hit and
// col_row_mismatch describe the same command and must never both hold;
// and on a column access to an open bank, exactly one of them must.
property p_hit_mismatch_exclusive;
  @(posedge clk) disable iff (!rst_n)
    !(row_hit && col_row_mismatch);
endproperty
assert property (p_hit_mismatch_exclusive);

property p_hit_mismatch_complete;
  @(posedge clk) disable iff (!rst_n)
    (cmd_ok && (cmd_type == CMD_COL) && bank_open)
      |-> (row_hit ^ col_row_mismatch);
endproperty
assert property (p_hit_mismatch_complete);

// P4 -- an illegal command changes NOTHING. "Report, never repair" stated
// as a checkable property rather than as a comment.
property p_illegal_changes_nothing;
  @(posedge clk) disable iff (!rst_n)
    (illegal_activate || illegal_precharge || illegal_column_closed
                      || bank_invalid)
      |=> (open_vector == $past(open_vector));
endproperty
assert property (p_illegal_changes_nothing);

// P5 -- the state actually moves. P1 to P4 are all restrictive, and a
// design whose table never changes satisfies every one of them. This is
// the liveness-flavoured companion that makes the set meaningful.
property p_legal_activate_opens;
  @(posedge clk) disable iff (!rst_n)
    (cmd_ok && (cmd_type == CMD_ACT) && !bank_open && !illegal_activate)
      |=> (bank_open_q[$past(cmd_bank)]
           && (open_row_q[$past(cmd_bank)] == $past(cmd_row)));
endproperty
assert property (p_legal_activate_opens);

P1 is the chapter's property. Everything else about a bank follows from it: if an operation on one bank could disturb another, banks would not be independent and the whole hierarchy above them would be meaningless. It is also the property most likely to be omitted, because it is about what does not happen to things the command never mentioned — and nobody writes tests for banks they were not thinking about.

P2 exists because P1 has a carve-out. An exception that is never itself asserted is a hole: P1 permits PREA to do anything, including nothing. Whenever you write X, except under condition C, you owe a second property that pins down what happens under C — otherwise the exception is an unverified region shaped exactly like the one you consciously decided was special.

P5 is the liveness companion, the same lesson as 4.5's P4 and 5.1's P3. P1 through P4 are all restrictive, and a table that never changes satisfies all four perfectly.

What none of them prove. Nothing about data — the silent mismatch of §4 corrupts data and no property here can see the data, only the state divergence that causes it. Nothing about timing, since the model has none. And, most importantly, nothing about whether this model matches the real device. These properties prove the controller's table is internally consistent; they cannot prove it agrees with the DRAM. That agreement is established by a scoreboard against a device model, and it is the one check that catches §4's hazard end-to-end.

7. Two Banks, Overlapping

ddr_bank_state_table — independent banks, and the two ways a controller can be wrong

10 cycles
Ten cycles with four banks. Bank zero is activated with row five, then bank one with row nine, and the open vector shows both open without either disturbing the other. Column accesses to each matching row report row hits. A column access to bank zero naming row seven reports a row mismatch because bank zero holds row five. Bank zero is precharged and reopened with row seven. An activate to the still-open bank one is refused and changes nothing. A final all-bank precharge closes every bank.two banks open independentlytwo banks openindependentlyoverlapped column accessoverlapped column accessclose, reopen, misuseclose, reopen, misusebank 0 untouchedbank 0 untouchedCOL reads the WRONG rowCOL reads the WRONG rowACT on an open bankACT on an open bankclkcmd_typeACTACTCOLCOLCOLPREACTCOLACTPREAcmd_bank010100001--cmd_row59597--773--open_vector0133332333row_hitcol_row_mismatchillegal_activatet0t1t2t3t4t5t6t7t8t9
Figure 3 — isolation, a silent mismatch, and a refused command in ten cycles.

Cycle 1 is the isolation guarantee made visible. open_vector goes from 0001 to 0011 — bank 1's bit set, bank 0's bit and row untouched. Nothing about opening bank 1 required knowing or changing anything about bank 0.

Cycles 2 and 3 are the payoff. Two column accesses to two different banks, both hits, back to back. In a monolithic array those two accesses would have required closing one row and opening another between them.

Cycle 4 is the dangerous one, and notice what the trace does not show. col_row_mismatch asserts in this model — but on real hardware there would be no such signal. The device would serve row 5's data for a request the controller believed was for row 7, and every signal in the system would look correct. This model manufactures an observable for something that is, in reality, unobservable — which is exactly what a controller-side model is for.

Cycle 8 is a refusal. illegal_activate asserts and open_vector stays at 0011 into cycle 9. The controller asked to open row 3 in a bank still holding row 9; the operation changed nothing rather than half-happening.

And cycle 9 breaks isolation on purpose. PREA closes every bank — the one command in the set that touches banks it did not name.

8. The Bank, Answered Systematically

Applying the structural questions this module asks of every level:

QuestionAnswer for a bank
What does it contain?Many rows; one of them open at a time
What resource does it share?The column data path (group-scoped in DDR4/5, device-scoped before), the command path, the external interface
What can operate in parallel with it?Row activity in any other bank — the expensive part
How is it selected?A bank field in the request, plus a bank-group field where groups exist
What must the controller track?Open or closed, and which row — see §4
What opportunity does it create?Overlapping the activate-and-restore of independent requests
What conflict does it create?Two requests to different rows of the same bank serialise completely
What must DV verify?Isolation above all, then legality, then classification exactness

The conflict row is the one that costs real systems money. Banks give overlap between banks and nothing at all within one — so an access pattern that repeatedly hits different rows of the same bank gets no benefit from any number of banks, and 5.1 §9's first debugging mechanism is exactly this.

9. Common Misconceptions

"More banks means proportionally more bandwidth." Wrong mental model: bank count multiplies throughput. Engineering action: sizing a memory system by bank count; expecting a 32-bank device to deliver twice a 16-bank device's throughput; assuming a bank-rich device rescues a poor access pattern. Observable failure / bad conclusion: a device with many banks delivering no more than one with few, because the workload concentrates on one bank or because the shared external interface — not bank availability — was already the limit. Banks overlap internal work only; the data path stays serialised. Correct model: banks create the opportunity to overlap the expensive activate-and-restore portion of independent accesses. They do not widen the interface, do not speed a single access, and deliver nothing at all if requests do not spread across them. Prevention: ask what is actually saturated. If the data interface is busy, more banks change nothing; if it is idle while the device works, bank parallelism is the lever.

"A bank is like a cache way." Wrong mental model: banks are alternative locations for an address, providing associativity. Engineering action: reasoning about "which bank a line lands in" as a placement policy; expecting some search or replacement behaviour; imagining conflict misses in the cache sense. Observable failure / bad conclusion: a mental model in which bank conflicts could be reduced by a better placement policy, when in fact the bank is determined entirely by the address and the only lever is the address mapping — a completely different intervention, owned by Module 18. Correct model: banks are a disjoint partition of the address space. An address lives in exactly one bank, selected by its bank field. Nothing searches, nothing is replaced. Ways multiply the places to look; banks divide the space so you need not look. Prevention: ask whether any structure ever searches the banks. Nothing does.

"A column access can name any row in an open bank." Wrong mental model: a column command specifies row and column. Engineering action: omitting or under-maintaining the per-bank open-row model; issuing column accesses without confirming which row is open; treating the model as a performance optimisation that can be approximated. Observable failure / bad conclusion: wrong data with no error reported anywhere — §4's hazard. This is uniquely dangerous because the command was well-formed, the bank was open, the device behaved correctly, and nothing in the system flags it. Correct model: a column command names a column within whatever row is currently open in the addressed bank. The device does not know what the controller intended. The controller's per-bank open-row table is therefore a correctness structure, not an optimisation. Prevention: ask what the device would do if the controller's model were wrong. If the answer is "return plausible wrong data silently", the model is load-bearing.

"Banks are fully independent." Wrong mental model: bank isolation is total. Engineering action: assuming any two accesses to different banks can proceed simultaneously; modelling bank count as the concurrency limit; ignoring bank groups entirely. Observable failure / bad conclusion: a performance model that over-predicts, sometimes substantially, because it treats every pair of banks as equally independent when accesses within a bank group share a column path and accesses anywhere in the device share the command path and the interface. Correct model: banks are independent in row state, which is the expensive part, and share everything downstream of it. In DDR4 and DDR5 the column path is shared at the bank-group level; the command path and data interface are shared device-wide. Isolation is real and bounded. Prevention: name the resource. "Independent" is meaningless without saying independent in what, and 5.3 exists because the answer is not "everything".

10. Debugging — A Request Mutated the Wrong Bank's State

Symptom. A controller's per-bank model disagrees with the device. Accesses return wrong data intermittently, or activates are refused by the device that the controller believes should be legal. The divergence appears after a period of correct operation.

A model that drifts from reality is a different problem from a model that is wrong from the start, and the split is the first thing to establish: a systematically wrong model fails immediately, while a drifting one runs correctly until some specific event.

Mechanism 1 — an all-bank operation the model did not account for. Inspect: every operation that closes banks without naming one — all-bank precharge, and refresh, which closes banks on real hardware. Expected evidence: divergence beginning immediately after such an event, with the model believing banks are open that the device has closed. Discriminator: correlate the first divergence against device-wide events, not against the failing access. This is first because it is the most common cause and because Module 15's refresh is easy to omit from a model that was built around addressed commands. The failing access is usually innocent; the event that broke the model happened earlier.

Mechanism 2 — an indexing error writing more banks than intended. Inspect: the state-update logic for a loop variable shadowing the bank index, or a write that is not guarded by the bank comparison. Expected evidence: multiple banks changing on a single addressed command — visible directly in open_vector. Discriminator: how many bits of the open vector change per command. A command that alters more than one bit and is not an all-bank operation is this mechanism, conclusively. §6's P1 is written precisely to catch it.

Mechanism 3 — the model mirrors the device instead of predicting it. Inspect: whether the expected row on a column access is computed from the controller's own intent or copied from the device's state. Expected evidence: a mismatch check that never fires regardless of stimulus. Discriminator: inject a deliberate divergence and see whether anything reports it. If nothing does, the check is self-fulfilling — the §5 debugging note's most dangerous shape. A check that cannot fail is worse than no check, because it is trusted.

Mechanism 4 — two requesters sharing one bank model. Inspect: whether more than one scheduling entity issues commands against the same device without a single shared state table. Expected evidence: divergence correlated with concurrent activity rather than with any particular address. Discriminator: does the problem disappear when only one requester is active? Two models of one device inevitably disagree, and this is a structural defect rather than a bug in either model.

Mechanism 5 — not a model error: the device was reset or re-initialised. Inspect: whether anything reset, re-trained or power-cycled the device without the controller's model being cleared. Expected evidence: divergence from a known system event, with the model believing state that predates it. Discriminator: did anything touch the device outside the command path? Chapter 4.4 §3 established that DDR3-and-later devices carry measured state that a reset invalidates; the bank table is invalidated by exactly the same events, and a model that survives a device reset is stale by construction.

Discrimination, cheapest first. Count how many open-vector bits change per command — that is one observation and it resolves mechanism 2 outright. Then find the first divergence and look at what happened immediately before it rather than at the access that failed. Then inject a deliberate mismatch to confirm the check can fail at all. Then ask whether anything else drives or resets the device.

The reasoning lesson. When a model diverges from reality, the bug is almost never at the point of divergence — it is at the last event both agreed on. The instinct is to debug the failing access, and the failing access is usually correct code operating on state that was already wrong. Find the first disagreement, not the first symptom, and prefer a trace that records the model and the device side by side over one that records only outcomes. That is also why §6's P1 matters so much: isolation violations corrupt state long before anything reads it, and an assertion catches them at the moment they happen rather than thousands of cycles later.

11. Interview Reasoning

"Why partition a DRAM device into banks?" Because sense amplifiers can hold only one row at a time, so a monolithic array has a single row context and every access to a different row serialises against every other — the current row must be closed and the new one opened before anything moves. Partitioning gives each bank its own sense amplifiers and row decoder, so N banks hold N open rows and accesses to different banks can overlap the expensive activate-and-restore work. It does not make any single access faster; it creates the opportunity for overlap, and whether that opportunity is taken depends on the access pattern.

"What exactly makes two banks able to overlap work?" That the resource each one needs for the expensive part — its own sense amplifiers holding its own open row — is replicated per bank rather than shared. Parallelism comes from replication and is destroyed by sharing, so the question is always which resource is duplicated. Banks duplicate row state; they do not duplicate the column data path, the command path or the external data interface. That is why the overlap is real but bounded: the slow internal work overlaps and the fast external transfer stays serialised.

"Why does a controller need per-bank state?" Primarily for correctness, not performance. A column command carries no row address — it names a column within whatever row is currently open in the addressed bank. So if the controller's model of which row is open is wrong, the device serves data from the wrong row, the command was well-formed, the bank was open, and nothing anywhere signals an error. The per-bank open-row table is the only thing standing between a modelling error and silent data corruption. It also avoids redundant activates, but that is the secondary benefit.

"Is an operation on one bank guaranteed not to affect another?" Yes, with exactly one deliberate exception. Bank isolation is the whole value of having banks — a command naming one bank changes only that bank's state — and it is what makes the hierarchy above banks meaningful. The exception is all-bank precharge, which closes every bank at once, and it exists because closing banks individually before a device-wide event such as refresh or a power-state change would cost a command per bank. Refresh itself also closes banks on real hardware, which is the most common thing a controller model forgets.

"How is a bank different from a cache way?" They are opposite in direction. Cache ways are alternative places the same address may live — they provide associativity, and a lookup searches them. A bank is a disjoint partition of the address space: an address lives in exactly one bank, determined by its bank field, and nothing searches. Ways multiply the places to look; banks divide the space so you need not look. The practical consequence is that a bank conflict cannot be fixed by a placement or replacement policy the way a cache conflict can — the only lever is the address mapping, which lives in the controller.

12. Engineering Check

A device has 8 banks. Trace the following command sequence and answer the questions. Educational sequence; no timing is implied, and real minimum intervals are Modules 13 and 14.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. ACT bank 2, row 100
   2. ACT bank 5, row 200
   3. COL bank 2  (controller intends row 100)
   4. ACT bank 2, row 300
   5. PRE bank 2
   6. ACT bank 2, row 300
   7. COL bank 5  (controller intends row 200)

1. After command 2, how many rows are open and which? Two — row 100 in bank 2 and row 200 in bank 5. Command 2 did not disturb bank 2 in any way, which is the isolation guarantee.

2. Is command 3 a hit? Yes. Bank 2 is open holding row 100, and the controller intends row 100. Note that the command itself carries no row — the hit is a property of the agreement between the controller's model and the device's state.

3. What happens at command 4? It is illegal — an activate to a bank that is already open. A real device requires a precharge first. The controller has lost track, and the command should change nothing rather than half-succeed.

4. Commands 5 and 6 fix it. What is the cost? A precharge and an activate before any data moves — the full close-and-open sequence, which is the expensive part of a DRAM access. This is a row conflict, and it costs the same whether it was intentional or the result of the mistake at command 4.

5. Is command 7 a hit, and what would happen if commands 5 and 6 had accidentally targeted bank 5 instead of bank 2? As written it is a hit — bank 5 still holds row 200, untouched by everything that happened to bank 2. Had 5 and 6 targeted bank 5, it would hold row 300, and command 7 would be the silent mismatch of §4: the device would return row 300's data for a request the controller believed was for row 200, with no error anywhere. The controller's model would still say row 200. That divergence is invisible until someone checks the data, which may be much later and in a completely different part of the system.

6. The workload repeatedly accesses rows 100, 300, 100, 300 — all in bank 2. How much does having 8 banks help? Not at all. Every access is a row conflict in the same bank, so each one pays a full close-and-open, and the other seven banks sit idle. Bank parallelism is worth exactly nothing to a pattern that does not spread across banks — which is why the address mapping (Module 18) determines whether bank count is an asset or a specification number. Remapping so that rows 100 and 300 land in different banks would let the two alternate freely, with no change to the hardware.

13. Summary

A bank is the scope of row state, and it is the first level of the hierarchy to replicate a shared resource. Sense amplifiers hold one row at a time, so a monolithic array has one row context and every different-row access serialises. N banks means N rows open at once, and accesses to different banks can overlap the expensive activate-and-restore work.

Partitioning creates opportunity, not speed. A single access is no faster. Whether the overlap happens depends on the access pattern, and a workload confined to one bank gets nothing from any number of banks.

Isolation is bounded, and naming the resource is the discipline. Bank-local: row state, the sense amplifiers holding the open row, and the row decoder. Shared above: the column data path — at the bank-group level in DDR4 and DDR5, at the device level before bank groups existed — and the command path and external data interface, shared device-wide and never overlapped. Banks overlap the slow part and cannot overlap the fast part, which is why the trade is favourable.

A bank is not a row, a rank, a channel, or a cache way. The last is worth holding: ways are alternative places one address may live and are searched; banks are a disjoint partition of the address space and nothing searches them.

And the reason a controller tracks per-bank state is correctness, not performance. A column command carries no row address — it names a column within whatever row is open. If the controller's model is wrong, the device returns the wrong row's data, correctly, and signals nothing. That failure is undetectable by the device, which makes the per-bank open-row table the only thing preventing silent corruption.

Isolation has exactly one deliberate exception: all-bank precharge, which closes every bank at once because doing it individually before a device-wide event would cost a command per bank. An exception that is never itself asserted is a hole in the verification, which is why §6 pins it down separately.

14. What Comes Next

Chapter 5.3 corrects this chapter's most important simplification. §3 noted that the column data path is shared at the bank-group level in DDR4 and DDR5 and then largely set it aside; §5's RTL treats every bank as equally independent, which is not true of any modern device.

Bank groups exist because not all bank pairs are equally independent. Chapter 4.5 argued why they were introduced — DDR4 needed rate from overlap once prefetch could not deepen. 5.3 establishes what they structurally are: which resource is shared at which scope, how a request is classified against its predecessor, and why that classification is the interface every later timing module consumes.

Return to The DDR Device Structure for the storage/selector/shared-resource sorting this chapter applied, Sense Amplifiers for what a row buffer physically is, or Memory Matrices and Hierarchy for the array partitioning banks are built from. Module 16 is the full banks-and-bank-groups treatment. 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.