DDR · Module 11
Write Timing Analysis
A write has five events owed by two parties, and only three appear on any interface. Reading a write trace means computing the rest — and every way that goes wrong produces silent corruption rather than an error.
Four chapters have built the pieces. A transaction created with a debt (11.1), a deadline scheduled (11.2), a burst sustained (11.3), and an obligation outlasting the data (11.4).
This chapter puts them together, and the difficulty is specific:
Given a write trace, are the command, the launch, the burst and the recovery mutually consistent?
A write has five events, owed by two parties, and only two of them appear on any wire. The command is observable and the beats are observable. The launch deadline, the recovery obligation, and the moment the write is actually finished are all computed — from a configuration that must be established before any measurement means anything.
And unlike the read side, every way this goes wrong produces silent corruption rather than an error.
1. Five Events, Two Observable
Chapter 11.4 §3 listed the five events. Here they are again with the column that makes analysis hard:
| # | Event | Owed by | Observable on an interface? |
|---|---|---|---|
| 1 | Request admitted | controller | no — internal |
| 2 | Command accepted | DRAM | yes — a command on the CA bus |
| 3 | Data launched | controller | yes — first beat driven |
| 4 | Final beat delivered | controller | yes — last beat driven |
| 5 | Recovery satisfied | device | no — nothing marks it |
Events 2, 3 and 4 are visible. Events 1 and 5 are not, and 5 is the one that matters most, because it is the precondition for closing the row and its violation is silent.
So a write trace supports exactly two direct measurements:
command event → first beat = the observed launch offset
first beat → last beat = the observed burst durationEverything else must be computed: whether the launch offset matches the configuration, whether the burst had the right number of beats, and whether enough time elapsed before the bank was closed — which requires knowing a configured interval that no observation reveals.
2. The Methodology
The procedure for a real write trace. Steps 1 to 3 involve no waveform, and skipping them is why analyses reach confident wrong conclusions.
1 — Establish the generation. Burst length, latency terminology and recovery semantics all depend on it. Chapter 11.3 §1's beat counts differ between DDR4 and DDR5.
2 — Establish the operating point. The marketed rate, and from it the clock period by Chapter 10.2 §3's derivation: MT/s ÷ 2 = MHz.
3 — Establish the configuration. The mode-register settings actually in force: the write latency, any additive latency, the burst configuration, and the write-recovery setting. Chapter 11.4 §4 established that recovery is configured rather than fixed — read the registers back rather than assuming what was programmed.
4 — Identify the write command sampling event. Chapter 6.1 and Chapter 7.1 §2. The origin of every measurement, and measuring from the wrong one shifts everything uniformly, which looks exactly like a latency error.
5 — Determine the applicable write latency. The total the controller must meet, including any additive component — not CWL alone. Chapter 11.2 §3.
6 — Locate the expected launch window. From 4 and 5, remembering it is a window rather than an instant: the strobe has a preamble before data is valid (Chapter 6.10).
7 — Determine the burst duration. In transfers, converted to clock periods before comparing against anything in cycles. Chapter 11.3 and Chapter 10.4 §6.
8 — Identify the final write-data event. The last beat — and this is the anchor for everything that follows, because recovery is measured from it.
9 — Determine the applicable recovery requirement, from step 3's configuration.
10 — Check whether the next operation to that bank was legal. Specifically, whether any precharge — explicit or implied by an auto-precharge flag — fell inside the recovery interval.
11 — Separate controller obligations from device obligations. Events 1 to 4 are the controller's; event 5 is the device's. A fault in the first four is a controller defect; a fault in the fifth is a controller defect too, but of a different kind: failing to wait for something it does not own.
3. What Integration Adds
Each earlier block is deliberately blind to the others, and the blindness is what makes integration produce something new.
Chapter 11.2's pipeline schedules and cannot observe. It will schedule deadlines for a device that is not listening.
Chapter 11.3's sequencer delivers and cannot time. A burst delivered perfectly at entirely the wrong cycle passes all its checks.
Chapter 11.4's guard blocks and cannot see the data. It trusts whatever tells it the data is done.
Together they can compare a schedule against an observation and anchor an obligation to a real event — which is the only way to detect either of the two silent failures:
expected launch offset (11.2, from configuration)
observed launch offset (11.3, from the first beat)
────────────────────────────
difference → data is landing in the wrong cycle
observed last beat (11.3)
observed next precharge (the command stream)
────────────────────────────
difference vs config → the row was closed too earlyNeither comparison exists inside any single block, and both are the ones whose failure corrupts data.
4. RTL — The Integrated Write Transaction Tracker
The engineering problem
Carry one write through all five events, measure the launch offset against the expectation, anchor the recovery obligation to the observed final beat, and report every inconsistency as a distinct condition.
Why hardware needs it
A controller that knows only whether a write "completed" cannot distinguish a write that landed correctly from one that landed in the wrong cycle, or one whose row was closed too soon. Measuring is what turns a working design into a characterised one — and on the write side, an uncharacterised design's failure mode is corruption.
Classification
SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
What it models
A write transaction's phase through all five events; its age; the observed launch offset compared against the expected one; beat progress; the recovery obligation anchored to the observed final beat; and five reported anomalies.
What it does NOT model
The device. Data correctness — it stores no data. Command-to-command legality beyond the one recovery constraint (Modules 13, 14). The PHY (Modules 19 to 22). Scheduling or multiple outstanding writes (Module 17). Masking (Chapter 6.11).
Interface and parameter contract
// ─────────────────────────────────────────────────────────────────────────
// write_transaction_tracker
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
//
// MODELS: one write transaction's phase through all five events of
// Chapter 11.4 Section 3, its age, the OBSERVED launch offset compared
// against the EXPECTED one, beat progress, and the recovery obligation
// ANCHORED TO THE OBSERVED FINAL BEAT.
//
// EXPECT_LAUNCH and RECOVERY_EVENTS ARE EDUCATIONAL MODEL PARAMETERS, NOT
// JEDEC VALUES. A real controller derives the first from the configured
// write latency plus the PHY's contribution (Module 19), the second from
// the configured write recovery.
//
// OVERDUE_CYCLES IS AN EDUCATIONAL THRESHOLD, NOT A PROTOCOL TIMEOUT. DDR
// has no write timeout.
//
// ONE OUTSTANDING WRITE. Not a scheduler, not a timing engine, not a DRAM
// model. MODELS NO PHYSICAL OR ANALOG BEHAVIOUR.
// ─────────────────────────────────────────────────────────────────────────
// The module in a type. Chapter 11.4 Section 3's five events as phases.
typedef enum logic [2:0] {
WTXN_IDLE = 3'd0,
WTXN_AWAIT_DATA = 3'd1, // admitted; the launch deadline has not elapsed
WTXN_BURSTING = 3'd2, // beats in flight; the controller owes the bus
WTXN_RECOVERING = 3'd3, // data delivered; the DEVICE owes internal work
WTXN_SAFE = 3'd4 // every obligation discharged; may retire
} wtxn_phase_e;
module write_transaction_tracker #(
parameter int TAG_W = 3,
parameter int BEATS_PER_WRITE = 8,
parameter int EXPECT_LAUNCH = 6,
parameter int RECOVERY_EVENTS = 6,
parameter int OVERDUE_CYCLES = 64,
parameter int CNT_W = (BEATS_PER_WRITE <= 1) ? 1 : $clog2(BEATS_PER_WRITE + 1),
parameter int AGE_W = (OVERDUE_CYCLES <= 1) ? 1 : $clog2(OVERDUE_CYCLES + 1),
parameter int REC_W = (RECOVERY_EVENTS <= 1) ? 1 : $clog2(RECOVERY_EVENTS + 1)
) (
input logic clk,
input logic rst_n,
// ── Event 1, from Chapter 11.1.
input logic accept,
input logic [TAG_W-1:0] accept_tag,
// ── Events 3 and 4, from Chapter 11.3's sequencer.
input logic beat_valid,
input logic beat_last,
// ── A precharge somebody wants to issue to this write's bank.
input logic precharge_req,
output wtxn_phase_e phase,
output logic txn_active,
output logic [TAG_W-1:0] txn_tag,
output logic [AGE_W-1:0] txn_age,
output logic [CNT_W-1:0] beats_seen,
// ── THE INTEGRATION. Observed launch offset against expected.
output logic [AGE_W-1:0] measured_launch,
output logic measured_valid,
output logic launch_early,
output logic launch_late,
// ── Event 5, anchored to the observed final beat.
output logic [REC_W-1:0] recovery_remaining,
output logic precharge_blocked,
output logic precharge_violation,
// ── Retirement is a phase, not a pulse: WTXN_SAFE.
output logic txn_safe,
output logic txn_overdue,
output logic phantom_beat
);
if (TAG_W < 1) begin : g_tw
initial $fatal(1, "write_transaction_tracker: TAG_W must be >= 1");
end
if (BEATS_PER_WRITE < 1) begin : g_bp
initial $fatal(1, "write_transaction_tracker: BEATS_PER_WRITE must be >= 1");
end
if (EXPECT_LAUNCH < 0) begin : g_el
initial $fatal(1, "write_transaction_tracker: EXPECT_LAUNCH must be >= 0");
end
if (RECOVERY_EVENTS < 0) begin : g_re
initial $fatal(1, "write_transaction_tracker: RECOVERY_EVENTS must be >= 0");
end
// The threshold must exceed the launch expectation, or every transaction
// is overdue before its data was ever due.
if (OVERDUE_CYCLES <= EXPECT_LAUNCH) begin : g_ov
initial $fatal(1, "write_transaction_tracker: OVERDUE_CYCLES must exceed EXPECT_LAUNCH");
end
wtxn_phase_e phase_q;
logic [TAG_W-1:0] tag_q;
logic [AGE_W-1:0] age_q;
logic [CNT_W-1:0] beats_q;
logic [REC_W-1:0] rec_q;
assign phase = phase_q;
assign txn_tag = tag_q;
assign txn_age = age_q;
assign beats_seen = beats_q;
assign recovery_remaining = rec_q;
assign txn_active = (phase_q != WTXN_IDLE);
assign txn_safe = (phase_q == WTXN_SAFE);
// A beat with no transaction expecting one.
assign phantom_beat = beat_valid
&& (phase_q != WTXN_AWAIT_DATA)
&& (phase_q != WTXN_BURSTING);
// ── The measurement. Taken once, on the first beat of the burst.
logic first_beat_now;
assign first_beat_now = beat_valid && (phase_q == WTXN_AWAIT_DATA);
assign measured_valid = first_beat_now;
assign measured_launch = age_q;
assign launch_early = first_beat_now && (age_q < AGE_W'(EXPECT_LAUNCH));
assign launch_late = first_beat_now && (age_q > AGE_W'(EXPECT_LAUNCH));
// ── The recovery obligation. Chapter 11.4's contract, anchored here to
// the OBSERVED final beat rather than to a separate input -- which is
// what makes this block an integration rather than a composition.
assign precharge_blocked = (phase_q == WTXN_RECOVERING);
assign precharge_violation = precharge_req
&& ((phase_q == WTXN_RECOVERING)
|| (phase_q == WTXN_BURSTING)
|| (phase_q == WTXN_AWAIT_DATA));
// Overdue: still waiting for the first beat. A burst in progress is not
// overdue, and conflating them would report a slow burst as a
// non-delivery -- Chapter 10.5's distinction, applied here.
assign txn_overdue = (phase_q == WTXN_AWAIT_DATA)
&& (age_q >= AGE_W'(OVERDUE_CYCLES));
logic at_last_beat;
assign at_last_beat = (beats_q + CNT_W'(1)) == CNT_W'(BEATS_PER_WRITE);
always_ff @(posedge clk) begin
if (!rst_n) begin
phase_q <= WTXN_IDLE;
tag_q <= '0;
age_q <= '0;
beats_q <= '0;
rec_q <= '0;
end else if (accept) begin
phase_q <= WTXN_AWAIT_DATA;
tag_q <= accept_tag;
age_q <= '0;
beats_q <= '0;
rec_q <= '0;
end else begin
// Age advances in every active phase, saturating at the threshold.
if ((phase_q != WTXN_IDLE) && (age_q < AGE_W'(OVERDUE_CYCLES)))
age_q <= age_q + AGE_W'(1);
unique case (phase_q)
WTXN_AWAIT_DATA: begin
if (beat_valid) begin
if (at_last_beat) begin
// Single-beat burst: straight to recovery.
beats_q <= '0;
rec_q <= REC_W'(RECOVERY_EVENTS);
phase_q <= (RECOVERY_EVENTS == 0) ? WTXN_SAFE : WTXN_RECOVERING;
end else begin
beats_q <= CNT_W'(1);
phase_q <= WTXN_BURSTING;
end
end
end
WTXN_BURSTING: begin
if (beat_valid) begin
if (at_last_beat) begin
beats_q <= '0;
rec_q <= REC_W'(RECOVERY_EVENTS);
// EVENT 4 → EVENT 5. The recovery obligation is anchored
// here, to the observed final beat -- Chapter 11.4 Section 3.
phase_q <= (RECOVERY_EVENTS == 0) ? WTXN_SAFE : WTXN_RECOVERING;
end else if (beats_q < CNT_W'(BEATS_PER_WRITE)) begin
beats_q <= beats_q + CNT_W'(1);
end
end
end
WTXN_RECOVERING: begin
if (rec_q <= REC_W'(1)) begin
rec_q <= '0;
phase_q <= WTXN_SAFE;
end else begin
rec_q <= rec_q - REC_W'(1);
end
end
WTXN_SAFE: begin
// Held until a new acceptance. A consumer reads txn_safe and
// retires; there is no separate retire input, because the phase
// IS the retirement condition.
phase_q <= WTXN_SAFE;
end
default: phase_q <= WTXN_IDLE;
endcase
end
end
endmoduleState representation and transitions
One phase enum, a tag, a saturating age, a beat count and a recovery count.
IDLE --accept--> AWAIT_DATA
AWAIT_DATA --first beat (measure!)--> BURSTING
BURSTING --last beat--> RECOVERING
RECOVERING --count elapses--> SAFE
SAFE --accept--> AWAIT_DATAThe phase enum is the module in a type, and it makes two things structural that were previously conventions: a precharge is blocked in three phases, not one — during recovery, during the burst, and while awaiting data, because closing a bank whose write has not even been delivered is at least as bad. And txn_safe is a phase rather than a pulse, so a consumer cannot retire a transaction by catching an edge and missing it.
Sequential behaviour and reset
Nonblocking throughout. The age saturates rather than wrapping, for Chapter 9.6 §5's reason.
Reset abandons the transaction at any phase, which is a genuine hazard whose severity depends on the phase: abandoning in AWAIT_DATA leaves the device expecting data that never comes; abandoning in RECOVERING forgets an obligation the device still holds. Chapter 11.4 §5's note, and the reason both chapters recommend establishing a known state after reset rather than resuming.
Cycle-by-cycle trace
EXPECT_LAUNCH = 4, BEATS_PER_WRITE = 4, RECOVERY_EVENTS = 3, with the launch one cycle late:
| Cycle | Event | phase | age | measured_launch | Flag |
|---|---|---|---|---|---|
| 0 | accept tag 3 | AWAIT_DATA | 0 | — | — |
| 1–4 | waiting | AWAIT_DATA | 1…4 | — | — |
| 5 | first beat | BURSTING | 5 | 5 | launch_late |
| 6–7 | beats 2, 3 | BURSTING | 6, 7 | — | — |
| 8 | beat 4 + last | RECOVERING | 8 | — | precharge_blocked |
| 9–10 | recovery | RECOVERING | 9, 10 | — | precharge_blocked |
| 11 | — | SAFE | 11 | — | txn_safe |
Cycle 5 is the first finding: the burst was well-formed and arrived a cycle late. Cycle 11 is the second: the write is finished three cycles after its last beat, and a precharge anywhere in cycles 8 to 10 would have corrupted it.
How to simulate, and expected output
Drive a nominal write and confirm measured_launch == EXPECT_LAUNCH with neither flag, then SAFE exactly RECOVERY_EVENTS cycles after the last beat. Then:
Launch early and late by one — confirm the correct flag and that the transaction still progresses. A launch discrepancy is not a completion failure.
precharge_req in each of the three blocking phases — confirm precharge_violation in all three, including AWAIT_DATA, which is the phase people forget.
No beats at all — txn_overdue at the threshold, and the age then stops. Confirm it does not assert during a burst.
RECOVERY_EVENTS = 0 — the phase goes straight from the last beat to SAFE. This exercises the conditional transition and is the only configuration where RECOVERING is never entered.
BEATS_PER_WRITE = 1 — first and last on one beat, from AWAIT_DATA directly to RECOVERING, never entering BURSTING.
OVERDUE_CYCLES <= EXPECT_LAUNCH must not elaborate.
Synthesis implications
A phase register, three counters and a tag — around 25 flops, per outstanding write. The cost scales with concurrency, which is the real reason controllers bound their outstanding writes rather than with any protocol limit.
Corner cases
RECOVERY_EVENTS == 0 skips RECOVERING and describes no real device; it exists so a consumer need not special-case it. EXPECT_LAUNCH == 0 is legal, though accept takes priority in the same cycle so the earliest measurable offset is 1. BEATS_PER_WRITE == 1 skips BURSTING. The unique case keeps a default that returns to IDLE for an out-of-range phase encoding — the safest recovery from a corrupted state, because continuing from a plausible phase would mask it.
Failure modes and debugging clues
launch_late on every write by a constant is a configuration error — Chapter 11.2 §9. precharge_violation at all is a consumer defect. Phase stuck in AWAIT_DATA means the launch path never delivered, which is Chapter 11.1 §9's payload_owed case seen from the tracker. Phase stuck in RECOVERING means RECOVERY_EVENTS is larger than intended.
Limitations
One outstanding write. No data, so it cannot detect a payload fault. The measured launch is in the controller's domain and includes the PHY's contribution — it is not the device's write latency. And it enforces one timing constraint; !precharge_blocked is necessary and not sufficient.
5. RTL — The Verification-Only Write-Path Checker
The engineering problem
Watch the same write independently and report what a design cannot report about itself: the distribution of launch offsets and recovery margins, and violations counted rather than merely flagged.
Classification
VERIFICATION-ONLY EDUCATIONAL MODEL. Not intended for synthesis.
Interface and parameter contract
// ─────────────────────────────────────────────────────────────────────────
// write_path_checker
//
// Classification: VERIFICATION-ONLY EDUCATIONAL MODEL.
// Not intended for synthesis. It drives nothing and exists to observe.
//
// MODELS: independent measurement of command-to-first-beat offsets and
// last-beat-to-precharge margins, their observed ranges, and counts of
// writes, bursts and orphan beats.
//
// TAKES NO EXPECTED VALUES, DELIBERATELY. A checker sharing the design's
// constants is wrong together with it and reports a clean check (Chapters
// 11.2 and 11.4). This MEASURES; the judgement belongs to whoever knows
// the configuration.
//
// REPORTS THE MINIMUM RECOVERY MARGIN. A design that never violates but
// routinely comes within one event of violating is about to fail on a
// different part, and only the margin shows that.
//
// Counters SATURATE, never wrap. MODELS NO PHYSICAL OR ANALOG BEHAVIOUR.
// ─────────────────────────────────────────────────────────────────────────
module write_path_checker #(
parameter int OFF_W = 8,
parameter int ACC_W = 16
) (
input logic clk,
input logic rst_n,
// Observed events only. No configuration, no expectation.
input logic obs_write_cmd,
input logic obs_first_beat,
input logic obs_last_beat,
input logic obs_precharge,
// Launch offsets: command → first beat.
output logic [OFF_W-1:0] launch_min,
output logic [OFF_W-1:0] launch_max,
output logic launch_range_valid,
// Recovery margins: last beat → precharge. THE MINIMUM IS THE FINDING.
output logic [OFF_W-1:0] recovery_margin_min,
output logic margin_valid,
output logic [ACC_W-1:0] cnt_writes,
output logic [ACC_W-1:0] cnt_bursts,
// A first beat with no command awaiting one.
output logic [ACC_W-1:0] cnt_orphan_beats,
output logic any_saturated
);
if (OFF_W < 1) begin : g_ow
initial $fatal(1, "write_path_checker: OFF_W must be >= 1");
end
if (ACC_W < 2) begin : g_aw
initial $fatal(1, "write_path_checker: ACC_W must be >= 2");
end
localparam logic [OFF_W-1:0] OFF_MAX = {OFF_W{1'b1}};
localparam logic [ACC_W-1:0] ACC_MAX = {ACC_W{1'b1}};
logic awaiting_q; // command seen, first beat not yet
logic [OFF_W-1:0] launch_age_q;
logic post_data_q; // last beat seen, precharge not yet
logic [OFF_W-1:0] margin_age_q;
always_ff @(posedge clk) begin
if (!rst_n) begin
awaiting_q <= 1'b0;
launch_age_q <= '0;
post_data_q <= 1'b0;
margin_age_q <= '0;
launch_min <= OFF_MAX;
launch_max <= '0;
launch_range_valid <= 1'b0;
recovery_margin_min <= OFF_MAX;
margin_valid <= 1'b0;
cnt_writes <= '0;
cnt_bursts <= '0;
cnt_orphan_beats <= '0;
any_saturated <= 1'b0;
end else begin
// ── Launch offset.
if (obs_write_cmd) begin
awaiting_q <= 1'b1;
launch_age_q <= '0;
if (cnt_writes == ACC_MAX) any_saturated <= 1'b1;
else cnt_writes <= cnt_writes + ACC_W'(1);
end else if (awaiting_q) begin
if (launch_age_q == OFF_MAX) any_saturated <= 1'b1;
else launch_age_q <= launch_age_q + OFF_W'(1);
end
if (obs_first_beat) begin
if (awaiting_q) begin
awaiting_q <= 1'b0;
if (!launch_range_valid || (launch_age_q < launch_min))
launch_min <= launch_age_q;
if (!launch_range_valid || (launch_age_q > launch_max))
launch_max <= launch_age_q;
launch_range_valid <= 1'b1;
end else begin
if (cnt_orphan_beats == ACC_MAX) any_saturated <= 1'b1;
else cnt_orphan_beats <= cnt_orphan_beats + ACC_W'(1);
end
end
// ── Recovery margin. Measured from the last beat to the next
// precharge; only the MINIMUM is retained, because that is the
// number that says how close the design came.
if (obs_last_beat) begin
post_data_q <= 1'b1;
margin_age_q <= '0;
if (cnt_bursts == ACC_MAX) any_saturated <= 1'b1;
else cnt_bursts <= cnt_bursts + ACC_W'(1);
end else if (post_data_q) begin
if (margin_age_q == OFF_MAX) any_saturated <= 1'b1;
else margin_age_q <= margin_age_q + OFF_W'(1);
end
if (obs_precharge && post_data_q) begin
post_data_q <= 1'b0;
if (!margin_valid || (margin_age_q < recovery_margin_min))
recovery_margin_min <= margin_age_q;
margin_valid <= 1'b1;
end
end
end
endmoduleWhat it teaches
recovery_margin_min is the number to put on a dashboard. It answers how close did we come, which a pass/fail check cannot, and it is the only cheap early warning for a constraint whose violation is silent.
A spread between launch_min and launch_max means the offset is varying — a configuration change mid-run, or something genuinely unstable below the boundary.
cnt_writes − cnt_bursts is the number of writes whose data never completed.
Limitations
One outstanding write assumed — the two flags are single. It measures controller-domain offsets, which include the PHY's contribution. It cannot see auto-precharge closures, which produce no precharge command — Chapter 11.4 §6's gap, and the reason a real checker must also key on the auto-precharge flag.
6. A Complete Write, in Cycles
write_transaction_tracker — five events, two measurements
10 cyclesThe three phase labels are the module. The controller owes data, then owes the bus, then owes nothing while the device owes internal work — and only after all three is the transaction safe.
Read the trace as four separate verdicts, which is the skill this chapter teaches:
Did the command go out? Yes.
Was the burst well-formed? Yes — four beats, last on the fourth, phase advanced correctly.
Did the data launch when expected? No. Expected at an age of 3; measured 4. Every other check passes and only the comparison notices.
Was the row closed too early? Not in this trace — but precharge_blocked marks the window in which it would have been, and nothing on any interface would have reported it.
And note what the finding is not. "The launch is late" is not "the device is slow" — §2's step 11 must run before anyone knows whether the extra cycle belongs to the controller's model, the PHY, or an expectation that was wrong.
EDUCATIONAL TRACE — NOT TO SCALE, NOT A JEDEC TIMING DIAGRAM. One beat per cycle is the readability convention; the real cadence is two transfers per clock period.
7. Five Assertions Worth Writing
// P1 -- phases advance only through the defined sequence. The five events
// as a contract: a transaction cannot reach recovery without a burst, or
// safety without recovery, which is what a shortcut in a consumer's
// retirement logic would produce.
property p_phase_sequence_is_respected;
@(posedge clk) disable iff (!rst_n)
(phase == WTXN_RECOVERING) && ($past(phase) != WTXN_RECOVERING)
|-> ($past(phase) == WTXN_BURSTING)
|| ($past(phase) == WTXN_AWAIT_DATA);
endproperty
assert property (p_phase_sequence_is_respected);
// P2 -- the launch measurement is taken once, on the first beat only. A
// measurement republished per beat would turn one late write into a
// fabricated pattern of increasingly late writes -- a convincing and
// entirely false diagnosis.
property p_measurement_is_once;
@(posedge clk) disable iff (!rst_n)
measured_valid |-> ($past(phase) == WTXN_AWAIT_DATA)
&& (beats_seen == '0);
endproperty
assert property (p_measurement_is_once);
// P3 -- a precharge is blocked in EVERY phase where it would be
// destructive, not only during recovery. Closing a bank whose write has
// not been delivered is at least as bad as closing one mid-recovery, and
// a guard that only covered recovery would permit it.
property p_precharge_blocked_in_all_unsafe_phases;
@(posedge clk) disable iff (!rst_n)
precharge_req && (phase != WTXN_IDLE) && (phase != WTXN_SAFE)
|-> precharge_violation;
endproperty
assert property (p_precharge_blocked_in_all_unsafe_phases);
// P4 -- safety is reached only after recovery is discharged. THE
// capstone's central property: it is what stops a transaction being
// declared finished while the device still holds an obligation.
property p_safe_requires_recovery_discharged;
@(posedge clk) disable iff (!rst_n)
(phase == WTXN_SAFE) && ($past(phase) != WTXN_SAFE)
|-> ($past(recovery_remaining) <= REC_W'(1));
endproperty
assert property (p_safe_requires_recovery_discharged);
// P5 -- overdue means awaiting data, not slow. A burst in progress is
// never overdue; conflating them reports a slow transfer as a
// non-delivery and sends the investigation to the wrong layer.
property p_overdue_means_awaiting;
@(posedge clk) disable iff (!rst_n)
txn_overdue |-> (phase == WTXN_AWAIT_DATA) && (beats_seen == '0);
endproperty
assert property (p_overdue_means_awaiting);What these prove. P1 makes the five-event sequence structural. P2 is subtler than it looks — a republished measurement fabricates a pattern that is more convincing than the real fault. P3 closes the gap a recovery-only guard leaves. P4 is the capstone property: a write is not finished until the device's obligation is discharged, and it is what a consumer retiring on the last beat violates. P5 keeps the overdue report meaningful.
What these do not prove. Nothing proves the data was correct — no block in this module stores data. Nothing proves EXPECT_LAUNCH or RECOVERY_EVENTS are right: a tracker configured wrongly satisfies every property while landing writes in the wrong cycle or permitting precharges that corrupt them, which is why §5's checker takes no expectations at all. Nothing proves JEDEC timing compliance — this is one constraint out of many, and !precharge_blocked remains necessary and not sufficient. Nothing proves the device honoured its own obligation. And nothing proves anything below the PHY boundary.
8. DV — Six Independent Dimensions
The module's verification argument, assembled. A write fails in six independent ways, and a single scoreboard collapses them.
| Dimension | Question | Detected by | Signature |
|---|---|---|---|
| Command | was the right write issued? | command monitor | wrong bank/column on the CA bus |
| Address | did it target the intended location? | reconstruction (Ch 8.6) | correct data at the wrong address |
| Payload | was the right data supplied? | scoreboard + reference memory | mismatch at a correct address |
| Beat structure | right count, right flags, no gaps? | Ch 11.3's sequencer | underrun, err_restart |
| Launch timing | did the data land in the right cycle? | §4's measurement | launch_late, or a spread in §5 |
| Recovery | was the row closed too early? | §4's phase, §5's margin | precharge_violation, or a small margin |
Every pair can fail independently. Correct payload delivered at the wrong cycle. Correct timing carrying the wrong bytes. A perfect burst whose row is closed one event too soon. And crucially, four of the six produce identical symptoms — wrong memory — with no error anywhere.
Five requirements for an environment that can localise:
Build expectations from admitted requests, carrying the payload. Chapter 11.1 §8.
Update the reference memory at the final delivered beat, applying the mask. Chapter 11.3 §8 works through why not earlier and not later.
Keep the functional model and the recovery checker separate. They answer different questions and merging them produces something wrong about both.
Measure both intervals and report their distributions — launch offset and recovery margin — rather than checking against constants the design also uses.
Reconcile counts at the end of every run. Writes admitted, bursts completed, beats delivered, underruns, orphan beats, precharge violations, and the minimum recovery margin. Those seven numbers take minutes to add and catch more than any per-beat check.
9. Debugging — Single Writes Pass, Back-to-Back Writes Fail
Symptom. Isolated writes read back correctly. Writes issued close together corrupt — wrong data, or data at the wrong location, with the failure rate rising with density.
The symptom localises itself immediately: something has a single-transaction assumption that isolated traffic never violates, and it is above the PHY boundary, because the device serves the commands it is given without any notion of how many the controller has in flight.
Candidate mechanisms.
- A second write is admitted while one is outstanding and the launch association cannot tell them apart. Chapter 11.1's single-outstanding admission prevents this — if relaxed without building the association, this is the result.
- The payload buffer cannot sustain two bursts in quick succession, so the second underruns. Chapter 11.3's
underrun. - The recovery guard is shared rather than per bank, so one write's completion releases a block another write still needs. Chapter 11.4 §13's exercise 5.
- The tracker's phase is overwritten by the second acceptance while the first is still recovering — so the first write's recovery obligation is silently discarded.
- The monitor has a single-outstanding model and the design does not.
Evidence to collect. The number of transactions outstanding at each failure. underrun and precharge_violation counts. The minimum recovery margin from §5. Whether the failing precharge and the failing write targeted the same bank. And whether the first write of each pair is correct.
Discriminator.
- Was more than one write outstanding at the failure? If not, the density is coincidental.
- Is
underrunasserting? Mechanism 2, and the fix is buffering rather than sequencing — Chapter 11.3 §5's extension idea. - Is the first write of a pair correct and the second wrong? Mechanism 1 — the association is broken for the later transaction. If the first is wrong and the second is fine, that is mechanism 4, and it is the more dangerous one: the second acceptance destroyed the first's recovery obligation, so the corruption belongs to the write that appeared to succeed.
- Is
recovery_margin_minat or near zero? Mechanism 3 or 4. Check whether the precharge and the write shared a bank; if they did not, the guard is shared and the structural fix is per-bank instantiation. - Check the monitor against a known-good design. Mechanism 5 is more common than it deserves to be.
Responsible layer. All of it is layer C, above the boundary — and that conclusion, reached from the symptom alone, eliminates the most expensive place to look before any evidence is gathered.
Fix. Per mechanism. And whichever it is, the structural fix is the same: one tracker per outstanding write, one recovery guard per bank, a stated association rule, and an assertion that every launch and every recovery obligation belongs to exactly one record.
10. Common Misconceptions
"Correct data proves the timing is correct."
Why it is tempting: the data read back matches, so everything upstream must have worked.
Concrete failure: a design landing data at the edge of the sampling window, or closing rows with one event of recovery margin, passes every functional test and fails on the next board or at the next temperature.
Correct model: correctness and margin are separate questions. Measure both intervals even when the data passes. §2's closing callout.
Prevention: recovery_margin_min and the launch distribution on the dashboard.
"Correct timing proves the payload is correct."
Why it is tempting: it is the converse and feels equally reasonable.
Concrete failure: a timing checker passes while the payload source supplied the wrong bytes, or the mask was misapplied — a perfectly timed write of the wrong data.
Correct model: six independent dimensions, §8. Timing says when; a reference model says what.
Prevention: separate checkers with separate reports.
"A waveform with four cycles proves a four-cycle JEDEC parameter."
Why it is tempting: the number is concrete and visible.
Concrete failure: an educational figure quoted in a review as a device value.
Correct model: every waveform in this module is labelled educational. Real values come from the device's documentation.
Prevention: treat any tutorial number as illustrative, including all of this chapter's.
"The observed launch offset is the device's write latency."
Why it is tempting: it is the interval you can measure.
Concrete failure: EXPECT_LAUNCH set from a datasheet's CWL, every write late by the PHY's contribution, then "fixed" by tuning the constant — which breaks after retraining.
Correct model: the controller-domain offset includes the PHY's share. §4's header, and Module 19 owns that term.
Prevention: §2's step 11 — does the discrepancy move with temperature or retraining?
"A write is finished when its last beat is sent."
Why it is tempting: every visible obligation is discharged at that point.
Concrete failure: a consumer retiring on the last beat frees the bank for a precharge that corrupts the write. Chapter 11.4's case, and the module's most damaging single error.
Correct model: the phase enum. WTXN_RECOVERING exists between the last beat and safety, and P4 forbids skipping it.
Prevention: retire on txn_safe, never on beat_last.
"A recovery violation will show up as a protocol error."
Why it is tempting: every other violation an engineer has met is refused or flagged.
Concrete failure: a team waits for an error that never comes; the violation surfaces as field data corruption.
Correct model: the device accepts the precharge. Chapter 11.4 §7. The only evidence is wrong data later.
Prevention: precharge_violation as a first-class simulation error, and the margin measurement for what it cannot catch.
11. Interview Reasoning
"Walk me through analysing a write trace you have never seen."
Before opening it: establish the generation, the operating point, and the mode-register configuration actually in force — including the write-recovery setting, read back rather than assumed. Those fix the burst length, the latency terminology and the clock period. Then identify the write command sampling event, which is the origin; determine the applicable write latency including any additive component; locate the launch window; convert the burst duration into whatever unit you will compare against; identify the final write-data event, because recovery is anchored there; and check whether any precharge — explicit or implied by an auto-precharge flag — fell inside the recovery interval. Then measure, and only then judge.
"How is analysing a write different from analysing a read?"
Five events instead of three, owed by two parties, and only two of the five appear on any wire. The launch deadline and the recovery obligation are both computed, not observed. And the consequences invert: a read's central failure announces itself as missing data, so a sloppy method costs time. A write's two invisible failures both produce silent corruption — data in the wrong cycle, or a row closed mid-write — discovered by a data check long after the cause. On the write side the discipline is not a matter of efficiency.
"How would you detect that a controller is closing rows too early?"
By measuring the interval from each write's last beat to the next precharge of that bank and reporting the minimum observed margin, not a pass/fail verdict. A violation is silent — the device accepts the precharge — so a check that only fires on violations tells you nothing until data is already wrong. The minimum margin answers how close the design came, which is the early warning. Two refinements matter: derive the required interval from the programmed mode registers rather than the design's constant, and include auto-precharge, where no precharge command appears on the interface at all and a command-keyed checker sees nothing.
"Writes work individually and corrupt back-to-back. What does that tell you?"
That something has a single-transaction assumption, and that the fault is above the PHY boundary — the device serves commands without any notion of how many the controller has in flight, so concurrency-dependent failures are never device faults. That eliminates the most expensive place to look immediately. Then the discriminators are narrow: an underrun points at the payload buffer; the second write being wrong points at a broken association; the first write being wrong points at its recovery obligation having been discarded by the second acceptance, which is the nastier case because the corruption belongs to the write that looked successful.
"What is the single most valuable number to put on a write dashboard?"
The minimum recovery margin. It is the only cheap early warning for a constraint whose violation is silent, permanent and unreported — and unlike a launch-offset check it cannot be made to agree with itself by sharing a constant, because it is a measurement rather than a comparison. A design showing a margin of zero has already failed somewhere; a design showing one event of margin will fail on a different part; and neither of those is visible in a pass/fail result.
12. Engineering Exercise
EXPECT_LAUNCH = 6, BEATS_PER_WRITE = 8, RECOVERY_EVENTS = 6, part at 3200 MT/s.
1. Derive the clock period. Give the total educational command-to-safe span in cycles, showing every term.
2. A write is accepted at cycle 200 and its first beat arrives at 207. Which outputs assert and what is measured_launch?
3. For that write, at which cycle does the phase become SAFE, and in which cycle range would a precharge have been a violation?
4. A consumer retires the transaction on beat_last. Give the failure and the property that catches it.
5. cnt_writes is 8,000, cnt_bursts is 8,000, cnt_orphan_beats is 0, and recovery_margin_min is 0. Is anything wrong?
6. Name the six checking dimensions of §8 and give, for each, one failure that produces wrong memory with no error reported.
13. Summary
A write has five events, owed by two parties, and only three of them appear on an interface. The command and the beats are observable; the admission, the launch deadline and the recovery obligation are computed — from a configuration that must be established before any measurement means anything.
Two direct measurements exist: the launch offset, command to first beat, and the recovery margin, last beat to the next precharge of that bank. Record both for every write, even when they pass, because a design with one event of margin is a design about to fail elsewhere.
Integration produces two checks no single block can. The pipeline schedules and cannot observe; the sequencer delivers and cannot time; the guard blocks and cannot see the data. Only their composition compares a schedule against an observation and anchors an obligation to a real event — and those are precisely the two failures that corrupt data.
The phase is the transaction. Awaiting, bursting, recovering, safe: the controller owes data, then owes the bus, then owes nothing while the device owes internal work. Retire on safety, never on the last beat.
Six independent checking dimensions — command, address, payload, beat structure, launch timing, recovery. Four of them produce identical symptoms with no error anywhere.
And a checker configured from the design's own constants cannot check them. Measure the offsets, report the ranges, and put the minimum recovery margin on the dashboard — it is the only cheap early warning for a constraint whose violation is silent, permanent and unreported.
14. What Comes Next
Module 11 is complete. A write is now an executable transaction: admitted with a debt, scheduled to a deadline, sustained across a burst that cannot pause, and outlasted by an obligation the controller does not own.
The module's claim in one sentence: a write command does not store data — it commits the controller to producing data at a moment it must compute, and leaves the device holding an obligation that nothing will remind anyone about.
Both data directions now exist as transactions. Module 10 built the read; this module built the write; and the asymmetry between them — a controller that waits versus a controller that owes — has been the organising idea throughout.
Module 12 — Burst Operations takes the thing both directions have in common and gives it the treatment neither chapter could. Both modules used a burst and deliberately took only what a transaction required: Chapter 10.4 and Chapter 11.3 each pointed forward at every turn. Module 12 owns burst length and its trade-offs, sequential and interleaved ordering, and the data-transfer efficiency arithmetic that decides how much of a memory system's theoretical bandwidth a real workload can reach.
And the question deferred in every chapter of both modules — when may any of this be issued relative to everything else — is Modules 13 and 14, which supply the parameters that turn every educational figure here into a real one.
Return to The Write Command for the transaction, Write Latency (CWL) for the deadline, Burst Writes for the sustained delivery, Write Recovery (tWR) for the obligation that outlasts it, Read Timing Analysis for the direction this one mirrors and differs from, and Mode-Register Set for where the configuration all of this depends on actually lives.
Continue learning
Related tutorials
- Related topic
Read Timing Analysis
A read crosses three timing domains, and the common analysis errors are attribution errors — measuring from the wrong event, comparing cycles against transfers, or blaming the layer that reported the fault.
- Related topic
Command Scheduling
An eight-step procedure for deciding when a command becomes temporally legal, worked through a full trace by hand — and a timing checker built in the opposite representation to the design, so the two cannot share a bug.
- Related topic
Write (WR / WRA)
A write's data arrives after its command, which makes three events impossible to conflate: observed, accepted, and completed. A monitor, a protocol checker and a scoreboard each attach to a different one.
- Related topic
Precharge (PRE / PREA)
Precharge closes a bank, and its scope is decided by an operand: one bank or all of them. It is also the command that exposes why a command stream does not fully describe device state — which is the hardest problem a DV monitor faces.
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.
