Skip to content
VLSI Mentor

DDR · Module 16

Bank-Aware Scheduling

A bank-aware controller evaluates every bank's candidate at once and produces a legal-candidate vector. The discipline that makes it correct is legality before policy — and the bug that defeats it is replacing timing legality with a bank comparison.

Chapter 16.1 bounded how much concurrency exists, and deliberately treated “a bank is ready” as an input it could not compute. This chapter computes it.

The shape of the computation is the lesson:

A bank-aware controller evaluates every bank's candidate simultaneously and produces a legal-candidate vector — and it determines legality completely before any policy runs.

Chapter 13.1 built command_legality_layers, which evaluates one candidate against three questions. A controller with sixteen banks has up to sixteen candidates every cycle, and the vector form is not just the scalar repeated — it changes what the block can report, what a checker can verify, and which bugs are reachable.

1. Candidate Reasoning, Before Any Algorithm

Start with a concrete situation and reason about it by hand, because naming an algorithm first is how the legality question gets skipped.

Current bank state:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  bank 0    OPEN,  row 12
  bank 1    CLOSED
  bank 2    OPEN,  row 8
  bank 3    PRECHARGING

Pending requests:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  A  →  bank 0, row 12
  B  →  bank 1, row 3
  C  →  bank 2, row 20
  D  →  bank 3, row 4

Before asking which to issue, ask what each one needs. Chapter 9.3's taxonomy answers that, and the answers differ:

RequestBank stateRow matchClassificationNext command needed
Aopen, row 12yesrow hitcolumn command
Bclosedclosed bankACTIVATE
Copen, row 8norow conflictPRECHARGE, then ACTIVATE, then column
Dprechargingbank busynothing yet

2. Three Filters, in Order

A candidate survives to the selection stage only by passing three filters, and the order is fixed.

Filter 1 — state legality. Does the bank's current state permit the command this request needs? A column command needs the bank open on the right row; an ACTIVATE needs it closed; a PRECHARGE needs it open. Chapter 5.2 owns the state and Chapter 13.1 §2 owns the question.

Filter 2 — timing legality. Have the applicable obligations expired? This is where cross-bank constraints enter, and §5 is about the way they get skipped.

Filter 3 — shared-resource availability. Chapter 16.1 §2's form B: one command bus, one command per cycle. Even a set of candidates that all pass filters 1 and 2 yields at most one issued command.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  requests

  [1] state legality        per-bank, from bank state

  [2] timing legality       per-bank AND cross-bank

  [3] shared resource       command bus, one per cycle

  legal candidate vector    ← THIS CHAPTER STOPS HERE

  policy selection          ← Module 17

3. Why the Order Is Not Negotiable

A controller could be written the other way round — choose a favoured request, then check whether its command is legal, and if not try the next. That design works. It is also the wrong shape, for three reasons that compound.

It conflates two different kinds of “no”. Chapter 13.1 §6 established that a state refusal needs work done on the request's behalf while a timing refusal needs only patience. A choose-then-check loop reports both as “try the next one” and loses the distinction.

It makes the policy's input non-deterministic. If the candidate set depends on the order in which requests were tried, then the policy is choosing from a set that is itself a function of the policy's previous behaviour. Reasoning about fairness or starvation becomes very hard.

It cannot report why nothing issued. With legality computed first, an empty vector is a fact about the machine state — and §6's block can say which filter emptied it. With choose-then-check, an empty result is just a loop that finished.

4. The Trap — bank != previous_bank

Now the bug this chapter exists to make findable. It is the most common defect in bank-aware controllers and it passes every functional test.

A designer knows that most timing constraints relax across banks. tRCD, tRAS, tRP and tRC are all bank-local — a different bank genuinely is unconstrained by them. So the shortcut suggests itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  timing_legal  =  (candidate_bank != previous_command_bank)

It is cheap, it is plausible, and it answers a different question.

The structural defence is the vector itself. A per-bank timing input that is computed from a bank comparison is visibly a bank comparison, whereas a scalar timing_legal hides where it came from. §6's block takes per-bank timing legality as an input vector precisely so the question of what produced it cannot be dodged.

The bank-aware legality pipeline as a sequence across three filters. Pending requests are presented together with the per-bank state held by the bank state table. The first filter asks, for each bank independently, whether that bank's current state permits the command the request needs, producing a state-legal vector. The second filter applies timing obligations, which include both bank-local obligations and cross-bank obligations such as activate spacing and the rolling activation window, producing a timing-legal vector. The third filter applies shared-resource availability, since the command bus carries one command per cycle. The result is a legal-candidate vector together with an attribution saying which filter eliminated each bank. The diagram ends there: selection among the legal candidates is a policy decision owned by the controller module and is deliberately outside this pipeline.RequestsBank stateTimingCandidate maskPolicy (M17)target bank + row1 — state-legalvectorcommand class needed2 — timing-legalvectorlegal candidates +why-notselection — NOT thischapter

