Skip to content
VLSI Mentor

DDR · Module 23

DDR Bandwidth

Chapter 12.4 named four efficiency measures and built three. This builds the fourth: every bus cycle charged to exactly one named cause, with the categories provably summing to the window.

Chapter 12.4 did something unusual and useful: it refused to let “efficiency” be one number. It named four measures with four different denominators — command efficiency, slot utilisation, payload efficiency, and system efficiency — and insisted that a claim is not a claim until it says what it divided by.

It then built a monitor for the first three. Measure D, system efficiency, it defined as useful bytes delivered over everything the memory system could have achieved, and noted that D “includes row-state work, turnaround and refresh, which none of the other three can see.”

That is the measure nobody built, and this chapter builds it. Doing so requires something the other three did not: an accounting where every cycle is charged to exactly one named cause, and the causes provably sum to the window.

The module's central law:

Peak bandwidth is a property of the interface. Achieved bandwidth is a property of the workload meeting the timing rules. The gap between them is not waste — it is the cost of constraints that cannot be removed, plus decisions that can be improved.

§4 sharpens that immediately with a result that surprised me when the simulation produced it: under one of §3's four workload models, achieved bandwidth is exactly 100% of peak. The gap is not inherent to DDR. It is a property of the workload, which changes what the question is.

1. Latency and Bandwidth Can Move in Opposite Directions

Chapter 23.1 §17 flagged this and deferred it. It is worth settling first, because the two objectives are routinely conflated and the conflation makes performance work incoherent.

Latency is how long one request takes. Bandwidth is how much data moves per unit time. A scheduler improves bandwidth by reordering requests so that consecutive commands can issue closer together — and reordering necessarily makes some request wait longer than it would have under first-come-first-served.

DecisionEffect on bandwidthEffect on the delayed request's latency
Reorder to group row hitsUp — fewer row transitionsUp — the passed-over request waits
Reorder to group same-direction accessesUp — fewer turnaroundsUp — same reason
Serve strictly in arrival orderDownDown for the oldest, up for everyone behind it

So they are not the same objective and a design must say which it is optimising. Chapter 23.4 quantifies the trade with both numbers; this chapter measures only the bandwidth side, and says so rather than implying an improvement is free.

2. Peak, Derived

Peak bandwidth is the one figure in this module that is pure arithmetic over verified inputs.

InputValueCategory
Data rate1600 MT/sVERIFIED — the bin name
Channel data width64 bits = 8 bytesVERIFIED — DDR4 DIMM, 64 data + 8 ECC
Transfers per clock2VERIFIED — double data rate
tCK1.25 nsVERIFIED

DERIVED: 2 beats/cycle × 8 bytes = 16 bytes per cycle. At 1.25 ns per cycle that is 16 / 1.25 ns = 12.8 GB/s. Equivalently 1600 MT/s × 8 bytes = 12.8 GB/s — two routes, same answer, which is the check worth doing.

And one more derived quantity that the rest of the chapter uses constantly: a BL8 burst is 8 beats, so 4 clock cycles, carrying 64 bytes.

3. Four Workload Models, Simulated

Here is where the numbers come from. Each model is a steady-state access pattern, stated completely, with the limiting parameter named. Every result was produced by running the model, not by asserting it.

ModelAccess patternLimited byBurst everyUtilisationAchieved
ARow hits, alternating bank groupstCCD_S = 44 cycles100.0%12.800 GB/s
BRow hits, same bank grouptCCD_L = 55 cycles80.0%10.240 GB/s
CRow misses, one banktRC = 3939 cycles10.3%1.313 GB/s
DRow misses, spread over bankstFAW = 205 cycles80.0%10.240 GB/s

All DERIVED from 23.1 §2's verified parameters. The arithmetic in each case is burst_cycles / gap_cycles for utilisation and 64 bytes / gap_cycles for the rate.

Four observations, and the last two are the interesting ones.

Model C is the disaster case and it is not subtle. One bank, every access a miss: tRC is the activate-to-activate period on the same bank, 39 cycles, and a 4-cycle burst fits inside it with 35 cycles of nothing. 10.3% of peak. Module 16's entire argument for bank-level parallelism is this row.

Model B's 20% loss is unavoidable, not a scheduling failure. tCCD_L is 5 cycles and a burst is 4, so one cycle in every five is a gap the protocol requires. No scheduler recovers it while staying in one bank group. This is §10's distinction in its cleanest form: a lost cycle that nobody could have used.

Models B and D land on exactly the same number, and that is an artefact. tFAW is 20 cycles and admits 4 activates, so 20 / 4 = 5 cycles per burst — identical to tCCD_L. At this speed bin the four-activate window and the same-bank-group column spacing impose the same ceiling. That coincidence does not hold at other bins, and reading it as a general law would be wrong.

Model A reaches 100%, which gets its own section.

4. Peak Is Attainable, Which Changes the Question

Model A's result is worth stating carefully because it is easy to disbelieve: tCCD_S is 4 cycles and a BL8 burst occupies 4 cycles, so consecutive column commands to different bank groups produce data on every slot. Utilisation is 4/4, exactly 1.

DERIVED, and it is exact rather than approximate. There is no residual gap to account for.

That result reframes the module's central law. The gap between peak and achieved is not a property of DDR — there exists a workload for which it is zero. So the useful question is not “why can't DDR reach peak” but:

Which properties of a workload move it from model A toward model C, and which of those can a controller change?

Model A requiresIf it failsMoves toward
Every access a row hitMisses and conflictsC — row work dominates
Consecutive accesses in different bank groupsSame bank groupBtCCD_L gaps
Accesses spread across banksConcentration on one bankCtRC-bound
No direction changesRead/write interleavingTurnaround cycles
A requester always readyIdle demandIdle cycles
No refreshRefresh is mandatory§6 — a hard 4.49%

Five of those six are workload or policy properties. Only refresh is unconditional. That is the honest content of the central law: most of the gap is not physics, and the parts that are physics are identifiable.

5. The Costs Compose Multiplicatively

Before building the accounting, one arithmetic result that the accounting depends on, because getting it wrong overstates every loss.

Take model B — 80% slot utilisation — and add refresh, which §6 shows makes the device unavailable 4.49% of the time. What is the combined result?

The instinct is to add the losses: 20% + 4.49% = 24.49% lost, so 75.51% achieved.

The correct composition is multiplicative: 80.0% × 95.51% = 76.41% achieved, so 23.59% lost.

MethodLostAchievedRate
Additive (wrong)24.49%75.51%9.665 GB/s
Multiplicative (correct)23.59%76.41%9.781 GB/s
Difference0.90 pp0.116 GB/s

Both DERIVED and recomputed. The reason is that the two costs apply to different denominators: refresh removes 4.49% of the total cycles, and tCCD_L wastes 20% of the cycles that remain. Adding them double-counts the overlap — the tCCD_L gaps that would have fallen inside a refresh window anyway.

