DDR · Module 21
Read Leveling
The read side has no phase comparator, only a correctness bit. That forces a two-edge search over a full sweep, an MPR stimulus whose format decides what can be trained at all, and per-bit adjustment to recover per-pin skew.
Chapter 21.1 had an unusually generous observable. The device acted as a phase comparator and reported which side of CK the strobe landed on: one bit, directly about the quantity of interest, from a procedure the standard defines end to end.
The read side gets nothing like it. There is no comparator in the DRAM for the read direction — the DRAM is transmitting, and it has no idea where the controller sampled. The only thing the controller can observe is whether the data it got back was the data it expected.
That single substitution changes the whole shape of the search, and this chapter is about the consequences.
The central law, and its qualification, unchanged:
Training is controlled search over configurable PHY state, using observable pass/fail information to discover an operating point with usable timing margin — and training does not create margin. A read path with no working sampling point must fail, not return the least-bad code.
1. A Weaker Observable
It is worth being precise about how much information is lost, because the loss explains every structural difference from 21.1.
| Write levelling (21.1) | Read levelling (this chapter) | |
|---|---|---|
| Observable | The CK level the device sampled | Whether returned data matched |
| What it reports | A side of a boundary | A conjunction of everything working |
| Bits per probe | 1, about one quantity | 1, about all quantities at once |
| A failing result localises | The strobe is on the other side | Nothing |
| A passing result establishes | The edge order at the device | That this pattern survived this path today |
| Shape of the answer | One transition | Two edges bounding a region |
The fourth row is the expensive one. A read comparison that fails tells you the returned data was wrong. It does not tell you the sample point was early, or late, or that the gate was misplaced, or that a lane is swapped, or that the pattern was never driven. Every failure mode in the read path reduces to the same bit.
This is why the read search cannot terminate on a single boundary. In 21.1 a 0-to-1 transition was meaningful because the bit meant side. Here a fail-to-pass transition just means things started working, and the interesting question — where in the working region to sit — needs the other end too.
2. Why the Stimulus Cannot Be Ordinary Memory
The observable is a comparison, so the search needs data whose correct value is known. Reading the array is the obvious idea and it is a bad one, for four independent reasons.
The array contents are unknown at bring-up. DRAM comes out of reset with undefined cell contents. To read known data you must first write it — and writing requires the write path to be trained, which at this point in the sequence it is not. That is a circular dependency, and 21.5 is where such dependencies get ordered.
A write-then-read conflates two paths. If the comparison fails, the fault could be in either direction. A read-side search wants a stimulus that exercises only the read direction.
The array imposes timing structure the search does not want. Activating a row, respecting tRCD, precharging, honouring tRP — all of Module 14's constraints apply, and they make back-to-back reads at a controlled cadence awkward.
Refresh interferes. A sweep takes many thousands of probes. Over that interval refresh must happen (Module 15), and a refresh landing mid-probe changes the timing of the burst under test.
So the stimulus needs to be a known pattern that comes from somewhere other than the array. DDR4 provides exactly that.
3. What Must Already Work
Before the first comparison can mean anything, two things must already be true, and both are other chapters' property.
The gate must admit the strobe. 19.4 established that the read strobe is only visible to the capture path while the gate is open, and that the gate's position is an estimate. If the gate is wrong, every comparison fails at every delay code — and the sweep returns an empty pass region, which is indistinguishable from a dead channel. That is why 21.3 runs first, and why this chapter's failure table has a row for it.
The burst must be framed correctly. 20.3's normaliser produces ordered beats; 19.2's assembler forms words. If either is misconfigured, the comparison fails for reasons that have nothing to do with the sampling point — and 20.3 §13 showed that a pairwise beat swap is perfectly deterministic, so it fails identically at every delay code, producing the same empty region.
4. The Multi-Purpose Register
DDR4 devices contain four 8-bit programmable registers — the Multi-Purpose Registers — used for DQ bit pattern storage. Once programmed they are activated by read commands to drive their contents onto the DQ bus during link training, instead of data from the array.
The relevant mode register fields, all in MR3:
| Field | Meaning |
|---|---|
| A2 | MPR Operation — 0 = normal, 1 = dataflow from/to MPR |
| A1:A0 | MPR page selection — 00 = Page 0, 01 = Page 1, 10 = Page 2, 11 = Page 3 |
| A12:A11 | MPR read format — 00 = Serial, 01 = Parallel, 10 = Staggered, 11 = Reserved |
Page 0 is the training pattern page. Its four locations have JEDEC-specified default values:
| Location | Default |
|---|---|
| MPR0 | 01010101 |
| MPR1 | 00110011 |
| MPR2 | 00001111 |
| MPR3 | 00000000 |
Read commands in MPR mode do not touch the array, so §2's four objections all disappear: the contents are known without writing them, only the read direction is exercised, no row needs activating, and the standard restricts the command set in this mode to a small list — MRS, RD, RDA, WR, WRA, DES, REF and Reset — with power-down and self-refresh disallowed. Auto-precharge is ignored on RDA because there is no row to close.
Two further details matter to a search:
MPR reads support BL8 and fixed BC4. Burst chop on the fly is not supported for MPR reads, so the burst length is a mode setting rather than a per-command choice.
Data Bus Inversion is not allowed during an MPR read. DBI would alter the pattern on the wire, which is exactly what a pattern-comparison search must not have happen.
5. The Read Format Decides What Can Be Trained
This is the part of the mechanism that repays careful reading, because the two useful formats train genuinely different things — and choosing the wrong one produces a search that cannot see the fault it is looking for.
Serial return sends the same pattern on every DQ lane. The 8-bit pattern is returned across the burst, so each lane sees the pattern's bit sequence in time, and every lane sees the identical sequence.
Parallel return sends the pattern across the lanes in the first unit interval, then repeats it for the remaining unit intervals of the burst. So each lane carries a constant value through the burst, and the lanes differ from one another.
The consequence is a clean division of labour:
SERIAL — pattern travels in TIME, lanes identical
DQ0 0 1 0 1 0 1 0 1 transitions per lane: yes
DQ1 0 1 0 1 0 1 0 1 lane distinguishable: NO
DQ2 0 1 0 1 0 1 0 1
└─ edges exist, so a sampling point can be located
PARALLEL — pattern travels across LANES, constant in time
DQ0 1 1 1 1 1 1 1 1 transitions per lane: NO
DQ1 0 0 0 0 0 0 0 0 lane distinguishable: yes
DQ2 1 1 1 1 1 1 1 1
└─ no edges, so no sampling point can be locatedA timing search needs transitions in time, so it needs serial format. A pattern that is constant across the burst has no edges for the sampling point to be early or late relative to; every delay code within the gate window would pass, and the sweep would report a pass region as wide as the domain. That is the read-training equivalent of 20.3 §12's silent failure — a search that returns a confident, meaningless answer.
A lane-mapping check needs the lanes to differ, so it needs parallel format. And the default patterns are built for it. Reading all four Page 0 locations in parallel format gives each lane a four-bit signature, and those signatures are distinct:
| Lane | MPR0 | MPR1 | MPR2 | MPR3 |
|---|---|---|---|---|
| DQ0 | 1 | 1 | 1 | 0 |
| DQ1 | 0 | 1 | 1 | 0 |
| DQ2 | 1 | 0 | 1 | 0 |
| DQ3 | 0 | 0 | 1 | 0 |
| DQ4 | 1 | 1 | 0 | 0 |
| DQ5 | 0 | 1 | 0 | 0 |
| DQ6 | 1 | 0 | 0 | 0 |
| DQ7 | 0 | 0 | 0 | 0 |
The first three columns are the lane index in binary, inverted, with MPR3 supplying a constant-zero reference. All eight signatures are distinct, so a single set of four parallel-format reads identifies every lane unambiguously — which is precisely the discriminator 20.3 §13 needed and did not have.
The serial-format transition counts across an 8-beat burst are also a graded set — 7, 3, 1 and 0 for MPR0 through MPR3 — so the same four registers offer four different toggle densities to sweep against. Whether a lower-density pattern passes where the densest one fails is a real and useful observation, though why it does is a channel property and therefore Module 22's.
6. Two Edges, Not One Transition
With a correctness observable, sweeping the read delay produces a region rather than a step.
delay code 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
result F F F P P P P P P P P F F F F F
└──────────┬──────────┘
pass region: codes 3..10
left edge 3, right edge 10, width 8
▲
centre: code 6 or 7The region has three properties the search must extract, and they are not equally trustworthy.
The left edge is the lowest passing code. The right edge is the highest passing code. The width is the count of passing codes, and it is the closest thing to a margin figure this search produces.
The selection policy is then the midpoint. And this is where the single most common read-training error lives.
An even-width region has no single middle code. Choosing the lower or the upper is a policy decision, and the only wrong choice is an inconsistent one: a search that rounds differently per lane introduces a per-lane offset it did not measure.
7. Why the Sweep Must Not Stop at the Right Edge
In 21.1 the search stopped the moment it found its transition. Here, stopping at the first failure after a run of passes is wrong, and the reason is worth stating plainly.
A pass region is not guaranteed to be contiguous. A sweep can look like this:
delay code 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
result F F P P F F F P P P P P P F F F
└──┬──┘ └────────┬────────┘
narrow region 2..3 wide region 7..12
▲ ▲
a search that stops the region that should
at code 4 commits actually be selected
centre 2 -- inside
a two-code islandA search that terminates at the first failing code after its first pass commits the centre of the first region it encountered, which here is a two-code island. The wide region is never seen.
So the read search has a structurally different stopping condition from 21.1: sweep the entire domain, record all regions, then select. That costs the full domain in probes every time, which is real boot time — and is the honest reason read training is one of the slower steps. Module 23 owns what that costs a system; the structural reason it cannot be avoided is here.
Having swept everything, the selection policy becomes explicit:
| Policy | Choice | Consequence |
|---|---|---|
| Widest region | The region with the most passing codes | The usual choice; most tolerance |
| First region | The lowest-code region | Fast, and wrong whenever islands exist |
| Require a minimum width | Reject regions narrower than MIN_WIDTH | Turns a marginal island into a failure rather than a commit |
The third is not an alternative to the first — it is a filter applied with it. A region two codes wide is not a usable operating region regardless of being the widest one found, and a search that commits it has produced a setting that will fail on the first temperature change. Requiring a minimum width is how “training does not create margin” gets enforced in code.
8. Per-Bit Adjustment
Chapter 20.5 §5 established, from published figures, that the per-pin window is wider than the per-device one — and that the difference quantifies per-bit skew, growing from roughly 0.03 to 0.08 UI as the data rate rises across the grades of the named datasheet.
That difference is exactly what per-bit adjustment recovers, and the mechanism follows directly from §6's region.
Run the sweep per lane rather than per byte. Each lane returns its own region:
code 0 1 2 3 4 5 6 7 8 9 10 11 12
DQ0 F F P P P P P P P F F F F centre 5
DQ1 F F F F P P P P P P P F F centre 7
DQ2 F F F P P P P P P P F F F centre 6
...
common F F F F P P P P P F F F F centre 6, width 5
└─ the intersection: what ONE setting for the whole
byte can achieve, narrower than any single laneWith one shared setting the byte lane is limited to the intersection of the per-lane regions, which is narrower than every individual lane's region. With a per-lane delay each lane sits at its own centre, and each keeps its own full width.
This is why per-bit deskew exists, and the size of the prize is the difference between the intersection's width and the individual widths — which is the code-space form of 20.5's per-pin versus per-device gap.
Two honest limits on that picture.
Per-bit adjustment requires per-bit hardware. A PHY with one delay per byte lane cannot do this, and the search must then deliberately compute the intersection rather than the per-lane centres. Which resources exist is an implementation fact (19.1 §5).
The strobe is still shared. Per-bit delays move the data relative to a common strobe. The strobe's own position is one setting for the lane, so per-bit deskew narrows the spread of data around the strobe; it does not give each lane an independent strobe.
9. The Search, in Sequence
Message seven carries the whole difficulty of the chapter. One bit comes back, and it is the logical AND of the gate being right, the burst being framed right, the lanes being mapped right, the pattern being driven, and the sample point being inside the window. Message ten is the structural answer to §7.
10. The Edge Finder
// ---------------------------------------------------------------------
// read_level_edge_finder -- two-edge search over a correctness
// observable, with full-domain sweep and region selection.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
// - any delay element or its granularity (§6's callout)
// - the capture path of 20.3 or the assembler of 19.2
// - the comparison: cmp_pass arrives already reduced
// - the MPR mechanism, the strobe, or the gate of 19.4
//
// The region this block finds is a region of CODES. Its width is a
// count of codes, not a duration.
// ---------------------------------------------------------------------
module read_level_edge_finder #(
parameter int DELAY_W = 6,
parameter int DELAY_MAX = 40,
// Reads compared per candidate. One passing burst is weak evidence;
// a candidate passes only if ALL repeats pass, which makes the
// recorded region the conservative one.
parameter int REPEATS = 4,
// A region narrower than this is NOT a usable operating region and
// is rejected (§7). POLICY: the right value depends on the delay
// resource's granularity, which this block cannot know.
parameter int MIN_WIDTH = 4,
// A COUNT of repeats up to REPEATS needs clog2(REPEATS+1).
parameter int REP_W = (REPEATS <= 1) ? 1 : $clog2(REPEATS + 1),
// A COUNT of codes up to DELAY_MAX+1 needs clog2(DELAY_MAX+2).
parameter int WID_W = $clog2(DELAY_MAX + 2)
) (
input logic clk,
input logic rst_n,
input logic start,
// ── To the capture path's delay resource, and to whatever issues
// the MPR reads. This block does not build either.
output logic [DELAY_W-1:0] delay_setting,
output logic read_req,
// ── The observable. ONE bit: did the returned burst match the
// expected pattern. Already reduced across beats and lanes.
input logic read_done,
input logic cmp_pass,
// ── THE COMMITTED RESULT.
output logic [DELAY_W-1:0] trained_delay,
output logic trained_valid,
output logic train_done,
output logic train_failed,
// ── Observability. The selected region is published so a checker
// can confirm the committed code is its midpoint, and so debug
// can see the width that was accepted.
output logic [DELAY_W-1:0] cand_delay,
output logic [DELAY_W-1:0] best_lo,
output logic [DELAY_W-1:0] best_hi,
output logic [WID_W-1:0] best_width,
// More than one pass region was seen in the sweep (§7). NOT an
// error -- it is a diagnostic, and a very informative one.
output logic fragmented,
output logic [2:0] state_out,
output logic err_done_while_busy,
output logic err_empty_region
);
initial begin
if (MIN_WIDTH < 1)
$fatal(1, "read_level_edge_finder: MIN_WIDTH must be at least 1");
if (DELAY_MAX > (2**DELAY_W - 1))
$fatal(1, "read_level_edge_finder: DELAY_MAX (%0d) exceeds DELAY_W", DELAY_MAX);
if (MIN_WIDTH > (DELAY_MAX + 1))
$fatal(1, "read_level_edge_finder: MIN_WIDTH exceeds the whole domain");
end
typedef enum logic [2:0] {
S_IDLE = 3'd0,
S_ARM = 3'd1,
S_READ = 3'd2,
S_CLASS = 3'd3,
S_STEP = 3'd4,
S_SELECT = 3'd5,
S_COMMIT = 3'd6,
S_FAIL = 3'd7
} state_e;
state_e state, next;
logic [DELAY_W-1:0] cand;
logic [REP_W-1:0] reps; // repeats completed at this candidate
logic all_passed; // every repeat so far passed
// Current run of consecutive passing codes.
logic in_run;
logic [DELAY_W-1:0] run_lo;
logic [WID_W-1:0] run_width;
// Best run seen so far.
logic [DELAY_W-1:0] b_lo, b_hi;
logic [WID_W-1:0] b_width;
logic seen_a_run; // at least one run has closed or opened
logic frag;
logic [DELAY_W-1:0] committed;
logic committed_ok;
// This candidate's classification: it passes only if every repeat did.
logic this_pass;
assign this_pass = all_passed;
logic domain_done;
assign domain_done = (cand >= DELAY_W'(DELAY_MAX));
// A run that is at least as wide as the best so far replaces it. The
// comparison is >= so that a later, equally wide region wins, which
// biases selection toward higher codes; either bias is defensible,
// but it must be CONSISTENT across lanes (§6).
logic run_is_best;
assign run_is_best = in_run && (run_width >= b_width);
// The width the SELECT decision must use. A run still open at the
// top of the domain is folded into b_width by S_SELECT's sequential
// block -- but that assignment lands on the edge LEAVING S_SELECT,
// while next-state logic evaluates during it. Deciding on b_width
// alone would therefore use the pre-fold value and could FAIL a
// domain-topping region that is in fact wide enough.
logic [WID_W-1:0] eff_width;
assign eff_width = run_is_best ? run_width : b_width;
always_comb begin
next = state;
unique case (state)
S_IDLE : if (start) next = S_ARM;
S_ARM : next = S_READ;
S_READ : if (read_done)
next = (reps + REP_W'(1) >= REP_W'(REPEATS)) ? S_CLASS : S_READ;
// NOTE: S_CLASS never exits to S_COMMIT. Unlike 21.1, finding a
// passing code is not a stopping condition -- the sweep must
// reach the end of the domain before anything is selected (§7).
S_CLASS : next = domain_done ? S_SELECT : S_STEP;
S_STEP : next = S_ARM;
S_SELECT : next = (eff_width >= WID_W'(MIN_WIDTH)) ? S_COMMIT : S_FAIL;
S_COMMIT : next = S_IDLE;
S_FAIL : next = S_IDLE;
default : next = S_IDLE;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
cand <= '0;
reps <= '0;
all_passed <= 1'b1;
in_run <= 1'b0;
run_lo <= '0;
run_width <= '0;
b_lo <= '0;
b_hi <= '0;
b_width <= '0;
seen_a_run <= 1'b0;
frag <= 1'b0;
// THE SAFE STATE: code zero, not the last candidate swept.
committed <= '0;
committed_ok <= 1'b0;
end else begin
state <= next;
unique case (state)
S_IDLE : if (start) begin
cand <= '0;
reps <= '0;
all_passed <= 1'b1;
in_run <= 1'b0;
run_lo <= '0;
run_width <= '0;
b_lo <= '0;
b_hi <= '0;
b_width <= '0;
seen_a_run <= 1'b0;
frag <= 1'b0;
committed_ok <= 1'b0;
end
S_READ : if (read_done) begin
reps <= reps + REP_W'(1);
// A single failing repeat fails the candidate. all_passed is
// cleared and never re-set within this candidate.
if (!cmp_pass) all_passed <= 1'b0;
end
S_CLASS : begin
if (this_pass) begin
if (!in_run) begin
// A run is opening. If one has already closed, the
// region is fragmented.
in_run <= 1'b1;
run_lo <= cand;
run_width <= WID_W'(1);
if (seen_a_run) frag <= 1'b1;
seen_a_run <= 1'b1;
end else begin
run_width <= run_width + WID_W'(1);
end
end else begin
// A run is closing. Commit it to "best" if it wins.
if (in_run) begin
if (run_is_best) begin
b_lo <= run_lo;
b_hi <= cand - DELAY_W'(1);
b_width <= run_width;
end
in_run <= 1'b0;
run_width <= '0;
end
end
reps <= '0;
all_passed <= 1'b1;
end
S_STEP : cand <= cand + DELAY_W'(1);
S_SELECT : begin
// A run still open at the top of the domain has never been
// closed, so it must be folded in here. Omitting this is a
// real bug: it loses the region whenever the domain's top
// code passes.
if (in_run && run_is_best) begin
b_lo <= run_lo;
b_hi <= DELAY_W'(DELAY_MAX);
b_width <= run_width;
end
end
S_COMMIT : begin
// The midpoint of the selected region, in CODE space. The
// truncating divide biases to the lower code on an even
// width; §6 requires only that the bias be consistent.
committed <= b_lo + DELAY_W'((b_hi - b_lo) / 2);
committed_ok <= 1'b1;
end
// S_FAIL writes nothing: the safe state is held.
default : ;
endcase
end
end
assign cand_delay = cand;
assign delay_setting = cand;
assign read_req = (state == S_READ) && !read_done;
assign trained_delay = committed;
assign trained_valid = committed_ok;
assign train_done = (state == S_COMMIT) || (state == S_FAIL);
assign train_failed = (state == S_FAIL);
assign best_lo = b_lo;
assign best_hi = b_hi;
assign best_width = b_width;
assign fragmented = frag;
assign state_out = state;
assign err_done_while_busy = train_done && (reps != '0);
// No code passed anywhere in the domain -- §3's six causes.
assign err_empty_region = (state == S_FAIL) && (b_width == '0);
endmoduleThe S_SELECT fold-in is the bug most likely to survive a casual review. A region that is still open when the sweep reaches the top of the domain has never hit the closing branch in S_CLASS, so without that block it is silently discarded — and the symptom is that read training fails only on the parts whose working region happens to sit at the top of the delay range.
The same hazard has a second, subtler form, which is why eff_width exists. S_SELECT folds the open run into b_width in its sequential block, so that assignment lands on the clock edge leaving the state — while the next-state logic that chooses between S_COMMIT and S_FAIL evaluates during it. A decision made on b_width alone would therefore read the pre-fold value and could reject a domain-topping region that is in fact wide enough. Folding the open run in and then deciding on the stale width is a fix that looks complete and is not.
11. A Sweep, Cycle by Cycle
A full sweep: two edges found, a centre committed
10 cyclesThe best_hi row is the one to read carefully: the right edge becomes known only when a code fails. An edge is detected by the transition out of the region, which is why a sweep that ends while still passing must fold its open run in explicitly — the S_SELECT block of §10.
12. What the Assertions Prove
// Bind unit note: these properties reference read_level_edge_finder's
// internal state and are written as though bound into the module, so
// clk and rst_n are visible directly.
// P1 -- the committed value is the midpoint of the published region.
// This is the selection policy of §6, stated as a checkable fact.
property p_commit_is_midpoint;
@(posedge clk) disable iff (!rst_n)
$rose(trained_valid)
|-> (trained_delay == best_lo + DELAY_W'((best_hi - best_lo) / 2));
endproperty
assert property (p_commit_is_midpoint);
// P2 -- the committed code is inside the region it came from. Weaker
// than P1 and worth keeping separately: it survives a change of
// rounding policy, so it does not need editing when policy changes.
property p_commit_inside_region;
@(posedge clk) disable iff (!rst_n)
trained_valid |-> ((trained_delay >= best_lo) && (trained_delay <= best_hi));
endproperty
assert property (p_commit_inside_region);
// P3 -- the minimum width rule was actually applied (§7).
property p_width_respected;
@(posedge clk) disable iff (!rst_n)
trained_valid |-> (best_width >= WID_W'(MIN_WIDTH));
endproperty
assert property (p_width_respected);
// P4 -- the candidate/committed separation of 21.1 §7, unchanged.
property p_commit_only_on_commit;
@(posedge clk) disable iff (!rst_n)
(trained_delay != $past(trained_delay, 1))
|-> ($past(state_out, 1) == 3'd6);
endproperty
assert property (p_commit_only_on_commit);
// P5 -- THE structural property of this chapter. Selection cannot
// begin until the sweep has reached the top of the domain. A search
// that short-circuited on the first region would violate this.
property p_no_select_before_domain_end;
@(posedge clk) disable iff (!rst_n)
(state_out == 3'd5) |-> ($past(cand_delay, 1) == DELAY_W'(DELAY_MAX));
endproperty
assert property (p_no_select_before_domain_end);
// P6 -- a candidate is classified as passing only if every repeat
// passed. The conservative reduction of §10, checkable.
property p_all_repeats_required;
@(posedge clk) disable iff (!rst_n)
((state_out == 3'd3) && in_run && ($past(state_out, 1) == 3'd2))
|-> $past(all_passed, 1);
endproperty
assert property (p_all_repeats_required);
// P7 -- the best width never shrinks during a sweep. A selection that
// could regress would mean a narrower region displacing a wider one.
property p_best_width_monotonic;
@(posedge clk) disable iff (!rst_n)
(state_out != 3'd0) |-> (best_width >= $past(best_width, 1));
endproperty
assert property (p_best_width_monotonic);
// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) $rose(trained_valid));
cover property (@(posedge clk) disable iff (!rst_n) err_empty_region);
// §7's island case was actually exercised.
cover property (@(posedge clk) disable iff (!rst_n) fragmented && trained_valid);
// A region open at the top of the domain -- the S_SELECT fold-in.
cover property (@(posedge clk) disable iff (!rst_n)
(state_out == 3'd5) && in_run);
// A region found but REJECTED for width: the §7 filter doing its job.
cover property (@(posedge clk) disable iff (!rst_n)
train_failed && (best_width != '0));
// The whole domain passed -- a suspicious result, per §5.
cover property (@(posedge clk) disable iff (!rst_n)
(state_out == 3'd5) && (best_width == WID_W'(DELAY_MAX + 1)));P5 is this chapter's signature property, and it is the one that distinguishes a read search from a write-levelling search at the assertion level. P3 is the one that encodes the module's central qualification: a setting is committed only when a region of defensible width was found.
The last cover is worth having even though a full-domain pass is not an error. Per §5 it is the signature of a stimulus with no transitions — a parallel-format pattern used for a timing search — and a regression that hits it has found a real configuration mistake.
13. Corner Cases
| Case | Behaviour | Why |
|---|---|---|
| No code passes anywhere | train_failed, err_empty_region | §3's six causes, indistinguishable here |
| Every code passes | Commits the domain midpoint; the full-domain cover fires | Legal, but §5 says suspect the stimulus first |
Region open at DELAY_MAX | Folded in at S_SELECT | Otherwise silently discarded — §10 |
Region of width 1 with MIN_WIDTH = 4 | train_failed, best_width non-zero | A one-code region is not an operating point |
| Two equal-width regions | The higher-code one wins (>=) | Either bias is fine; consistency across lanes is not optional |
cmp_pass glitches high once in a failing run | Candidate still fails | all_passed clears on any failing repeat and never re-sets |
read_done with no request | Not counted | S_READ gates on its own request |
start mid-sweep | Ignored outside S_IDLE | Would merge two sweeps' regions |
MIN_WIDTH larger than the domain | $fatal at elaboration | No region could ever satisfy it, so every search would fail |
| Re-run after success | Previous region and commit discarded | The window may have moved — 19.5 §4 |
The second row deserves the emphasis it gets. A search that passes everywhere feels like the best possible outcome and is usually a sign that the observable is not sensitive to the variable being swept.
14. DV — Model the Channel, Check the Selection
The checker must not re-implement the sweep. 20.5 §14 already covers sweeping a window model and checking the shape of its classification; the job here is different — model a device and channel that has a hidden pass region, and verify the engine's selection against it.
// Independent channel model: knows the true region; the engine does not.
// SIMULATION-ONLY -- contains randomisation.
class rl_channel_model;
int unsigned lo, hi; // the true pass region, hidden
int unsigned island_lo, island_hi; // an optional second region
bit has_island;
function bit compare(int unsigned code);
if (code >= lo && code <= hi) return 1'b1;
if (has_island && code >= island_lo && code <= island_hi) return 1'b1;
return 1'b0;
endfunction
endclass| Check | What it establishes |
|---|---|
trained_valid implies best_lo == lo and best_hi == hi | The edges found are the real edges, not off by one |
Committed code is within one of (lo+hi)/2 | The selection policy landed on the true midpoint |
With has_island and a narrow island below lo, the wide region is still selected | §7's structural requirement, which a first-region search fails |
With has_island and an island wider than the main region, the island is selected | Confirms “widest” and not “first” or “lowest” |
Set hi = DELAY_MAX; expect the region still found | The S_SELECT fold-in of §10 |
Set lo = hi; expect train_failed with MIN_WIDTH > 1 | The width filter, not a commit |
Set the region empty; expect err_empty_region | §3 |
| Per-lane models with different regions; expect the intersection when one shared delay is used | §8's intersection claim, independently computed |
The third and fourth checks together are what pin the selection policy down. A search that stops early passes the first two checks and fails the third; a search that selects the lowest region passes the third and fails the fourth.
Here is the failure the third check catches, written out:
READ LEVELLING SELECTION MISMATCH
domain 0..15, MIN_WIDTH = 4, REPEATS = 4
model truth : main region 7..12 (width 6)
island 2..3 (width 2)
sweep observed by the engine
code 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
pass F F P P F F F P P P P P P F F F
engine under test (stops at first failure after a pass):
best_lo 2 best_hi 3 best_width 2 committed --
result train_failed, err_empty_region = 0
reason width 2 < MIN_WIDTH 4
engine as specified (full-domain sweep):
best_lo 7 best_hi 12 best_width 6 committed 9
fragmented = 1
diagnosis : the early-exit engine never observed codes 4..15. It
reported a FAILURE on a channel with a perfectly good
six-code region.
why it is easy to miss : the early-exit engine is not wrong about
what it saw. Its region really is two codes wide. The
bug is the stopping condition, not the arithmetic.
caught by : P5 -- selection began with cand_delay at 3, not at
DELAY_MAX. The width filter then turned a wrong search
into a clean-looking failure, which is the most
misleading possible outcome.That last note is the reason P5 exists as a separate property. The width filter of §7 is a safety feature, and here it masks a search bug by converting it into a plausible failure report. Without P5, the symptom in the lab is “read training fails on this board” — with no indication that the search never looked.
15. Debugging
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Empty region at every lane | Gate misplaced — §3 | 21.3 has not run or failed; check gate_trained |
| Empty region, gate known good | Burst misassembled — 20.3 §13 | Use parallel format and the §5 lane signatures |
| Region found but very narrow everywhere | Channel, not search | Module 22; compare against 20.5's published width |
| Whole domain passes | Stimulus has no transitions — §5 | Confirm MR3 A12:A11 is serial, not parallel |
| Region differs a lot between lanes | Expected — per-bit skew, 20.5 §5 | Per-lane sweep; consider per-bit delays (§8) |
| Centre differs by one between otherwise identical lanes | Even-width rounding — §6 | Check the rounding is consistent, not per-lane |
| Training passes, traffic fails later | Region was real at train time and has drifted | Periodic retraining — 19.5 §4 |
fragmented set with a healthy wide region | Genuinely informative, not an error | Log it; a newly fragmented sweep is a channel change |
| Passes with MPR, fails on array reads | The array path adds what MPR skipped | Row timing, refresh, or the write path that placed the data |
| Region moves between consecutive sweeps | REPEATS too low, or a marginal channel | Raise REPEATS; a stable region should repeat exactly |
The last row is the read-side analogue of 21.1 §5's boot-to-boot variance, and it has the same fix: the observable is a sample, so take more of them. The difference is that here a single passing burst is weaker evidence than a single write-levelling probe, because it asserts a conjunction.
The ninth row is worth remembering because it looks alarming and is often benign: MPR deliberately bypasses the array, so a path that works with MPR and fails on real reads has isolated the fault to what MPR skipped.
16. Misconceptions
“Read levelling measures the data eye.” It finds a set of codes at which one pattern compared equal. The eye is a physical property; this is a comparison result. 20.5 owns the published window and is explicit that the two are not the same object.
“The region's width is a margin in time.” It is a count of codes, and codes have no defined size (§6).
“The midpoint of the region is the centre of the eye.” Only under an assumption of linear, uniform delay steps that the hardware does not promise. What the midpoint maximises is code-space tolerance.
“You can stop once data starts passing.” §7. That commits the first region, which may be a two-code island.
“You can stop at the first failure after passing.” Same error, one step later. The region may not be contiguous, and the wide region may be further up the domain.
“Any known pattern works as a stimulus.” §5. A pattern that is constant across the burst has no edges, so a timing sweep against it passes everywhere and means nothing.
“MPR reads exercise the same path as normal reads.” They deliberately do not — no row activation, no array. That is the point, and it is also why passing MPR training does not guarantee array reads work.
“A pass tells you the sample point is good.” A pass tells you the whole path worked once, for this pattern. It is a conjunction, and it localises nothing (§1).
“Per-bit deskew gives each lane its own strobe.” §8. It moves the data relative to a strobe that remains shared across the lane.
“A wide region proves the channel is healthy.” It is good evidence at one temperature and voltage, with one pattern. It is not a guarantee, which is why retraining exists.
17. Interview Reasoning
Why does the read side need two edges when write levelling needed one transition? Because the observables differ in kind. Write levelling's bit reports a side, so one crossing locates the boundary. A correctness bit reports only that everything worked, so the working set has to be bounded from both directions before a position inside it can be chosen.
Why read from the MPR instead of from memory? Four reasons: the array contents are unknown before the write path is trained, a write-then-read conflates both directions, row timing and refresh interfere with a long sweep, and the MPR gives a pattern whose value is known without writing it.
You sweep the read delay and every code passes. Good news? Almost certainly a broken experiment. The most likely cause is a stimulus with no transitions across the burst — parallel format where serial was needed — so the comparison is insensitive to the variable being swept.
Why must the sweep cover the whole domain? Because the pass region may be fragmented. Stopping at the first failure after a pass commits the centre of whichever region came first, which can be a narrow island.
What does the region's width actually tell you? How many delay codes could be lost to drift before the path stops working. It is the best margin proxy the search produces, and it is in code units, not time.
Two lanes have regions 3–9 and 6–12. With one shared delay, where do you sit? In the intersection, 6–9, centre 7 or 8 — noticeably narrower than either lane alone. That gap is the argument for per-bit delays, and its size is the code-space form of the per-pin versus per-device difference in the published figures.
Read training passes and real traffic fails. First hypothesis? That the trained region was genuine and conditions have changed — temperature or voltage drift since bring-up. Second hypothesis: the array path adds something MPR skipped. Both are checkable, and the first is the reason periodic recalibration exists.
Why is an empty pass region a bad diagnostic? Because six unrelated faults produce it identically: gate, framing, lane mapping, stimulus, channel, and domain range. The observable is a conjunction, so its failure carries no localisation at all.
18. Exercises
-
Remove the
S_SELECTfold-in from §10's RTL and run §14's fifth check. Which assertion fires, and which does not? Explain why the failure is invisible for any channel whose region does not touchDELAY_MAX. -
The engine treats a candidate as passing only if all
REPEATSreads pass. Write the alternative where a majority suffices, and describe a channel on which the two policies commit different centres. Which policy would you ship, and what does the choice depend on? -
run_is_bestuses>=, so a later equal-width region wins. Change it to>and identify the specific per-lane hazard §6 warns about that this creates in a per-bit deskew flow. -
Using the §5 signature table, write the comparison logic that detects a swap of DQ2 and DQ5 from four parallel-format reads. Then explain why a serial-format sweep cannot detect that swap at any delay code.
-
Construct a hidden region layout on which the specified engine commits a worse centre than a first-region engine would. What does its existence say about calling the widest-region policy optimal?
-
MIN_WIDTHis a policy parameter this block cannot derive. Describe what you would need to measure — and on what hardware — to choose it defensibly. Relate your answer to 21.1 §11's argument aboutCOMMIT_OFFSET. -
Add a
region_countoutput and the property that it equals one whenfragmentedis low. Then argue whetherfragmentedshould ever block a commit, given §7's claim that it is a diagnostic rather than an error. -
The chapter says an empty region has six causes. Design the smallest set of additional experiments that would separate them, given only the observables available in this chapter plus those of 19.4 and 20.3. Which causes remain indistinguishable, and why?
19. Where This Goes
The read search is built, and it cost more than the write-levelling search in every dimension: a full-domain sweep instead of an early exit, two edges instead of one transition, a width filter, a fragmentation diagnostic, and a stimulus mechanism with a format choice that decides what the search can see at all.
All of it followed from one substitution — a correctness bit in place of a phase report.
Two things this chapter assumed are still unearned. It assumed the gate already admits the strobe, and §3 was explicit that a misplaced gate produces an empty region indistinguishable from a dead channel. Chapter 21.3 searches for that gate position — and does it with a third kind of observable, because the question “did the strobe arrive in my window” is not a correctness comparison and not a phase report. It is also the one search in this module that the standard does not define, which turns out to change how it must be built.
It also assumed a fixed reference level at the receiver. 21.4 removes that assumption and adds a second search axis, at which point the midpoint arithmetic of §6 stops being adequate.
Continue learning
Related tutorials
- Related topic
Timing Alignment
A write command and its data leave the PHY on different paths and must arrive in a defined relationship. The device specifies the relationship; the PHY absorbs the difference between the paths.
- Related topic
Write Leveling Algorithm
Chapter 20.4 built a probe that asks the device one yes-or-no question. Turning it into a search means sweeping a delay, distrusting any single answer near the boundary, confirming a transition, and keeping the candidate out of the production datapath.
- Related topic
Gate Training
The DQS gate exists only inside the PHY, so JEDEC defines no procedure for it — only a preamble the PHY can find. The resulting search has a one-sided pass region whose safe side produces no observable at all.
- Related topic
Eye Centering
A second axis turns a line search into a grid search, the probe count into a product, and the centre into something undefined. The grid is a measured proxy for an eye — and JEDEC says of its own receiver mask that it is not the eye.
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.
