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.
| Decision | Effect on bandwidth | Effect on the delayed request's latency |
|---|---|---|
| Reorder to group row hits | Up — fewer row transitions | Up — the passed-over request waits |
| Reorder to group same-direction accesses | Up — fewer turnarounds | Up — same reason |
| Serve strictly in arrival order | Down | Down 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.
| Input | Value | Category |
|---|---|---|
| Data rate | 1600 MT/s | VERIFIED — the bin name |
| Channel data width | 64 bits = 8 bytes | VERIFIED — DDR4 DIMM, 64 data + 8 ECC |
| Transfers per clock | 2 | VERIFIED — double data rate |
tCK | 1.25 ns | VERIFIED |
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.
| Model | Access pattern | Limited by | Burst every | Utilisation | Achieved |
|---|---|---|---|---|---|
| A | Row hits, alternating bank groups | tCCD_S = 4 | 4 cycles | 100.0% | 12.800 GB/s |
| B | Row hits, same bank group | tCCD_L = 5 | 5 cycles | 80.0% | 10.240 GB/s |
| C | Row misses, one bank | tRC = 39 | 39 cycles | 10.3% | 1.313 GB/s |
| D | Row misses, spread over banks | tFAW = 20 | 5 cycles | 80.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 requires | If it fails | Moves toward |
|---|---|---|
| Every access a row hit | Misses and conflicts | C — row work dominates |
| Consecutive accesses in different bank groups | Same bank group | B — tCCD_L gaps |
| Accesses spread across banks | Concentration on one bank | C — tRC-bound |
| No direction changes | Read/write interleaving | Turnaround cycles |
| A requester always ready | Idle demand | Idle cycles |
| No refresh | Refresh 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.
| Method | Lost | Achieved | Rate |
|---|---|---|---|
| Additive (wrong) | 24.49% | 75.51% | 9.665 GB/s |
| Multiplicative (correct) | 23.59% | 76.41% | 9.781 GB/s |
| Difference | 0.90 pp | 0.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.
| Input | Value | Category |
|---|---|---|
tREFI at 1X, normal temperature | 7.8 µs | VERIFIED |
tRFC1 at 8Gb | 350 ns | VERIFIED |
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 shared — Module 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.
| Component | Cycles | Category |
|---|---|---|
| Read data | 4 | DERIVED — BL8 at 2 beats/cycle |
| Read-to-write bus gap | 2 | ILLUSTRATIVE — see the callout |
| Write data | 4 | DERIVED |
tWTR_L (write-to-read, same bank group) | 6 | VERIFIED |
| Total per read+write pair | 16 | of 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/s — exactly 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:
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 windowHalf 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.
// ---------------------------------------------------------------------
// 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);
endmoduleTwo assertions belong with it:
// 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 cost | A cycle no decision could have used — tCCD_L's gap, refresh |
| Avoidable cost | A cycle lost to a policy that could have differed |
| Measurement | What 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:
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 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
// ---------------------------------------------------------------------
// 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;
endmoduleThree 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.
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
// ---------------------------------------------------------------------
// 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;
endmoduleThe 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
// 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
| Case | Behaviour | Why |
|---|---|---|
| Every cycle a data beat | n_data == n_window, utilisation 100% | §4's model A; the second cover requires it |
| No demand for a whole window | Everything in IDLE, valid partition | Not an error; the requester was quiet |
| Demand present, nothing else true | Charged to IDLE; the last cover fires | The hole a demand-gated IDLE would leave |
| Data beat during a refresh | Charged to DATA (P3) | Should be impossible; if seen, refresh detection is wrong |
| Three causes simultaneously true | Highest priority charged; err_overlap_seen | §9's projection, made visible |
| Window boundary mid-burst | Cycles split across two windows | Correct; a burst is not atomic to a window |
CNT_W narrower than WIN_W | $fatal at elaboration | One category could hold the whole window |
| A counter clamping | err_saturated; sum may break | Saturation and exhaustiveness cannot both hold |
n_refresh exceeding n_window | avail floors at zero | Nonsensical input, safe output |
n_data exceeding n_window | err_data_exceeds_window | Impossible from a valid attribution |
Report with attrib_sum_ok low | ratios_valid low, err_reported_without_sum | A ratio from a broken partition is worse than none |
WINDOW of 8 | Legal; statistically useless | Small 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.
// 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| Check | What it establishes |
|---|---|
Every category matches the DUT's counter at window_done | Two methods, one answer |
partitions() holds on the reference itself | The reference is exhaustive before being used as one |
Replay §12's model B: expect 160 / 40 and zeros elsewhere | The published trace reproduced from the RTL |
Replay §3's model A: expect n_data == n_window | 100% 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 it | P3, and a refresh-detection fault made visible |
Drive all six causes true every cycle; expect all to DATA | The priority chain's top |
Drive only c_demand; expect all to IDLE | The catch-all |
Report masked_cycles() alongside every result | The undercount quantified rather than ignored |
Force a counter to clamp; expect sum_ok to fail, loudly | Saturation and exhaustiveness are incompatible |
The ninth check produces the result worth publishing:
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
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Categories do not sum | Seven independent increments, not a decoder — §10 | P1; this is the defining defect |
| Utilisation above 100% | A cycle charged to two categories | P2 and err_data_exceeds_window |
Nearly everything in IDLE | Real: no demand. Or: causes not wired | The last cover — is c_demand high while IDLE charges? |
| Utilisation 80% and flat | tCCD_L-bound — §3's model B | dominant_loss == tCCD; try alternating bank groups |
| Utilisation near 10% | tRC-bound on one bank — §3's model C | dominant_loss == row work; 23.3 and 18.2 |
| Refresh larger than 4.49% | Fine-granularity mode, or high temperature | 15.5 owns both |
| Efficiency looks worse than expected by ~1 pp | Costs added instead of multiplied — §5 | Recompute as a product |
| Controller blamed for refresh cycles | Wrong denominator — §13 | Use ctrl_den, which subtracts refresh |
err_overlap_seen high | Causes genuinely coincide — §16 | Not a bug; the report is a projection, not a partition of causes |
err_saturated and a broken sum | Counters clamped | Saturation and exhaustiveness are incompatible — §15 |
| Bandwidth improved, latency regressed | Expected — §1 | Reordering trades one for the other; 23.4 quantifies both |
tFAW count high with few misses | Activates clustered in time, not numerous | tFAW 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
-
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. -
§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. -
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.
-
Change §11's priority order to put
tCCD GAPaboveREFRESH. Which property fires? Then argue what engineering claim the reordered version would be making, and why it is wrong. -
Gate
IDLEon!c_demandand construct the condition vector that is then charged to nothing. Which property catches it, and why does the sum property alone not? -
Using §16's
masked_cycles(), derive the maximum possible undercount for a window ofWcycles withCcauses. Then state the condition under which a priority-ordered attribution and a per-constraint activity count give identical answers. -
§13 publishes
ctrl_denas window minus refresh. Argue whethertCCD GAPcycles should also be subtracted before judging a controller, and what 23.4 would have to establish for the answer to be yes. -
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
Related tutorials
- Related topic
SDR SDRAM
Making DRAM synchronous replaced an analog timing negotiation with a clocked contract, which is what made pipelining and counted bursts possible. It also fixes the vocabulary the rest of the curriculum depends on: clock frequency, transfer rate, data rate and bandwidth are four different quantities.
- Related topic
HBM Overview
HBM reaches hundreds of GB/s with a per-pin rate lower than DDR5's. It wins on width, not speed — and getting that width required changing the packaging, which adds a fourth design layer to array physics, device architecture and the interface.
- Related topic
DQ — The Data Bus
Saying DQ is bidirectional says almost nothing. What matters is who drives these wires right now, how ownership changes, and what guarantees both sides never drive at once — which is a state machine, not a property.
- Related topic
Data-Transfer Efficiency
Bursting buys command efficiency and can spend payload efficiency to get it. Those are different quantities that trade against each other, and collapsing them into a single percentage is how architectural arguments go wrong.
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.