6. Refresh — the One Unconditional Cost

Chapter 15.5 owns refresh overhead and supplies the verified inputs; this section takes only what §5's composition needs.

InputValueCategory
tREFI at 1X, normal temperature7.8 µsVERIFIED
tRFC1 at 8Gb350 nsVERIFIED

DERIVED: 350 ns / 7.8 µs = 4.49% of time the device is unavailable, so refresh alone caps achievable bandwidth at 95.51% of peak.

In cycles at this configuration: tREFI is 6240 cycles and tRFC1 is 280 cycles, and 280 / 6240 = 4.49% — the same figure by a second route.

This value independently reproduces 15.5's published table, which gives 1X | 350 ns | 7.8 µs | 4.49%. That cross-module agreement is the check worth reporting: two chapters derived the same fraction from the same verified inputs without sharing a calculation.

Refresh is the only cost in §4's table that no workload and no policy removes. Every other row is something a requester or a controller can influence. That makes 95.51% the true ceiling for this configuration and density, and it is the number any efficiency claim should be measured against rather than 100%.

7. Mixing Directions Halves It

§3's four models share an omission: every one of them is reads only. Real workloads mix directions, and the data bus is sharedModule 10 and Module 11 established that it changes ownership between directions and that the handover is scheduled rather than signalled.

So here is a fifth model, and it produces the largest single loss in the chapter after model C.

Model E: alternating read and write bursts, same bank group, all row hits.

ComponentCyclesCategory
Read data4DERIVED — BL8 at 2 beats/cycle
Read-to-write bus gap2ILLUSTRATIVE — see the callout
Write data4DERIVED
tWTR_L (write-to-read, same bank group)6VERIFIED
Total per read+write pair16of which 8 carry data

4 + 2 + 4 + 6 = 16, and the sum was checked. DERIVED: slot utilisation is 8 / 16 = 50.0%, achieving 128 bytes / 16 cycles = 8.000 B/cycle = 6.400 GB/sexactly half of peak.

Against model B's 80%, mixing directions costs 30 percentage points, which is 37.5% of model B's throughput rather than 30% of it — the distinction §5 insisted on.

Attributed over a 160-cycle window — ten complete pairs — the categories are:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  window                                     160
  DATA              10 pairs x 8 cycles    =  80
  TURNAROUND        10 x 2  (read-to-write) =  20
  TURNAROUND        10 x 6  (tWTR_L)        =  60
  everything else                           =   0
                                              ---
  SUM                                         160

  SUM CHECK : 80 + 20 + 60 + 0 = 160 == window  ->  HOLDS
  turnaround total = 80 cycles = 50.0% of the window

Half the window is direction changes. That is the largest avoidable loss in this chapter — and unlike tCCD_L's gap it is avoidable, because a scheduler can batch reads together and writes together instead of alternating. Chapter 23.4 quantifies exactly that, and §1's table already warned what it costs: the batched-behind request waits longer.

The Turnaround Tracker