5. RTL — The Per-Bank Candidate Mask

Collision check. Chapter 13.1 §7's command_legality_layers evaluates one candidate against semantic, state and timing layers and reports which refused. Chapter 9.3's row_request_classifier classifies one request against bank state. Chapter 5.2's ddr_bank_state_table holds per-bank state. Chapter 16.1's activate_concurrency_budget reports the cross-bank activation budget.

All four are reused, and none of them produces a vector. command_legality_layers is a scalar decision function; running sixteen copies would produce sixteen independent verdicts and no reduction, no attribution, and no shared-resource stage — which is where the interesting content is.

The engineering problem. Evaluate every bank's candidate in one cycle, produce a legal-candidate vector, and report which filter eliminated each non-candidate — because §3 established that an empty vector should be a diagnosable fact rather than a loop that finished.

Classification: controller-side combinational legality evaluation over a bank vector. A pure function of its inputs; the state it consults lives elsewhere.

What it does not model. No array internals, no analog behaviour. No bank state — Chapter 5.2 owns it and supplies it. No timing measurement — the per-bank timing verdicts arrive as an input vector, computed by the Module 13 and 14 machinery, and §4 explains why that is a deliberate structural choice rather than laziness. And no selection: there is no priority encoder in this block, because that is Chapter 17.1.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
//  bank_candidate_mask
//
//  CLASSIFICATION
//    Controller-side combinational legality evaluation over a BANK
//    VECTOR. A pure function; no clock, no state.
//
//  WHAT IT MODELS
//    §2's three filters applied to every bank at once:
//      [1] state legality   -- does this bank's state permit the
//                              command this request needs?
//      [2] timing legality  -- have the applicable obligations
//                              expired? INPUT, see below.
//      [3] shared resource  -- one command bus, one command/cycle.
//    plus per-bank ATTRIBUTION: which filter eliminated each bank.
//
//  WHAT IT DOES NOT MODEL
//    No array internals, no analog behaviour.
//    No bank state -- Chapter 5.2's ddr_bank_state_table owns it and
//      supplies open/row via the inputs.
//    No timing MEASUREMENT -- the per-bank timing verdicts arrive as
//      an input vector from the Module 13/14 machinery. That is
//      deliberate: §4 shows the common bug is computing timing
//      legality from a bank comparison, and making it an explicit
//      per-bank INPUT forces the question of what produced it.
//    NO SELECTION. There is deliberately no priority encoder and no
//      "selected" output: choosing among legal candidates is policy,
//      owned by Chapter 17.1. §3 argues the omission is the point.
//
//  RELATIONSHIP TO EXISTING RTL
//    Chapter 13.1's command_legality_layers is the SCALAR ancestor --
//    one candidate, three layers, a refusal code. This is the vector
//    form, and the vector adds three things the scalar cannot have:
//    a reduction (any_legal), a shared-resource stage that only makes
//    sense across candidates, and attribution across banks.
//
//  COMMAND CLASS ENCODING
//    Each request names the command its state requires, which
//    Chapter 9.3's row_request_classifier determines:
//      0 = column command   (bank open, row matches)
//      1 = ACTIVATE         (bank closed)
//      2 = PRECHARGE        (bank open, row differs)
//      3 = none             (bank busy / no request)
//
//  SIMULTANEITY
//    several banks legal at once -> ALL are reported. Reducing to one
//        here would be selection, which this block does not do.
//    no bank legal -> any_legal low and the attribution vectors say
//        why, per bank. §3's third argument.
//
//  GENERATION SCOPE
//    Generation-neutral. NUM_BANKS is a parameter; the timing inputs
//    carry whatever generation's obligations apply.
// ─────────────────────────────────────────────────────────────────────
module bank_candidate_mask #(
  parameter int NUM_BANKS = 16,
  parameter int ROW_W     = 17,
  parameter int BK_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS)
) (
  // ── One pending request per bank. A controller with deeper queues
  //    presents its best candidate per bank; Chapter 17.2 owns the
  //    queues that choose it.
  input  logic [NUM_BANKS-1:0]            req_valid,
  input  logic [NUM_BANKS-1:0][ROW_W-1:0] req_row,

  // ── Per-bank state, from Chapter 5.2's ddr_bank_state_table.
  input  logic [NUM_BANKS-1:0]            bank_open,
  input  logic [NUM_BANKS-1:0][ROW_W-1:0] bank_open_row,
  // A bank mid-transition cannot accept anything yet.
  input  logic [NUM_BANKS-1:0]            bank_busy,

  // ── FILTER 2's input, PER BANK. §4: a scalar here would hide
  //    whether it came from real obligations or from a bank
  //    comparison. Three vectors, one per command class, because a
  //    bank can be legal for a PRECHARGE and illegal for a column
  //    command at the same instant (Chapter 14.3's window).
  input  logic [NUM_BANKS-1:0]            timing_ok_column,
  input  logic [NUM_BANKS-1:0]            timing_ok_activate,
  input  logic [NUM_BANKS-1:0]            timing_ok_precharge,

  // ── FILTER 3. Shared, not per-bank: Chapter 16.1 §2's form B.
  input  logic                            cmd_bus_available,

  // ── What each bank's request needs, per the encoding above.
  output logic [NUM_BANKS-1:0][1:0]       needed_cmd,

  // ── The three filter outputs, published separately so a consumer
  //    and a checker can see where a bank was lost.
  output logic [NUM_BANKS-1:0]            state_legal,
  output logic [NUM_BANKS-1:0]            timing_legal,

  // ── THE output of this chapter.
  output logic [NUM_BANKS-1:0]            candidate_legal,
  output logic                            any_legal,

  // ── Attribution: why each requesting bank is NOT a candidate.
  //    Mutually exclusive per bank, and all low for a legal bank.
  output logic [NUM_BANKS-1:0]            blocked_by_state,
  output logic [NUM_BANKS-1:0]            blocked_by_timing,
  output logic                            blocked_by_bus,

  // ── Observability for §7's trace and §9's checker.
  output logic [NUM_BANKS-1:0]            is_row_hit,
  output logic [NUM_BANKS-1:0]            is_row_conflict
);

  // ── Elaboration guards.
  if (NUM_BANKS < 1) begin : g_banks
    initial $fatal(1, "bank_candidate_mask: NUM_BANKS must be >= 1");
  end
  if (ROW_W < 1) begin : g_row
    initial $fatal(1, "bank_candidate_mask: ROW_W must be >= 1");
  end

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

  logic [NUM_BANKS-1:0] t_ok;

  always_comb begin
    for (int unsigned b = 0; b < NUM_BANKS; b++) begin

      // ── Classification. Chapter 9.3's taxonomy, applied per bank.
      //    A busy bank is classified as needing nothing: it is
      //    mid-transition and no command is state-legal yet.
      is_row_hit[b]      = req_valid[b] && !bank_busy[b] && bank_open[b]
                        && (req_row[b] == bank_open_row[b]);
      is_row_conflict[b] = req_valid[b] && !bank_busy[b] && bank_open[b]
                        && (req_row[b] != bank_open_row[b]);

      if (!req_valid[b] || bank_busy[b]) begin
        needed_cmd[b] = CMD_NONE;
      end else if (is_row_hit[b]) begin
        needed_cmd[b] = CMD_COL;
      end else if (is_row_conflict[b]) begin
        needed_cmd[b] = CMD_PRE;
      end else begin
        needed_cmd[b] = CMD_ACT;          // bank closed
      end

      // ── FILTER 1. State legality is exactly "the classification
      //    produced a command", because the classification was
      //    derived FROM the state. A bank with no request, or busy,
      //    has nothing state-legal.
      state_legal[b] = (needed_cmd[b] != CMD_NONE);

      // ── FILTER 2. Select the timing verdict matching the command
      //    the state requires. Selecting the WRONG vector here is a
      //    real and subtle bug: a bank can be timing-legal for a
      //    precharge and illegal for a column command in the same
      //    cycle (Chapter 14.3's readable-not-closeable window).
      unique case (needed_cmd[b])
        CMD_COL: t_ok[b] = timing_ok_column[b];
        CMD_ACT: t_ok[b] = timing_ok_activate[b];
        CMD_PRE: t_ok[b] = timing_ok_precharge[b];
        default: t_ok[b] = 1'b0;
      endcase
      timing_legal[b] = state_legal[b] && t_ok[b];

      // ── FILTER 3 and the result.
      candidate_legal[b] = timing_legal[b] && cmd_bus_available;

      // ── Attribution. Mutually exclusive, and only meaningful for a
      //    bank that actually had a request -- a bank with nothing to
      //    do is not "blocked".
      blocked_by_state[b]  = req_valid[b] && !state_legal[b];
      blocked_by_timing[b] = req_valid[b] && state_legal[b] && !t_ok[b];
    end

    any_legal = (candidate_legal != '0);

    // The bus blocked everything only if something would otherwise
    // have been a candidate. Reported once, not per bank, because the
    // resource is shared -- Chapter 16.1 §2.
    blocked_by_bus = !cmd_bus_available && (timing_legal != '0);
  end

endmodule

Interface contract. timing_ok_* are three separate per-bank vectors, not one. That is the structural point of §4: a scalar would hide its provenance, and three vectors force a design to state which obligation set it evaluated for which command class. candidate_legal is the chapter's output and there is no selected portChapter 17.1 owns that.

Parameter contract. NUM_BANKS == 1 is legal and degenerates to the scalar case, which is the sanity configuration §9 uses; note it also makes every cross-bank timing input meaningless, so it is precisely the configuration in which §4's bug is invisible.

Why three timing vectors and not one. A bank can be timing-legal for one command class and illegal for another in the same cycleChapter 14.3 established the window where a column command is legal and a PRECHARGE is not. A single per-bank timing bit cannot represent that, and a design using one would either over-permit precharges or under-permit column commands.

Why the classification and filter 1 are the same computation. state_legal is needed_cmd != NONE, which looks like a tautology and is not: the classification is derived from the state, so a bank that is busy or has no request produces no command and is therefore not state-legal. Writing it this way makes the derivation visible rather than duplicating the state tests.

Corner cases. No request on a bank: not a candidate and not blocked — attribution is only meaningful for a bank that wanted something. Busy bank with a request: blocked_by_state, which is correct and distinct from a timing block. All banks legal: all reported, and the reduction to one is the consumer's job. Bus unavailable with legal candidates: candidate_legal is empty and blocked_by_bus says why — a distinct third reason. req_row equal to bank_open_row on a closed bank: correctly classified as needing ACTIVATE, because the open-row field of a closed bank is meaningless and bank_open gates it.

Synthesis implications. Per bank: one row comparator of ROW_W bits, a small classifier, a 4-way mux on the timing vectors, and two AND gates. For 16 banks and 17-bit rows that is sixteen 17-bit comparators — the dominant cost, and unavoidable since row matching is the classification. Entirely combinational, so it sits in the issue path and its depth matters; a real controller would likely register the classification.

Failure modes. Computing timing_ok_* from a bank comparison — §4's trap, invisible here because the inputs look correct. Using one timing vector for all command classes — over-permits whichever class has the looser obligation. Dropping filter 3 — reports several candidates as issuable in one cycle when the bus carries one. Adding a priority encoder — builds Chapter 17.1 and collapses legality into policy, which is the confusion the whole pipeline prevents. Making attribution non-exclusive — a bank reported as blocked by two filters sends a debugger to both.

6. The Worked Trace — Four Banks

§1's situation, now with timing, solved cycle by cycle.

History up to cycle 20:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cycle  6   ACT bank 1  (later precharged)
  cycle 10   ACT bank 0  → row 12
  cycle 14   ACT bank 2  → row 8
  cycle 16   column command to bank 0
  cycle 19   PRE bank 3  → bank 3 now precharging

Evaluating at cycle 20:

BankRequestClassNeedsFilter 1 stateApplicable timingFilter 2Candidate?
0row 12row hitcolumn✓ open, row matchestRCD → 14 ✓; tCCD → 16+4 = 20YES
1row 3closedACTIVATE✓ closedtRRD → 14+2 = 16 ✓; tFAW: 2 of 4 used ✓YES
2row 20conflictPRECHARGE✓ open, row differstRAS → 14+8 = 22no
3row 4busy✗ prechargingno

The tFAW check, worked: activations at cycles 6, 10 and 14. At cycle 20 the window spans cycles 9 through 20, so the activation at 6 has aged out and two remain — 10 and 14. Two of four used, so bank 1's ACTIVATE fits.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  candidate_legal   =  { bank 0 , bank 1 }
  blocked_by_state  =  { bank 3 }          precharging
  blocked_by_timing =  { bank 2 }          tRAS, 2 cycles short
  any_legal         =  yes

And the part this chapter stops at. Two candidates are legal: bank 0's column command (a row hit, cheap, serves a request immediately) and bank 1's ACTIVATE (opens a row that will serve a request later). The command bus carries one.

Which to issue is not a legality question. Both are legal; the device will accept either. Chapter 16.3 shows that both choices are defensible and that the better one depends on traffic the controller cannot see, and Chapter 17.1 owns making the decision.

7. Four Assertions Worth Writing

Where these live. bank_candidate_mask is combinational and has no clk or rst_n port — it is a pure function of its inputs. A concurrent property needs a sampling event, so these belong in a testbench or a bind unit, sampled on the surrounding environment's clock. Chapter 13.2 §9 develops what that costs: the properties check once per sampling edge rather than on every input change, which is acceptable here because the vector is consumed on an edge anyway — a downstream stage registers its decision.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── P1. THE safety property: a candidate passed all three filters.
//    Catches a design that permits a bank on state legality alone,
//    which is §4's trap arriving at the output rather than the input.
property p_candidate_passed_all_filters;
  @(posedge clk) disable iff (!rst_n)
    ( (candidate_legal & ~(state_legal & timing_legal)) == '0 )
    && ( (candidate_legal != '0) |-> cmd_bus_available );
endproperty
a_candidate_passed_all_filters: assert property (p_candidate_passed_all_filters);

// ── P2. Timing legality is evaluated against the command the STATE
//    requires. Catches the single-timing-vector bug: a bank whose
//    state needs a PRECHARGE must be judged on the precharge timing,
//    not the column timing. Chapter 14.3's readable-not-closeable
//    window is exactly when the two differ.
//    genvar-wrapped: a procedural for-loop is illegal in a property.
generate
  for (genvar gb = 0; gb < NUM_BANKS; gb++) begin : g_class
    property p_timing_matches_needed_command;
      @(posedge clk) disable iff (!rst_n)
        state_legal[gb] |->
          ( timing_legal[gb] ==
              ( (needed_cmd[gb] == 2'd0) ? timing_ok_column[gb]
              : (needed_cmd[gb] == 2'd1) ? timing_ok_activate[gb]
              :                            timing_ok_precharge[gb] ) );
    endproperty
    a_timing_matches_needed_command:
      assert property (p_timing_matches_needed_command);
  end
endgenerate

// ── P3. Attribution is exclusive and complete for any requesting
//    bank that is not a candidate. Catches a report that names two
//    filters, which sends a debugger to both, and one that names
//    none, which leaves an empty vector unexplained -- §3's third
//    argument for computing legality first.
generate
  for (genvar gc = 0; gc < NUM_BANKS; gc++) begin : g_attrib
    property p_attribution_is_exclusive_and_complete;
      @(posedge clk) disable iff (!rst_n)
        ( (req_valid[gc] && !candidate_legal[gc])
            |-> ( $countones({blocked_by_state[gc], blocked_by_timing[gc]}) == 1
               || blocked_by_bus ) )
        and ( candidate_legal[gc]
            |-> (!blocked_by_state[gc] && !blocked_by_timing[gc]) );
    endproperty
    a_attribution_is_exclusive_and_complete:
      assert property (p_attribution_is_exclusive_and_complete);
  end
endgenerate

// ── P4. Classification is derived from STATE, not from intent.
//    A row hit requires the bank open AND the rows equal; a conflict
//    requires open AND rows differing. Catches a classifier that
//    trusts a requester's claim, which Chapter 9.3 §8 warns about,
//    and one that reads a closed bank's meaningless open-row field.
generate
  for (genvar gd = 0; gd < NUM_BANKS; gd++) begin : g_classify
    property p_classification_from_state;
      @(posedge clk) disable iff (!rst_n)
        ( is_row_hit[gd] == (req_valid[gd] && !bank_busy[gd] && bank_open[gd]
                             && (req_row[gd] == bank_open_row[gd])) )
        and ( is_row_conflict[gd] == (req_valid[gd] && !bank_busy[gd]
                             && bank_open[gd]
                             && (req_row[gd] != bank_open_row[gd])) );
    endproperty
    a_classification_from_state: assert property (p_classification_from_state);
  end
endgenerate

// ── C1. The interesting vector states are REACHED. The multi-
//    candidate cover is the one people omit, and it is the state that
//    makes this a vector problem rather than a scalar one.
c_multiple_candidates: cover property (@(posedge clk) disable iff (!rst_n)
                                         ($countones(candidate_legal) >= 2));
c_empty_but_requesting: cover property (@(posedge clk) disable iff (!rst_n)
                                         ((req_valid != '0) && !any_legal));
c_blocked_by_bus:      cover property (@(posedge clk) disable iff (!rst_n)
                                         blocked_by_bus);

What these prove. That a candidate cleared all three filters; that timing was judged against the command the state requires; that the attribution is exclusive and complete; and that classification comes from state rather than from a requester's claim.

What these do not prove, and the first is the chapter's central limitation.

Nothing here proves timing_ok_* is correct. They are inputs. A design that computes them as bank != previous_bank satisfies every property above — the vectors are well-formed, the selection among them is right, the attribution is exact, and the controller violates tRRD and tFAW under sustained cross-bank traffic. §4 is the whole warning and no property of this block can catch it, because the block cannot see what produced its input. The check has to be against the obligations themselves, which is Chapter 16.1's budget block and Module 14's guards.

Nothing here proves the bank state is current. Chapter 5.2 owns it, and a stale open-row field produces confident misclassification — §10's second entry.

And nothing here concerns selection, because there is none. A property asserting something about a chosen command belongs in Chapter 17.1.

Vacuity. P2 and P3's antecedents need a requesting bank in the relevant condition; with a single-bank configuration C1's multi-candidate cover can never hit, which is precisely the configuration in which the vector's value is invisible.

8. DV — Reconstructing the Vector Independently

Invert the representation. The block evaluates a vector combinationally in one pass. A checker should hold an associative record per bank — last activate cycle, open row, last command — and recompute each bank's classification and legality from the observed command history, not from the design's state inputs. Chapter 9.3 §8's rule applies unchanged: classify from state, and derive the state from what was actually issued.

Compute timing legality from obligations, never from a bank comparison. This is the chapter-specific obligation and §7's first limitation is why. A checker that consumes the design's timing_ok_* inherits §4's bug perfectly. It must apply the Module 14 obligations itself — bank-local ones per bank, and cross-bank ones from the shared history.

Check the attribution, not only the verdict. A vector that is right for the wrong reason is a design that will mislead its next debugger.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CANDIDATE CLASSIFICATION ERROR
    cycle             : 4188
    bank              : 6
    request row       : 0x0241
    ─────────────────────────────────────────────────────────────────
    bank state (reference, from command history):
      last ACT        : cycle 4102, row 0x0241
      last PRE        : none since
      → OPEN, row 0x0241
    reference class   : ROW HIT        → needs column command
    design class      : ROW CONFLICT   → needs PRECHARGE
    ─────────────────────────────────────────────────────────────────
    consequence       : design would issue PRE + ACT + column,
                        three commands where one was legal,
                        and would close a row it should have used
    root question     : is bank 6's open-row field stale?

and:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CROSS-BANK TIMING ESCAPE
    cycle             : 2051
    bank              : 12   (ACTIVATE)
    design timing_ok  : TRUE
    ─────────────────────────────────────────────────────────────────
    reference obligations for an ACTIVATE:
      tRRD  last ACT any bank @ 2050  → legal from 2054   VIOLATED
      tFAW  window [2040..2051] holds 4 → legal from 2056  VIOLATED
      tRC   last ACT bank 12  @ 1900  → legal from 1974    ok
    ─────────────────────────────────────────────────────────────────
    banks differ?     : yes — 12 vs 7
    root question     : was timing_ok computed as (bank != prev_bank)?

The second report's closing question is the one worth stealing. It names §4's bug directly, and the banks differ? yes line is what makes the diagnosis immediate — the design thought a different bank was sufficient, and the reference shows two cross-bank obligations that a bank comparison cannot see.

9. Debugging

Symptom. Traffic spread across many banks behaves worse than expected, or a timing checker fires on commands the controller believed were legal.

Candidate mechanismEvidenceDiscriminator
Timing legality computed from a bank comparisonViolations only under sustained cross-bank traffic; single-bank tests cleanThe decisive check: find a violation and ask whether the two commands targeted different banks. If they always do, the comparison is standing in for the obligation. §4.
Stale open-row metadataMisclassification: hits reported as conflicts or vice versa§8's first report. Reconstruct the bank's state from command history and compare.
Single timing vector for all command classesA bank precharges too early, or a column command is refused while a precharge is permitted§5's three-vector argument. Chapter 14.3's window is where they differ.
Filter 3 omittedSeveral commands selected in one cycleThe command bus carries one. Chapter 16.1 §2.
Legality computed after selectionEmpty result cannot be explained; fairness hard to reason about§3. Look for a choose-then-check loop.
Attribution collapsed to one bitCannot tell a busy bank from a timing-blocked one§6's callout — both become available together by coincidence and mean different things.
Cross-bank obligation applied per bankRate scales with bank countChapter 16.1 §10. tRRD and tFAW are not per-bank.

The discriminator that defines this chapter is whether the violating pair targeted different banks. If every violation involves two different banks, the design is treating bank difference as sufficient — and that single observation identifies §4's bug without any further instrumentation. If violations occur within one bank too, the problem is elsewhere.

The second discriminator is whether misclassification correlates with recent activity on that bank. A stale open-row field misclassifies only after the row changed, so the errors cluster after activations.

Responsible layer. If the vector is correct, the attribution is exact, and throughput still disappoints, this chapter is exonerated: the question becomes which legal candidate is being chosen, which is policy — Chapter 16.3 for the trade and Chapter 17.1 for the mechanism.

10. Common Misconceptions

“Different bank means the command is legal now.” Tempting because bank-local obligations genuinely do relax across banks, and it is the cheapest possible check. Why it is wrong: §4 — tRRD, tFAW and tCCD are cross-bank, and Chapter 16.1 §4 showed tFAW is the binding constraint in every verified configuration. Consequence: a controller that violates under exactly the traffic it was designed for, with single-bank tests clean. Replacement model: bank comparison answers state independence; timing legality needs the obligations. Debugging clue: every violating pair targets different banks.

“Bank-aware scheduling is the same thing as a DDR scheduler.” Tempting because both concern choosing commands and the word scheduling is in the name. Why it is wrong: §3 — this stage computes a policy-independent statement of what the device will accept. Choosing among the results is a different problem with no single right answer, owned by Module 17. Consequence: legality and preference implemented in one pass, so neither can be verified or changed independently. Replacement model: a legality pipeline feeding a policy stage. Debugging clue: a module that both computes legality and picks a winner.

“If a request is legal, it should be issued.” Tempting because legality is the hard part and issuing seems like the obvious next step. Why it is wrong: §6's trace ends with two legal candidates and one command bus. Legality is necessary and not sufficient, and Chapter 13.4 §5 established that minimum separations permit deferral. Consequence: a controller that issues the first legal thing it finds, which is a policy — an unexamined one. Replacement model: legality produces a set; policy chooses from it. Debugging clue: no explicit policy anywhere, and behaviour that depends on bank index order.

“A row hit is always the best request to schedule.” Tempting because a row hit is genuinely the cheapest command sequence — one command against three. Why it is wrong: cheapest for that request is not best for the system. §6's trace has a row hit competing with an activation that would unlock a second bank's future traffic. Chapter 16.3 works the trade. Consequence: a policy that starves banks needing activation and concentrates work. Replacement model: row hits are cheap, and cheapness is one input to a choice. Debugging clue: excellent row-hit rate with disappointing throughput.

“Legality and arbitration are the same stage.” Tempting because both narrow a set of requests and it seems wasteful to traverse twice. Why it is wrong: §3's three arguments — they conflate two kinds of refusal, make the policy's input depend on the policy, and lose the ability to explain an empty result. Consequence: a design whose fairness properties cannot be stated, because the candidate set is policy-dependent. Replacement model: filter, then choose. Debugging clue: fairness reasoning that has to talk about the order requests were examined in.

“One timing-legal bit per bank is enough.” Tempting because the pipeline diagram shows one filter and one vector per stage. Why it is wrong: §5 — a bank can be timing-legal for a PRECHARGE and illegal for a column command at the same instant, which is Chapter 14.3's window. Consequence: whichever class has the looser obligation over-permits. Replacement model: one verdict per command class per bank, selected by what the state requires. Debugging clue: precharges issued during the minimum active interval, or column commands refused while precharges are permitted.

“A DV checker should copy the controller's bank-state table.” Tempting because the table is right there and copying guarantees agreement. Why it is wrong: guaranteed agreement is the failure. A checker sharing the design's state shares its staleness, and §8's first report — a misclassification caused by a stale open-row field — becomes undetectable. Consequence: a checker that certifies the bug it was written to find. Replacement model: reconstruct state from the observed command history. Debugging clue: a checker that has never disagreed with its design.

11. Interview Reasoning

“How does a controller decide what to issue when it has requests to several banks?” In two stages that must not be merged. First it computes legality for every bank's candidate at once — state legality from the bank's own state, timing legality from the applicable obligations including the cross-bank ones, and shared-resource availability since the command bus carries one command. That produces a legal-candidate vector, which is a policy-independent statement of what the device will accept. Then, separately, a policy chooses one. The reason to keep them apart is that the first has one correct answer and the second does not.

“Why must legality be determined before arbitration policy?” Three reasons that compound. It keeps the two kinds of refusal distinct — a state refusal needs work done on the request's behalf, a timing refusal needs only patience. It makes the policy's input deterministic, rather than a function of the order the policy happened to try things in, which is what makes fairness arguments possible at all. And it lets an empty result be explained: with legality first, you can say which filter eliminated each bank.

“What is the most common bug in a bank-aware controller?” Replacing timing legality with a comparison of bank indices. It is cheap and plausible, because the bank-local obligations really do relax across banks — but activate spacing, the rolling activation window and column spacing are all cross-bank, and the rolling window is typically the binding constraint. The failure signature is memorable: single-bank tests pass and violations appear only under sustained cross-bank traffic, so the design fails more the better it spreads work. The diagnostic is to check whether every violating pair targeted different banks.

“Four requests, four banks, one open on the right row. What happens?” Each needs a different command, which is the point. The row hit needs a column command; a closed bank needs an activate; a bank open on the wrong row needs a precharge first; a bank mid-transition needs nothing yet. So they are constrained by different obligations — column spacing, activate spacing and the window, the minimum active interval, and the precharge interval respectively. Legality has to be computed per candidate against its own applicable set, and then at most one issues because there is one command bus.

“Why would you want one timing verdict per command class rather than one per bank?” Because a bank can be legal for one command and not another at the same instant. The clearest case is a freshly activated bank: the column command becomes legal well before the precharge does, so a single bit would either permit the precharge too early or refuse the column command needlessly. Making the verdict depend on the command the bank's state requires is what keeps both correct.

“How would you verify a bank-aware selector independently?” Reconstruct each bank's state from the observed command history rather than reading the design's table — otherwise a stale open-row field is invisible to both. Compute timing legality from the obligations themselves, never from the design's timing inputs, because that is precisely where the bank-comparison bug lives and no property of the selector can see it. And check the attribution as well as the verdict: a vector that is right for the wrong reason will mislead the next person to debug it.

12. Engineering Exercises

1. Classify and find the candidates. State: bank 0 open row 5; bank 1 open row 9; bank 2 closed; bank 3 open row 5. Requests: bank 0 → row 5, bank 1 → row 2, bank 2 → row 5, bank 3 → row 7. Give each classification, the command needed, and which filter each must clear.

Worked: bank 0 — row hit, column command, needs tCCD and tRCD. Bank 1 — conflict (9 ≠ 2), PRECHARGE, needs tRAS. Bank 2 — closed, ACTIVATE, needs tRRD and tFAW. Bank 3 — conflict (5 ≠ 7), PRECHARGE, needs tRAS. The instructive detail: banks 0 and 3 both involve row 5 and are completely unrelated — Chapter 9.3 §3 established row numbers repeat per bank, so the coincidence means nothing.

2. Apply the timing filter. Using §6's educational values, with ACT bank 2 at cycle 30, ACT bank 1 at cycle 33, and now cycle 35: is an ACTIVATE to bank 0 timing-legal? Which obligation binds?

Worked: tRRD from the most recent activate at 33 gives 33 + 2 = 35 — satisfied. tFAW with activations at 30 and 33 uses 2 of 4 — satisfied. Legal. Now suppose activations had occurred at 26, 28, 30 and 33: the window [24..35] holds four, so the budget is full and the earliest legal cycle is when the one at 26 ages out, at 38. tFAW binds, which Chapter 16.1 §4 showed is the normal case.

3. Construct the trap's blind spot. Write a two-command sequence that a bank != previous_bank check permits and that violates a real obligation. Then write one it correctly refuses.

Worked: Permits and violates: ACT bank 0 at cycle 100, ACT bank 7 at cycle 101. Banks differ so the check passes; tRRD requires at least 2 (educational) or 4 (verified DDR4-3200 x8), so it is a violation. Correctly refuses: ACT bank 0 at 100, column command bank 0 at 101 — same bank, so the check refuses, and tRCD also refuses. The comparison is right for same-bank cases and wrong for cross-bank ones, which is why it survives casual testing.

4. Size the misclassification cost. A stale open-row field causes a row hit to be classified as a conflict. How many commands does the controller issue instead of one, and what does it do to the row?

Worked: it issues PRECHARGE, ACTIVATE, then the column command — three commands instead of one — and it closes and reopens the row it was already sitting on. So the cost is not only two extra commands but a full tRP plus tRCD of added latency, and the row it destroyed was the one the request wanted. The reverse error, a conflict misread as a hit, is worse: it issues a column command against the wrong row and returns wrong data.

5. Explain the attribution. In §6's trace, banks 2 and 3 both become available at cycle 22. Explain why reporting them identically would be a mistake, and construct a case where they do not coincide.

Worked: bank 3 is mid-transition — state blocked — and bank 2 is open and waiting on tRAStiming blocked. In §6 they coincide by accident. Move bank 3's PRECHARGE to cycle 15 and its tRP completes at 18, while bank 2's tRAS still completes at 22 — four cycles apart, and a controller that had conflated them would have missed bank 3's availability for four cycles.

6. Argue the omission. §5's block has no selection output. Make the case that a priority encoder belongs here, then rebut using §3 and the Module 17 boundary.

7. Find the single-vector bug. A design uses one timing_ok per bank. Construct a cycle at which it permits an illegal PRECHARGE, using Chapter 14.3's window.

13. Summary

Chapter 16.1 bounded the concurrency available. This chapter computes which banks can actually be used right now, and the answer takes the form of a vector rather than a decision.

Four requests to four banks need four different commands — a column command, an ACTIVATE, a PRECHARGE, and nothing — constrained by four different obligation sets. So legality must be evaluated per candidate against its own applicable set, not by a single comparison.

Three filters, in a fixed order. State legality from the bank's own state; timing legality from the applicable obligations, including the cross-bank ones; and shared-resource availability, because the command bus carries one command per cycle no matter how many banks are ready.

Legality is computed completely before policy runs, and the order is not negotiable. Choose-then-check conflates two kinds of refusal that demand opposite responses, makes the policy's input a function of the policy's own history, and leaves an empty result unexplainable. This chapter produces the vector and stops — selection is Chapter 17.1's, and §5's block deliberately contains no priority encoder, because adding one would suggest the choice is part of legality.

The trap is bank != previous_bank. It is cheap, plausible, and answers state independence while pretending to answer timing legality. It cannot see tRRD, tFAW or tCCD — and Chapter 16.1 showed tFAW is the binding constraint in every verified configuration, so it misses the one that matters most. Its signature is that single-bank tests pass and violations need sustained cross-bank traffic, so the design fails more the better it spreads work.

bank_candidate_mask takes timing legality as three per-bank input vectors, one per command class — because a bank can be legal for a precharge and not a column command in the same cycle, and because a scalar input would hide whether it came from real obligations or from a bank comparison. It publishes attribution per bank, which §6's trace showed separates a mid-transition bank from a timing-blocked one — two conditions that coincided by accident there and demand different responses in general.

And the limitation that matters: no property of this block can prove its timing inputs are real. A design computing them from a bank comparison passes every assertion here. The check belongs with the obligations themselves.

14. What Comes Next

§6's trace ended with two legal candidates and one command bus, and this chapter declined to choose between them — a row hit in one bank against an activation that would open a second.

Chapter 16.3 takes up that choice, and the first thing it establishes is that the two obvious objectives conflict. A policy that always takes the row hit maximises row-hit rate and can concentrate all work in one bank, starving the activation that would have created future concurrency. A policy that always spreads across banks maximises bank usage and destroys locality the workload had.

Row-hit rate and bank-level parallelism are different quantities, and improving one can reduce the other. Neither is the objective; both are proxies, and the chapter's job is to show the mechanism of the trade rather than to declare a winner.

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.