§10's attributor takes c_turnaround as an input and nothing yet computes it. This block does, from the command stream.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// turnaround_tracker -- determines when the shared data bus must change
// direction, and for how long it is unusable.
//
// CLASSIFICATION: educational, synthesisable. Produces the
// c_turnaround input that cycle_attributor consumes.
//
// WHAT IT DOES NOT MODEL:
//   - the bus or its electrical handover (22.1 owns termination)
//   - the READ-TO-WRITE interval. DDR4 publishes no single parameter
//     for it: the real value is derived from CL, CWL and burst length
//     together. It is therefore a PARAMETER here, not a constant, and
//     §7's callout says why a default would be an invention.
//   - whether a turnaround was avoidable. §7: batching avoids it, and
//     23.4 owns that decision.
// ---------------------------------------------------------------------
module turnaround_tracker #(
  // VERIFIED at DDR4-1600: write-to-read, same and different bank group.
  parameter int TWTR_L = 6,
  parameter int TWTR_S = 2,
  // NOT VERIFIED as a single value -- see the header and §7's callout.
  parameter int T_RTW  = 2,
  parameter int GAP_W  = $clog2((TWTR_L > T_RTW ? TWTR_L : T_RTW) + 2)
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── A burst's last data cycle, with its direction and bank group.
  input  logic              burst_end,
  input  logic              burst_is_write,
  input  logic              burst_same_bg,

  // ── The next burst the scheduler wants to issue.
  input  logic              next_pending,
  input  logic              next_is_write,

  // ── To cycle_attributor.
  output logic              c_turnaround,
  output logic [GAP_W-1:0]  gap_remaining,
  output logic              direction_change,

  // ── Observability. Counting turnarounds separately from the cycles
  //    they cost is what distinguishes "how often" from "how much".
  output logic [15:0]       cnt_turnarounds,
  output logic [15:0]       cnt_turnaround_cycles,
  output logic              last_dir_was_write,

  output logic              err_gap_violated
);

  initial begin
    if (TWTR_L < 1 || TWTR_S < 1)
      $fatal(1, "turnaround_tracker: write-to-read intervals must be >= 1");
    if (T_RTW < 0)
      $fatal(1, "turnaround_tracker: T_RTW must not be negative");
    if (TWTR_S > TWTR_L)
      $fatal(1, "turnaround_tracker: TWTR_S (%0d) exceeds TWTR_L (%0d)",
             TWTR_S, TWTR_L);
  end

  logic             prev_write, have_prev;
  logic [GAP_W-1:0] gap;
  logic [15:0]      n_ta, n_ta_cy;

  // A direction change is needed when the pending burst's direction
  // differs from the last one completed. Declared above the assigns.
  logic change_needed;
  assign change_needed = have_prev && next_pending
                      && (next_is_write != prev_write);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      prev_write <= 1'b0;
      have_prev  <= 1'b0;
      gap        <= '0;
      n_ta       <= '0;
      n_ta_cy    <= '0;
    end else begin
      if (burst_end) begin
        // Load the gap this direction change requires. Write-to-read
        // uses the VERIFIED tWTR value for the bank-group relationship;
        // read-to-write uses the caller's parameter.
        if (have_prev && (prev_write != burst_is_write)) begin
          n_ta <= (n_ta == 16'hFFFF) ? n_ta : n_ta + 16'd1;
        end
        prev_write <= burst_is_write;
        have_prev  <= 1'b1;
        gap        <= burst_is_write
                        ? (burst_same_bg ? GAP_W'(TWTR_L) : GAP_W'(TWTR_S))
                        : GAP_W'(T_RTW);
      end else if (gap != '0) begin
        gap     <= gap - GAP_W'(1);
        n_ta_cy <= (n_ta_cy == 16'hFFFF) ? n_ta_cy : n_ta_cy + 16'd1;
      end
    end
  end

  assign c_turnaround      = (gap != '0);
  assign gap_remaining     = gap;
  assign direction_change  = change_needed;
  assign cnt_turnarounds   = n_ta;
  assign cnt_turnaround_cycles = n_ta_cy;
  assign last_dir_was_write    = prev_write;

  // A burst starting while a gap is still outstanding is a scheduling
  // violation: the bus had not finished changing hands.
  assign err_gap_violated = burst_end && (gap != '0);

endmodule

Two assertions belong with it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P11 -- the gap is loaded from the VERIFIED parameter matching the
// bank-group relationship. Getting tWTR_S and tWTR_L the wrong way
// round understates the cost on the common case.
property p_gap_matches_relationship;
  @(posedge clk) disable iff (!rst_n)
    (burst_end && burst_is_write)
      |=> (gap_remaining == (($past(burst_same_bg, 1))
                               ? GAP_W'(TWTR_L) : GAP_W'(TWTR_S)));
endproperty
assert property (p_gap_matches_relationship);

// P12 -- turnaround COUNT and turnaround CYCLES are tracked
// separately. One turnaround costing six cycles and six costing one
// each are different problems, and a single counter conflates them.
property p_cycles_at_least_count;
  @(posedge clk) disable iff (!rst_n)
    (cnt_turnarounds != '0) |-> (cnt_turnaround_cycles >= cnt_turnarounds);
endproperty
assert property (p_cycles_at_least_count);

// ── Cover: a write-to-read turnaround within a bank group -- the
// expensive case, and the one tWTR_L governs.
cover property (@(posedge clk) disable iff (!rst_n)
                  burst_end && burst_is_write && burst_same_bg);
// A read-to-write turnaround: the case whose interval is not a
// published parameter.
cover property (@(posedge clk) disable iff (!rst_n)
                  burst_end && !burst_is_write && next_pending && next_is_write);

cnt_turnarounds and cnt_turnaround_cycles are separate on purpose. One turnaround costing six cycles and six turnarounds costing one each both report six cycles, and they are different problems: the first says the interval is expensive, the second says the scheduler is thrashing directions. A single counter cannot distinguish them, and §16's debugging table needs both.

8. Attribution Is Not Blame

Before the RTL, the discipline that keeps an attribution honest. The module's three-way distinction applies here more sharply than anywhere else:

Irreducible costA cycle no decision could have used — tCCD_L's gap, refresh
Avoidable costA cycle lost to a policy that could have differed
MeasurementWhat a counter observed

A counter cannot tell the first two apart, and this is the central limitation of everything below.

A cycle charged to tFAW was genuinely unavailable — but whether the workload should have been activating four rows in that window at all is a mapping and policy question the counter cannot see. A cycle charged to turnaround was genuinely a direction change, and whether the scheduler should have batched more reads before switching is exactly 23.4's subject.

So the accounting answers “where did the cycles go” and never “whose fault was it”. §15's closing callout restates this as the limit on what the properties prove, and §18's debugging table is built so that each attribution routes to the chapter that owns the corresponding decision.

9. Exhaustive and Disjoint Is the Hard Part

The categories must partition the window: every cycle in exactly one, no cycle in two, none left over. Both halves are harder than they look.

Disjointness fails when two causes are simultaneously true. A cycle can be inside a refresh and inside a tCCD_L gap and inside a bus turnaround. All three descriptions are accurate. Charging the cycle to all three inflates the total beyond the window; charging it to none loses it.

The resolution is a strict priority order, declared once and applied always:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. DATA           a beat is on the bus. Nothing else matters --
                     the cycle was USED.
   2. REFRESH        the device is unavailable. Dominates every
                     controller-side cause, because no decision
                     could have used this cycle.
   3. TURNAROUND     the bus is changing direction.
   4. ROW WORK       a PRE or ACT is occupying the path to data.
   5. tFAW STALL     an activate was wanted and the window forbade it.
   6. tCCD GAP       a column command was ready and spacing forbade it.
   7. IDLE           no demand. The requester had nothing.

The order is an engineering statement, not a convenience. Refresh above every controller-side cause encodes §8's claim that a refresh cycle is irreducible. IDLE last encodes that a cycle with no demand is only idle if nothing else explains it — which prevents the most flattering possible error, charging real stalls to “the requester was not asking.”

Exhaustiveness fails at the edges. A window that ends mid-burst, a refresh spanning the boundary, a category that is only checked when another is false — each leaves cycles unclaimed. The RTL handles it by construction: there is a default branch, IDLE catches everything, and §15's P1 asserts the sum.

10. The Attribution Architecture

The cycle attribution architecture, in four rows. The top row holds the incoming data bus cycle, a category decoder that applies the strict priority order, an exactly-one check confirming that precisely one category was selected, and a sum check confirming the category counters add up to the window length. The second and third rows hold the seven mutually exclusive categories a cycle can be charged to: data transferred, refresh, bus turnaround, four-activate-window stall, row conflict work, column-to-column spacing gap, and idle with no demand. The bottom row holds the bandwidth calculator, which consumes the data count and the validated sum to produce achieved bytes and a utilisation ratio. Edges run from the bus cycle into the decoder, from the decoder into each of the seven categories and into the exactly-one check, from that check into the sum check, and from the data category and the sum check into the calculator. The structure shows that the decoder is the only place a cycle is classified, which is what makes the categories disjoint by construction.Bus cycleone per clockCategory decoderstrict priority — §9Exactly-one checkdisjointnessSum checkexhaustivenessDATAthe cycle was usedREFRESHirreducible — §6TURNAROUNDdirection changetFAW STALLwindow forbade an ACTROW WORKPRE and ACT — 9.xtCCD GAPspacing forbade a CASIDLEno demand — lastCalculatorbytes and ratio12

The structural point is that the decoder is the only place a cycle is classified. Seven counters, one decision. An implementation with seven independent if statements — each counter deciding for itself whether this cycle belongs to it — is the natural design and it is the one that produces a total exceeding the window, because on a cycle where three conditions hold, three counters increment.

11. The Cycle Attributor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// cycle_attributor -- charges every cycle in an observation window to
// exactly ONE named cause, exhaustively.
//
// CLASSIFICATION: educational, synthesisable. Builds measure D of
// Chapter 12.4, which that chapter defined and did not construct.
//
// WHAT IT DOES NOT MODEL:
//   - the device, the workload, or the scheduler
//   - whether a charged cycle was AVOIDABLE. §8: attribution is not
//     blame, and no counter can separate irreducible from avoidable.
//   - absolute time. Cycles only; the ns conversion needs tCK.
//
// THE TWO INVARIANTS:
//   DISJOINT    -- exactly one category charged per cycle (P2)
//   EXHAUSTIVE  -- the categories sum to the window length (P1)
// Both are asserted, and P1 is the single most valuable property in
// this module: an attribution whose parts do not sum is not one.
// ---------------------------------------------------------------------
module cycle_attributor #(
  // Observation window in cycles. A window is REQUIRED: a utilisation
  // without a stated denominator is not a quantity (12.4 §2).
  parameter int WINDOW  = 1024,
  parameter int CNT_W   = 24,
  parameter int WIN_W   = (WINDOW <= 1) ? 1 : $clog2(WINDOW + 1),
  // Seven categories, fixed by §9's priority order. Not configurable:
  // changing the set changes what the block claims to measure.
  parameter int NCAT    = 7
) (
  input  logic              clk,
  input  logic              rst_n,

  input  logic              enable,

  // ── Condition inputs, one per cause. These may be simultaneously
  //    true -- that is the whole difficulty (§9), and resolving it is
  //    this block's job rather than its caller's.
  input  logic              c_data_beat,     // a beat is on the bus
  input  logic              c_refresh,       // device unavailable
  input  logic              c_turnaround,    // bus changing direction
  input  logic              c_row_work,      // PRE or ACT occupying
  input  logic              c_faw_block,     // an ACT wanted, tFAW said no
  input  logic              c_ccd_block,     // a CAS ready, tCCD said no
  input  logic              c_demand,        // the requester wants something

  // ── Per-category counts, valid when window_done.
  output logic [CNT_W-1:0]  n_data,
  output logic [CNT_W-1:0]  n_refresh,
  output logic [CNT_W-1:0]  n_turnaround,
  output logic [CNT_W-1:0]  n_row_work,
  output logic [CNT_W-1:0]  n_faw,
  output logic [CNT_W-1:0]  n_ccd,
  output logic [CNT_W-1:0]  n_idle,

  // ── Observability.
  output logic [2:0]        charged,         // which category, this cycle
  output logic [WIN_W-1:0]  cycles_elapsed,
  output logic              window_done,
  // The invariants, published so a checker requires rather than
  // recomputes them.
  output logic              sum_ok,
  output logic              exactly_one,
  output logic [2:0]        dominant_loss,   // largest NON-data category

  output logic              err_saturated,
  output logic              err_overlap_seen  // two causes coincided
);

  initial begin
    if (WINDOW < 8)
      $fatal(1, "cycle_attributor: WINDOW (%0d) too small to be meaningful", WINDOW);
    if (NCAT != 7)
      $fatal(1, "cycle_attributor: NCAT is fixed at 7 by the priority order");
    // Every cycle of the window could fall in one category, so a
    // counter must be able to hold WINDOW. A narrower counter would
    // saturate on a perfectly ordinary trace.
    if ((CNT_W < WIN_W))
      $fatal(1, "cycle_attributor: CNT_W (%0d) cannot hold WINDOW", CNT_W);
  end

  localparam logic [2:0] K_DATA = 3'd0;
  localparam logic [2:0] K_REF  = 3'd1;
  localparam logic [2:0] K_TURN = 3'd2;
  localparam logic [2:0] K_ROW  = 3'd3;
  localparam logic [2:0] K_FAW  = 3'd4;
  localparam logic [2:0] K_CCD  = 3'd5;
  localparam logic [2:0] K_IDLE = 3'd6;

  // ── THE DECODER. §9's priority order, in one place, as a priority
  //    chain. This is the only classification in the block, which is
  //    what makes the categories disjoint BY CONSTRUCTION rather than
  //    by a check applied afterwards.
  logic [2:0] sel;
  always_comb begin
    if      (c_data_beat)  sel = K_DATA;   // used -- nothing else matters
    else if (c_refresh)    sel = K_REF;    // irreducible, dominates policy
    else if (c_turnaround) sel = K_TURN;
    else if (c_row_work)   sel = K_ROW;
    else if (c_faw_block)  sel = K_FAW;
    else if (c_ccd_block)  sel = K_CCD;
    else                   sel = K_IDLE;   // catch-all: exhaustive
  end

  // ── How many causes were simultaneously true. Not used for
  //    classification -- published so the undercount §9's callout
  //    admits to is observable rather than hidden.
  logic [3:0] n_true;
  always_comb
    n_true = 4'($countones({c_data_beat, c_refresh, c_turnaround,
                            c_row_work, c_faw_block, c_ccd_block}));

  logic [CNT_W-1:0] cnt [7];
  logic [WIN_W-1:0] elapsed;
  logic             sat, wdone, ovl;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int k = 0; k < 7; k++) cnt[k] <= '0;
      elapsed <= '0;
      sat     <= 1'b0;
      wdone   <= 1'b0;
      ovl     <= 1'b0;
    end else begin
      wdone <= 1'b0;

      if (enable) begin
        if (elapsed == WIN_W'(WINDOW)) begin
          // Window complete: report and restart. Counters clear here
          // rather than on the first cycle of the next window, so a
          // consumer reading on window_done sees a complete set.
          wdone   <= 1'b1;
          elapsed <= WIN_W'(1);
          for (int k = 0; k < 7; k++) cnt[k] <= '0;
          cnt[sel] <= CNT_W'(1);
          ovl <= (n_true > 4'd1);
        end else begin
          elapsed <= elapsed + WIN_W'(1);
          if (cnt[sel] == {CNT_W{1'b1}}) sat <= 1'b1;
          else                           cnt[sel] <= cnt[sel] + CNT_W'(1);
          if (n_true > 4'd1) ovl <= 1'b1;
        end
      end
    end
  end

  assign n_data       = cnt[K_DATA];
  assign n_refresh    = cnt[K_REF];
  assign n_turnaround = cnt[K_TURN];
  assign n_row_work   = cnt[K_ROW];
  assign n_faw        = cnt[K_FAW];
  assign n_ccd        = cnt[K_CCD];
  assign n_idle       = cnt[K_IDLE];

  assign charged        = sel;
  assign cycles_elapsed = elapsed;
  assign window_done    = wdone;

  // ── EXHAUSTIVENESS. Summed in a wider accumulator so the comparison
  //    cannot itself overflow and report a false pass.
  logic [CNT_W+3:0] tally;
  always_comb begin
    tally = '0;
    for (int k = 0; k < 7; k++) tally = tally + {4'b0, cnt[k]};
  end
  assign sum_ok = (tally == {{(CNT_W+4-WIN_W){1'b0}}, elapsed});

  // ── DISJOINTNESS. The decoder guarantees it structurally; this makes
  //    the guarantee checkable.
  assign exactly_one = 1'b1;   // one `sel` per cycle, by construction

  // ── Largest non-data category: the binding constraint, and the only
  //    question anyone asks of an attribution.
  always_comb begin
    dominant_loss = K_REF;
    for (int k = 2; k < 7; k++)
      if (cnt[k] > cnt[dominant_loss]) dominant_loss = 3'(k);
  end

  assign err_saturated    = sat;
  assign err_overlap_seen = ovl;

endmodule

Three points, and the middle one is the chapter.

c_demand is an input and is not used in the decoder. That is deliberate and worth noticing: IDLE is the catch-all, reached when no other cause holds, rather than a category gated on demand being absent. Gating it on !c_demand would leave a hole — a cycle with demand present and no other cause true would be charged to nothing, breaking exhaustiveness. The port exists so a consumer can distinguish idle with demand (a real stall the categories failed to explain) from idle without demand, which is the more useful report.

err_overlap_seen publishes the undercount rather than hiding it. §9's callout admits that a lower-priority cause is undercounted whenever a higher one coincides. n_true counts how many causes were simultaneously true, and the sticky flag says it happened. An attribution with err_overlap_seen low is a partition of genuinely independent causes; one with it high is a priority-ordered projection, and those support different conclusions.

Counters clear on window_done, and that cycle's category is charged to the new window. Clearing on the next cycle would either lose a cycle or double-count one at every window boundary — and over many windows a one-cycle-per-window error is a systematic bias, not noise.

12. A 200-Cycle Window, Attributed

Model B from §3, run for 200 cycles. Every figure DERIVED and recomputed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  Model B: row hits, same bank group. tCCD_L = 5, burst = 4 cycles.
  Window = 200 cycles.

  bursts issued        = 200 / 5          = 40
  DATA cycles          = 40 x 4           = 160
  tCCD GAP cycles      = 40 x (5 - 4)     =  40
  REFRESH              =                     0   (none in this window)
  TURNAROUND           =                     0   (reads only)
  ROW WORK             =                     0   (all hits)
  tFAW STALL           =                     0   (no activates)
  IDLE                 =                     0   (demand always present)
                                            ----
  SUM                                        200

  SUM CHECK : 160 + 40 + 0 + 0 + 0 + 0 + 0 = 200 == window  ->  HOLDS

  slot utilisation  = 160 / 200 = 80.0%
  achieved          = 40 bursts x 64 B / 200 cycles = 12.800 B/cycle
                    = 10.240 GB/s of 12.8 GB/s peak = 80.0%
  dominant_loss     = tCCD GAP (40 cycles)

Two things this trace demonstrates that the §3 table could not.

The attribution and the utilisation agree by construction. n_data / window is 160 / 200 = 80%, which is exactly §3's model B figure computed a completely different way — from a per-cycle tally rather than from burst_cycles / gap_cycles. Two routes, one answer.

dominant_loss names tCCD GAP, and §8 says what to do with that. It identifies the binding constraint, and it does not say the scheduler failed. tCCD_L is a device rule; those 40 cycles were irreducible while the workload stayed in one bank group. The actionable reading is that moving to alternating bank groups would eliminate them entirely — which is model A, at 100%.

13. The Bandwidth Calculator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// bandwidth_reporter -- turns an attribution into byte counts and
// explicit ratio numerator/denominator pairs.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
//   - causes, avoidability, or time. Cycles and bytes only.
//   - a percentage. IT DOES NOT DIVIDE (12.4 §2): every ratio is
//     published as a numerator and a denominator so the consumer is
//     forced to state what it divided by.
//
// WHY NO DIVISION: 12.4 established that "efficiency" as a single
// number is not a quantity. A block that produced one would be
// endorsing exactly the trap that chapter named.
// ---------------------------------------------------------------------
module bandwidth_reporter #(
  // Bytes carried by one fully-used cycle. DERIVED from the interface:
  // beats per cycle times bytes per beat (§2 -- 2 x 8 = 16).
  parameter int BYTES_PER_CYCLE = 16,
  parameter int CNT_W  = 24,
  parameter int BYT_W  = CNT_W + 5,
  parameter int BPC_W  = (BYTES_PER_CYCLE <= 1) ? 1 : $clog2(BYTES_PER_CYCLE + 1)
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── From cycle_attributor, on window_done.
  input  logic              window_done,
  input  logic [CNT_W-1:0]  n_data,
  input  logic [CNT_W-1:0]  n_refresh,
  input  logic [CNT_W-1:0]  n_window,      // total cycles observed
  input  logic              attrib_sum_ok,

  // ── Byte counts.
  output logic [BYT_W-1:0]  bytes_moved,
  output logic [BYT_W-1:0]  bytes_peak,        // if every cycle carried
  // Bytes the device could have carried given refresh alone -- the
  // TRUE ceiling of §6, which is below peak and is the honest
  // denominator for a controller-efficiency claim.
  output logic [BYT_W-1:0]  bytes_ceiling,

  // ── Ratio PAIRS. Never a quotient.
  output logic [CNT_W-1:0]  util_num,          // n_data
  output logic [CNT_W-1:0]  util_den,          // n_window
  output logic [CNT_W-1:0]  ctrl_num,          // n_data
  output logic [CNT_W-1:0]  ctrl_den,          // n_window - n_refresh
  output logic              ratios_valid,

  output logic              err_data_exceeds_window,
  output logic              err_reported_without_sum
);

  initial begin
    if (BYTES_PER_CYCLE < 1)
      $fatal(1, "bandwidth_reporter: BYTES_PER_CYCLE must be >= 1");
    if (BYT_W <= CNT_W)
      $fatal(1, "bandwidth_reporter: BYT_W must exceed CNT_W to hold a product");
  end

  logic [BYT_W-1:0] mv, pk, cl;
  logic [CNT_W-1:0] un, ud, cn, cd;
  logic             vld, bad, nosum;

  // Available cycles after refresh: the denominator a CONTROLLER should
  // be judged against, because refresh is not its decision (§6, §8).
  logic [CNT_W-1:0] avail;
  always_comb begin
    avail = (n_window >= n_refresh) ? (n_window - n_refresh) : '0;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      mv <= '0; pk <= '0; cl <= '0;
      un <= '0; ud <= '0; cn <= '0; cd <= '0;
      vld <= 1'b0; bad <= 1'b0; nosum <= 1'b0;
    end else if (window_done) begin
      // Products are computed into BYT_W, which the elaboration guard
      // requires to be wider than CNT_W -- a same-width multiply would
      // silently truncate on a large window.
      mv <= {{(BYT_W-CNT_W){1'b0}}, n_data}  * BYT_W'(BYTES_PER_CYCLE);
      pk <= {{(BYT_W-CNT_W){1'b0}}, n_window} * BYT_W'(BYTES_PER_CYCLE);
      cl <= {{(BYT_W-CNT_W){1'b0}}, avail}   * BYT_W'(BYTES_PER_CYCLE);

      un <= n_data;   ud <= n_window;
      cn <= n_data;   cd <= avail;

      // A report is only trustworthy if the attribution it came from
      // was exhaustive. Reporting from a broken attribution is worse
      // than not reporting.
      vld   <= attrib_sum_ok && (n_window != '0);
      bad   <= (n_data > n_window);
      nosum <= !attrib_sum_ok;
    end
  end

  assign bytes_moved   = mv;
  assign bytes_peak    = pk;
  assign bytes_ceiling = cl;
  assign util_num = un; assign util_den = ud;
  assign ctrl_num = cn; assign ctrl_den = cd;
  assign ratios_valid = vld;

  assign err_data_exceeds_window   = bad;
  assign err_reported_without_sum  = nosum;

endmodule

The three byte outputs are the chapter's practical contribution, and the middle one is the one usually missing.

bytes_peak divides by the whole window — 12.4's measure B denominator, and the number marketing uses.

bytes_ceiling divides by the window minus refresh — the honest denominator for judging a controller, because §6 established that refresh is the one cost no controller decision affects. A controller achieving 80% of peak is achieving 80 / 95.51 = 83.8% of what was actually available to it. DERIVED, and it is a materially different claim.

And nothing divides. Four numerator/denominator pairs go out; the consumer states which it used. That is 12.4 §2's discipline enforced at the interface rather than recommended in prose.

14. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind unit note: P1-P7 reference cycle_attributor's internals and are
// written as though bound into it. P8-P10 reference bandwidth_reporter
// likewise. Both see clk and rst_n directly.

// P1 -- THE property of this chapter, and of this module. The
// categories sum to the cycles elapsed. If it holds, the attribution
// is a partition; if not, it is seven counters.
property p_categories_sum_to_window;
  @(posedge clk) disable iff (!rst_n)
    enable |-> sum_ok;
endproperty
assert property (p_categories_sum_to_window);

// P2 -- exactly one category is charged per enabled cycle. §9's
// disjointness, and the guard against the seven-independent-ifs design.
property p_one_category_per_cycle;
  @(posedge clk) disable iff (!rst_n)
    (enable && !err_saturated && !window_done)
      |=> ($countones({n_data       != $past(n_data, 1),
                       n_refresh    != $past(n_refresh, 1),
                       n_turnaround != $past(n_turnaround, 1),
                       n_row_work   != $past(n_row_work, 1),
                       n_faw        != $past(n_faw, 1),
                       n_ccd        != $past(n_ccd, 1),
                       n_idle       != $past(n_idle, 1)}) == 1);
endproperty
assert property (p_one_category_per_cycle);

// P3 -- a data beat is ALWAYS charged to DATA. §9's priority order at
// its top: a used cycle is never charged to a loss category, which
// would understate achieved bandwidth.
property p_data_beat_charged_to_data;
  @(posedge clk) disable iff (!rst_n)
    (enable && c_data_beat) |-> (charged == 3'd0);
endproperty
assert property (p_data_beat_charged_to_data);

// P4 -- refresh outranks every controller-side cause. §8's claim that
// an irreducible cycle is never charged to a policy category.
property p_refresh_outranks_policy;
  @(posedge clk) disable iff (!rst_n)
    (enable && !c_data_beat && c_refresh) |-> (charged == 3'd1);
endproperty
assert property (p_refresh_outranks_policy);

// P5 -- IDLE is only charged when nothing else holds. The flattering
// error -- charging real stalls to "no demand" -- forbidden.
property p_idle_is_last_resort;
  @(posedge clk) disable iff (!rst_n)
    (enable && (charged == 3'd6))
      |-> !(c_data_beat || c_refresh || c_turnaround
            || c_row_work || c_faw_block || c_ccd_block);
endproperty
assert property (p_idle_is_last_resort);

// P6 -- elapsed never exceeds the window.
property p_window_bounded;
  @(posedge clk) disable iff (!rst_n)
    (cycles_elapsed <= WIN_W'(WINDOW));
endproperty
assert property (p_window_bounded);

// P7 -- no category can exceed the cycles elapsed.
property p_no_category_exceeds_elapsed;
  @(posedge clk) disable iff (!rst_n)
    enable |-> ((n_data <= {{(CNT_W-WIN_W){1'b0}}, cycles_elapsed})
             && (n_idle <= {{(CNT_W-WIN_W){1'b0}}, cycles_elapsed}));
endproperty
assert property (p_no_category_exceeds_elapsed);

// P8 -- a report is never issued from a non-exhaustive attribution.
property p_no_report_without_sum;
  @(posedge clk) disable iff (!rst_n)
    ratios_valid |-> !err_reported_without_sum;
endproperty
assert property (p_no_report_without_sum);

// P9 -- the controller denominator never exceeds the peak denominator,
// because refresh cycles are subtracted rather than added.
property p_ceiling_below_peak;
  @(posedge clk) disable iff (!rst_n)
    ratios_valid |-> (bytes_ceiling <= bytes_peak);
endproperty
assert property (p_ceiling_below_peak);

// P10 -- bytes moved never exceeds the ceiling. A controller cannot
// move data during a refresh, so exceeding it means refresh cycles
// were also counted as data.
property p_moved_within_ceiling;
  @(posedge clk) disable iff (!rst_n)
    ratios_valid |-> (bytes_moved <= bytes_ceiling);
endproperty
assert property (p_moved_within_ceiling);

// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) window_done);
// 100% utilisation -- §4's model A, which a suite that never hits it
// has not tested against the actual ceiling.
cover property (@(posedge clk) disable iff (!rst_n)
                  window_done && (n_data == n_window));
// Zero data in a whole window: the pathological case.
cover property (@(posedge clk) disable iff (!rst_n)
                  window_done && (n_data == '0));
// Each loss category was dominant at least once.
cover property (@(posedge clk) disable iff (!rst_n)
                  window_done && (dominant_loss == 3'd5));   // tCCD
cover property (@(posedge clk) disable iff (!rst_n)
                  window_done && (dominant_loss == 3'd3));   // row work
cover property (@(posedge clk) disable iff (!rst_n)
                  window_done && (dominant_loss == 3'd1));   // refresh
// Two causes coincided -- the projection case of §9's callout.
cover property (@(posedge clk) disable iff (!rst_n) err_overlap_seen);
// A window with demand present and everything idle: the hole that a
// demand-gated IDLE category would have created.
cover property (@(posedge clk) disable iff (!rst_n)
                  enable && c_demand && (charged == 3'd6));

P1 is the module's most valuable property and P5 is its most easily overlooked. Charging an unexplained stall to IDLE is the single most flattering error an attribution can make — it converts a controller problem into “the requester wasn't asking” — and P5 forbids it by requiring every other cause to be false.

The last cover is the one that proves the §11 design decision was necessary: a cycle with demand present, charged to IDLE, is exactly the cycle a demand-gated IDLE category would have left unclaimed.

15. Corner Cases

CaseBehaviourWhy
Every cycle a data beatn_data == n_window, utilisation 100%§4's model A; the second cover requires it
No demand for a whole windowEverything in IDLE, valid partitionNot an error; the requester was quiet
Demand present, nothing else trueCharged to IDLE; the last cover firesThe hole a demand-gated IDLE would leave
Data beat during a refreshCharged to DATA (P3)Should be impossible; if seen, refresh detection is wrong
Three causes simultaneously trueHighest priority charged; err_overlap_seen§9's projection, made visible
Window boundary mid-burstCycles split across two windowsCorrect; a burst is not atomic to a window
CNT_W narrower than WIN_W$fatal at elaborationOne category could hold the whole window
A counter clampingerr_saturated; sum may breakSaturation and exhaustiveness cannot both hold
n_refresh exceeding n_windowavail floors at zeroNonsensical input, safe output
n_data exceeding n_windowerr_data_exceeds_windowImpossible from a valid attribution
Report with attrib_sum_ok lowratios_valid low, err_reported_without_sumA ratio from a broken partition is worse than none
WINDOW of 8Legal; statistically uselessSmall windows have huge variance; the denominator is still stated

Row eight is the interesting tension. Saturation and exhaustiveness are incompatible: once a counter clamps, the categories no longer sum. The block reports both rather than pretending one, and P1 is written under enable so a saturated run fails it loudly rather than quietly reporting a partition that is not one.

16. DV — An Independent Tally

The checker must not re-implement the priority chain. What it should do is keep its own tally by a different method and compare — and separately measure the undercount §9 admits to.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Independent tally. Records the condition vector every cycle and
// classifies afterwards, from the recorded history, rather than
// live -- a different method from the DUT's priority chain.
// SIMULATION-ONLY.
class attrib_tally;
  int unsigned tot[7];
  int unsigned cycles;
  // How many cycles had N causes simultaneously true, for N = 0..6.
  int unsigned coincidence[7];

  function void observe(bit d, bit rf, bit tn, bit rw, bit fw, bit cd);
    int unsigned n; int unsigned k;
    n = d + rf + tn + rw + fw + cd;
    coincidence[n]++;
    cycles++;
    // Same ORDER as the DUT, but applied to a recorded vector rather
    // than to live signals -- so a live-signal timing bug shows up as
    // a disagreement instead of being reproduced.
    if      (d)  k = 0;
    else if (rf) k = 1;
    else if (tn) k = 2;
    else if (rw) k = 3;
    else if (fw) k = 4;
    else if (cd) k = 5;
    else         k = 6;
    tot[k]++;
  endfunction

  function bit partitions();
    int unsigned s; s = 0;
    for (int i = 0; i < 7; i++) s += tot[i];
    return s == cycles;
  endfunction

  // The undercount: cycles where a lower-priority cause was true but
  // not charged. This is what §9's callout admits to, quantified.
  function int unsigned masked_cycles();
    int unsigned m; m = 0;
    for (int n = 2; n < 7; n++) m += coincidence[n] * (n - 1);
    return m;
  endfunction
endclass
CheckWhat it establishes
Every category matches the DUT's counter at window_doneTwo methods, one answer
partitions() holds on the reference itselfThe reference is exhaustive before being used as one
Replay §12's model B: expect 160 / 40 and zeros elsewhereThe published trace reproduced from the RTL
Replay §3's model A: expect n_data == n_window100% is reachable and measured
Replay §3's model C: expect n_data / n_window near 10.3%The disaster case, measured not asserted
Drive a data beat with refresh asserted; expect DATA and flag itP3, and a refresh-detection fault made visible
Drive all six causes true every cycle; expect all to DATAThe priority chain's top
Drive only c_demand; expect all to IDLEThe catch-all
Report masked_cycles() alongside every resultThe undercount quantified rather than ignored
Force a counter to clamp; expect sum_ok to fail, loudlySaturation and exhaustiveness are incompatible

The ninth check produces the result worth publishing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  THE PRIORITY ORDER'S UNDERCOUNT, QUANTIFIED
    Model B, 200 cycles, with a refresh inserted over cycles 100-119
    (20 cycles, ILLUSTRATIVE -- a real tRFC is 280 cycles).

    reference tally                     DUT
      DATA        144                     144
      REFRESH      20                      20
      tCCD GAP     36                      36
      others        0                       0
      SUM         200                     200      partition HOLDS

    coincidence histogram from the reference:
      1 cause  true : 180 cycles
      2 causes true :  20 cycles   <- the refresh window
      masked_cycles() = 20 x (2-1) = 20

    reading it : during the 20 refresh cycles, tCCD_L was ALSO
      forbidding a column command. Those cycles are charged to
      REFRESH, correctly by §9's order -- and tCCD GAP is therefore
      UNDERCOUNTED by 20 relative to "cycles on which tCCD_L was
      binding", which was 56, not 36.

    diagnosis : NOT A BUG. It is the projection §9's callout describes,
      and the two readings answer different questions:
        "what bounded this cycle"      -> tCCD GAP = 36  (the DUT)
        "when was tCCD_L binding"      -> 56 cycles      (the tally)
    why it matters : a report of 36 tCCD cycles understates how often
      that constraint was active. A design deciding whether to change
      bank-group interleaving wants the second number; a design
      deciding where the cycles WENT wants the first.
    what would be wrong : charging those 20 cycles to both, which
      sums to 220 over a 200-cycle window and fails P1.

That is the most useful output of this chapter's verification, and it is not a defect. A priority-ordered attribution and a per-constraint activity count are different measurements, both legitimate, and only one of them partitions. Reporting the first as if it were the second is the error — and quantifying the gap is cheap.

17. Debugging

SymptomLikely causeHow to confirm
Categories do not sumSeven independent increments, not a decoder — §10P1; this is the defining defect
Utilisation above 100%A cycle charged to two categoriesP2 and err_data_exceeds_window
Nearly everything in IDLEReal: no demand. Or: causes not wiredThe last cover — is c_demand high while IDLE charges?
Utilisation 80% and flattCCD_L-bound — §3's model Bdominant_loss == tCCD; try alternating bank groups
Utilisation near 10%tRC-bound on one bank — §3's model Cdominant_loss == row work; 23.3 and 18.2
Refresh larger than 4.49%Fine-granularity mode, or high temperature15.5 owns both
Efficiency looks worse than expected by ~1 ppCosts added instead of multiplied — §5Recompute as a product
Controller blamed for refresh cyclesWrong denominator — §13Use ctrl_den, which subtracts refresh
err_overlap_seen highCauses genuinely coincide — §16Not a bug; the report is a projection, not a partition of causes
err_saturated and a broken sumCounters clampedSaturation and exhaustiveness are incompatible — §15
Bandwidth improved, latency regressedExpected — §1Reordering trades one for the other; 23.4 quantifies both
tFAW count high with few missesActivates clustered in time, not numeroustFAW bounds a rate; spreading them helps

Row eight is the one that changes conclusions. A controller judged against peak is being charged for refresh, which is not its decision. At this configuration the honest ceiling is 95.51% of peak, so a controller at 80% of peak is at 83.8% of what was available — and those two numbers support different verdicts on the same design.

18. Misconceptions

“DDR cannot reach peak bandwidth.” §4. Model A reaches exactly 100%: tCCD_S equals the burst length in cycles, so consecutive column commands to different bank groups leave no gap.

“The gap between peak and achieved is waste.” §8. Some of it is device rules no decision could avoid — tCCD_L's one-in-five gap, refresh's 4.49%. Some is policy. A counter cannot tell them apart.

“Efficiency is one number.” 12.4 §2, consumed here. Four measures, four denominators, and §13's block refuses to divide for exactly that reason.

“Losses add up.” §5. Independent fractional costs multiply. Adding them overstates the loss and, with enough costs, produces a negative achieved bandwidth.

“Refresh costs about 5% and that is the main overhead.” §6 and §3. Refresh is 4.49% and is the smallest of the costs in model C, where row work removes nearly 90%.

“A high tFAW count means too many row activations.” §17's last row. tFAW bounds activates per window; a moderate number clustered in time hits it while the same number spread out does not.

“Models B and D both give 80%, so bank spreading does not help.” §3. That equality is an artefact of tFAW / 4 = tCCD_L = 5 at this specific bin, and it does not hold generally.

“Charging an unexplained stall to idle is harmless.” §14's P5. It converts a controller problem into “the requester wasn't asking” — the most flattering error available.

“The attribution tells you what to fix.” §8. It tells you where the cycles went. Whether a cost was avoidable is a question about decisions, which is 23.4's and 23.5's.

“A priority-ordered attribution counts how often each constraint was binding.” §16. It counts which constraint bounded each cycle. Where causes coincide, lower-priority ones are undercounted — the report quantifies the gap.

“80% of peak is 80% of what the controller could have done.” §13 and §17's row eight. It is 83.8%, because refresh removed 4.49% before the controller made any decision.

“Bandwidth and latency improve together.” §1. Reordering for throughput makes some request wait longer, by construction.

19. Interview Reasoning

Derive peak bandwidth for DDR4-1600 on a 64-bit channel. Two beats per cycle at 8 bytes each is 16 bytes per cycle; at 1.25 ns that is 12.8 GB/s. Or directly: 1600 MT/s × 8 bytes. Both routes agree, which is the check worth doing.

Can a DDR interface actually reach peak? Yes. tCCD_S is 4 cycles and a BL8 burst occupies 4 cycles, so row hits alternating between bank groups produce data on every slot — exactly 100%, not approximately.

Why does staying in one bank group cost 20%? tCCD_L is 5 cycles against a 4-cycle burst, so one cycle in five is a gap the protocol requires. No scheduler recovers it without leaving the bank group.

What does the all-misses-one-bank case give? About 10% of peak. tRC is 39 cycles between activates on the same bank and the burst is 4, so 35 cycles of every 39 carry nothing. That row is the entire argument for bank-level parallelism.

Two costs, 20% and 4.49%. What is the combined loss? 23.59%, not 24.49%. They apply to different denominators, so they multiply: 0.80 × 0.9551 = 0.7641 achieved.

Why must a cycle attribution use a priority order? Because several causes can be true at once, and charging a cycle to all of them makes the total exceed the window. The order decides which cause is recorded as binding.

What does the order encode? Data first, because a used cycle is used. Refresh above every controller-side cause, because a refresh cycle is irreducible. Idle last, so a stall is never mislabelled as absent demand.

What does a priority-ordered attribution undercount? Every lower-priority cause, whenever a higher one coincides. So the tCCD count answers “which cycles were bounded by tCCD”, not “how often was tCCD binding” — and the second number is larger.

Your controller hits 80% of peak. Is that good? Unanswerable without a denominator. Against peak it is 80%. Against what was available after refresh it is 83.8%. Against model A's demonstrated 100% it means the workload is not in model A's shape, and the attribution says which constraint is binding instead.

Attribution says tCCD GAP dominates. What do you change? Not the scheduler — tCCD_L is a device rule. The lever is bank-group interleaving in the address map, which moves the workload from model B toward model A. That is a mapping decision, and the attribution has told you which one.

20. Exercises

  1. Recompute all four §3 models for DDR4-1866M — tCK = 1.071 ns, tCCD_L = 5, tRC = 34 + 13.92 ns. Which model's utilisation changes and which does not? Explain why utilisation and GB/s do not move together.

  2. §3 notes that models B and D coincide at this bin because tFAW / 4 = tCCD_L. Find a DDR4 bin where they differ, state which becomes the binding constraint, and give the general condition for B to bound D.

  3. Compose three independent costs of 20%, 4.49% and 10% both additively and multiplicatively. Report the difference in percentage points, then find the number of 10% costs at which the additive method produces a negative achieved bandwidth.

  4. Change §11's priority order to put tCCD GAP above REFRESH. Which property fires? Then argue what engineering claim the reordered version would be making, and why it is wrong.

  5. Gate IDLE on !c_demand and construct the condition vector that is then charged to nothing. Which property catches it, and why does the sum property alone not?

  6. Using §16's masked_cycles(), derive the maximum possible undercount for a window of W cycles with C causes. Then state the condition under which a priority-ordered attribution and a per-constraint activity count give identical answers.

  7. §13 publishes ctrl_den as window minus refresh. Argue whether tCCD GAP cycles should also be subtracted before judging a controller, and what 23.4 would have to establish for the answer to be yes.

  8. Take §12's 200-cycle window and insert one read-to-write turnaround. Using 23.1 §2's verified tWTR_L = 6, recompute the full attribution, confirm the sum, and state which category absorbs the change and which is undercounted as a result.

21. Where This Goes

Bandwidth now has a peak derived from verified parameters, four simulated workload models spanning 10.3% to 100% of that peak, a correct composition rule, and an accounting in which every cycle is charged to exactly one named cause with the partition asserted rather than assumed.

Three results carry forward. Peak is attainable, so the interesting question is which workload properties move a system away from it. The costs multiply rather than add, which matters structurally more than the 0.9 percentage points it changed here. And the largest single cost in the worst model is row work — model C loses nearly 90% of peak to tRC, while refresh, the only unconditional cost, removes 4.49%.

That last asymmetry sets up the next chapter. §17's debugging table routes a row-work-dominant attribution to locality, and §19's last answer says the lever is the address map rather than the scheduler. Both are claims this chapter can make and cannot support: it can say row work bounded the cycles, and it cannot say what produced the misses.

Chapter 23.3 supplies that. 9.6 already counts hits, misses and conflicts and already argues that a hit rate alone is insufficient; what nothing yet owns is what a workload's access structure does to those counts — how a stride destroys locality that a sequential walk preserves, how to measure reuse without storing every address, and why the same request sequence under two address mappings produces different class counts and therefore different bandwidth.

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.