DDR · Module 21
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.
Every search so far has had one axis. 21.1 swept a strobe delay and found a transition. 21.2 swept a sampling delay and found two edges. 21.3 swept a gate position and found a one-sided boundary.
This chapter adds a second axis: the reference level against which a receiver decides whether an arriving signal is a one or a zero. The search space stops being a line and becomes a grid, and almost nothing from the previous three chapters generalises cleanly.
The central law, and its qualification, for the last time before 21.5 assembles them all:
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 grid with no passing cell must fail.
1. Which Reference Level, and On Which Side
Before any search, a distinction that causes a great deal of confusion. There are two reference levels in a DDR link, they belong to opposite ends, and they serve opposite directions of traffic.
WRITE direction: controller drives, DEVICE receives
controller TX ───────────────────────────► DRAM RX
│
compares against VrefDQ
INSIDE THE DRAM.
JEDEC-specified, MR6.
READ direction: device drives, CONTROLLER receives
DRAM TX ◄─────────────────────────────── controller RX
│ │
│ compares against a reference
│ INSIDE THE PHY.
│ Not JEDEC-specified at all.So DDR4's VrefDQ training trains the DRAM's own receiver, and therefore affects writes. It has nothing to do with how the controller samples read data. The controller's read-side reference is internal to the PHY, is as technology-specific as its delay elements (19.1 §5), and has no standard register interface.
JEDEC's own language confirms the side: the specification describes a DQ Receiver (Rx) compliance mask and defines each DQ pin's Vref level as the centre — that is, the widest opening — of the cumulative data input eye. An input eye, at a receiver.
2. What the Standard Specifies for the Device Side
The device-side level is programmed through MR6, whose fields are:
| Field | Meaning |
|---|---|
| A7 | VrefDQ Training Enable — 0 = normal operation, 1 = training mode |
| A6 | VrefDQ Training Range — 0 = Range 1, 1 = Range 2 |
| A5:A0 | VrefDQ Training Value — the code |
The code table is published, and it is exactly linear in the code:
| Base | Step | Codes | Span | |
|---|---|---|---|---|
| Range 1 | 60.00% of VDDQ | 0.65% | 000000–110010 (0–50) | 60.00% – 92.50% |
| Range 2 | 45.00% of VDDQ | 0.65% | 000000–110010 (0–50) | 45.00% – 77.50% |
Codes 110011 through 111111 are reserved in both ranges. So there are 51 usable codes per range, and the published value for code k is base + k × 0.65%.
3. The Two Ranges Do Not Overlap Where It Matters
The two ranges look like they overlap. Range 1 starts at 60.00% and Range 2 reaches 77.50%, so their spans share the region from 60.00% to 77.50% — 27 of Range 1's codes lie inside Range 2's span.
But the offset between the ranges is 15.00%, and the step is 0.65%:
15.00 / 0.65 = 23.0769... not an integerSo no voltage is reachable in both ranges. Not one. The grids interleave instead of coinciding:
| Code | Published | |
|---|---|---|
| Range 2 | 23 | 59.95% |
| Range 1 | 0 | 60.00% |
| Range 2 | 24 | 60.60% |
| Range 1 | 1 | 60.65% |
The closest cross-range pair is 0.05% apart. Read naively, that suggests a clever trick: use both ranges together and get an effective step of 0.05% instead of 0.65% — a thirteen-fold improvement in resolution.
That trick does not work, and understanding why is the most useful thing in this chapter.
4. The Code Step Is Not the Voltage Step
Here is where JEDEC's two statements about Vref have to be read together, because separately each is easy to misuse.
Statement one — the code table, from §2: the published value for code k is base + k × 0.65%, exact to the hundredth of a percent.
Statement two — the device specification, from the standard's DQ Vref Training section: the Vref step size ranges from 0.5% VDDQ to 0.8% VDDQ, and for a given design the DRAM has one value for step size that falls within that range.
Those are not the same number, and they are not in conflict — they describe different things. The first is the nominal mapping the table defines. The second is the physical step the silicon actually implements, which is a design property the standard constrains only to a band.
The consequence is immediate:
50 codes of travel, according to the table : 32.50% of VDDQ
50 codes of travel, if the design's step is
0.5% -> 25.00% of VDDQ
0.65% -> 32.50% of VDDQ (only here does the table hold)
0.8% -> 40.00% of VDDQSo a sweep of the whole code range covers somewhere between 25% and 40% of VDDQ, and the algorithm cannot know which. The Vref axis has an unknown scale — exactly like the delay axis, whose codes 21.1 §4 established have no defined size.
The standard adds one more thing that finishes off the 0.05% idea. It defines a Vref set tolerance: the variation of the actual Vref from its ideal setting, which accounts for accumulated error over multiple steps, and whose uncertainty is a function of the number of steps taken. So the code-to-voltage map is not merely of unknown slope — it is not exactly linear either, and the deviation grows with distance travelled.
5. The Search Space Is a Grid
With two axes, sweeping produces a two-dimensional pass/fail map. Conventionally it is drawn as a schmoo:
sampling delay code
0 2 4 6 8 10 12 14 16
┌─────────────────────────────────────
32 │ F F F F F F F F F
30 │ F F F P P P F F F
28 │ F F P P P P P F F
V 26 │ F P P P P P P P F
r 24 │ F P P P P P P P F
e 22 │ F P P P P P P P F
f 20 │ F F P P P P P F F
18 │ F F F P P P F F F
16 │ F F F F F F F F F
└─────────────────────────────────────
PASS = the comparison succeeded at this (delay, Vref) pair.
The passing cells form a region. Its shape is informative.
Its AREA is not a number with units.Three things about this picture are true and worth separating from three things that are not.
True: every cell is a measurement — a pattern was compared at that pair of codes and it passed or failed. The region's shape carries information: a region that is tall and narrow says the timing axis is the binding constraint, a wide flat one says the level axis is. A region that is asymmetric says something is loading one side more than the other, which is a real observation even though its cause is Module 22's.
Also true: the region is bounded by the codes actually swept, so a region touching the edge of the grid is a region whose extent is unknown — the same trap as 21.3 §12's whole-domain pass.
6. Cost Grows as a Product
The practical consequence of a second axis is arithmetic and unforgiving.
A one-axis search over D delay codes with R repeats costs D × R probes. Add V level codes and a full grid costs D × V × R.
With Range 1's 51 usable codes and, say, 40 delay codes and 4 repeats, a full grid is 51 × 40 × 4 — over eight thousand probe sequences, per lane, per rank, per direction. That is the reason real training flows do not sweep full grids.
The standard alternative is coordinate descent: sweep one axis with the other fixed, take the best value, then sweep the other. Cost falls from a product to a sum — (D + V) × R — which for the numbers above is a reduction of more than an order of magnitude.
FULL GRID COORDINATE DESCENT
every cell probed one row, then one column
F F F F F F F . . . . . . .
F F P P P F F . . . . . . .
F P P P P P F F P P P P P F <- sweep Vref fixed
F P P P P P F . . . P . . . then delay at
F P P P P P F . . . P . . . the chosen Vref
F F P P P F F . . . P . . .
F F F F F F F . . . . . . .
cost D x V x R cost (D + V) x R
sees the whole region sees a cross through itWhat coordinate descent buys in probes it pays for in generality, and the price is specific:
It finds the centre of a cross, not the centre of a region. If the passing region is a tilted or irregular shape, the row through an arbitrary starting Vref may not cross it at its widest point, and the resulting centre can sit well off the true middle.
It can stall. If the starting point on one axis lands entirely outside the region, the first sweep finds nothing and there is no gradient to follow — the same non-observation problem 21.3 §5 described for coarse-before-fine.
Repeating it helps, and is not guaranteed to converge to the best cell. Alternating the two sweeps usually improves the estimate, which is why real flows iterate. For a convex region a few passes are enough; regions in this problem are usually convex enough in practice, and that is an empirical statement rather than a guarantee.
7. Selecting a Centre in Two Dimensions
Chapter 21.2 §6 selected the midpoint of a one-dimensional region and warned that the midpoint in code space is not the centre of the window. In two dimensions that warning gets worse, because there is no longer a single obvious candidate for “middle.”
| Policy | Definition | Problem |
|---|---|---|
| Per-axis midpoint | Midpoint of the widest row, midpoint of the widest column | The two may not intersect inside the region |
| Largest inscribed rectangle | The biggest axis-aligned all-pass box; commit its centre | Depends on the axes' unknown relative scales (§5) |
| Maximum-margin cell | The passing cell furthest from any failing cell | Requires a distance, and there is no metric across two unscaled axes |
| Widest-row midpoint | Sweep the level axis, pick the row with the most passes, take its midpoint | Ignores level-axis margin entirely |
Every row in that table has the same root problem: each policy needs to compare a distance along one axis with a distance along the other, and no such comparison is defined. Moving three delay codes and moving three Vref codes are not commensurable quantities.
There are only two honest ways out.
Choose a policy that never compares the axes. Per-axis midpoints do this: each axis's centre is computed only from that axis's own codes. That is what the RTL in §9 does, and it is why coordinate descent and per-axis selection fit together naturally.
Supply the conversion from outside. If the integrator knows the delay step in picoseconds and the Vref step in millivolts, a weighted metric becomes meaningful — and it becomes the integrator's number, carried as a parameter, not something the search derived.
8. The Search Architecture
Two facts the diagram cannot carry are worth restating against it. Neither of the two resources in the top row has a known physical scale — the delay codes have no defined size and the level codes map to voltage through the 0.5%–0.8% band of §4 — so distances along the two axes cannot be compared, which is what §7's per-axis policy is forced by. And the single bit returned by the compare stage is a conjunction (21.2 §1): it says the whole path worked, so a failing cell localises nothing.
The structural point is the separation between the candidate pair in row two and the committed trio in row four. It is the same discipline as 21.1 §7, and with two axes there is simply more to leak: a search that wrote either axis directly into its trained register would leave the interface configured with an experimental level, an experimental delay, or a mismatched pair of one committed and one experimental value — which is the worst of the three, because each half looks defensible alone.
9. The Two-Axis Search
// ---------------------------------------------------------------------
// eye_center_search -- two-axis centring by coordinate descent.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
// - any delay element or reference-level generator
// - volts, VDDQ, or the code-to-voltage map of §4 -- the Vref axis
// here is an INDEX, and the standard specifies the physical step
// it maps to only as a 0.5%..0.8% band
// - Vref set tolerance, which accumulates over steps (§4)
// - the comparison, the stimulus, or the link
//
// Selection is PER AXIS and never compares the two axes, because no
// conversion between them exists inside this block (§7).
// ---------------------------------------------------------------------
module eye_center_search #(
parameter int VREF_W = 6,
// JESD79-4 usable VrefDQ codes are 0..50; 51..63 are reserved. The
// default reflects that, and the guard below enforces it.
parameter int VREF_MAX = 50,
parameter int DLY_W = 6,
parameter int DLY_MAX = 40,
parameter int REPEATS = 4,
// Minimum acceptable passing width, PER AXIS. Two separate values,
// because the axes are not commensurable (§7) -- a single shared
// threshold would be asserting a conversion that does not exist.
parameter int MIN_VREF_WIDTH = 4,
parameter int MIN_DLY_WIDTH = 4,
// Coordinate-descent passes. One pass is a Vref sweep then a delay
// sweep; more passes refine the estimate (§6).
parameter int PASSES = 2,
parameter int REP_W = (REPEATS <= 1) ? 1 : $clog2(REPEATS + 1),
parameter int PAS_W = (PASSES <= 1) ? 1 : $clog2(PASSES + 1),
parameter int VW_W = $clog2(VREF_MAX + 2),
parameter int DW_W = $clog2(DLY_MAX + 2)
) (
input logic clk,
input logic rst_n,
input logic start,
// Starting point for the first sweep. Coordinate descent needs one,
// and a bad one can stall the search (§6) -- so it is an input, not
// a constant, and 21.5 supplies it from an earlier step's result.
input logic [VREF_W-1:0] seed_vref,
input logic [DLY_W-1:0] seed_delay,
// ── To the two configurable resources. EXPERIMENTAL values.
output logic [VREF_W-1:0] cand_vref,
output logic [DLY_W-1:0] cand_delay,
output logic probe_req,
// ── The observable: one bit, a conjunction (21.2 §1).
input logic probe_done,
input logic cmp_pass,
// ── THE COMMITTED PAIR. Both, or neither -- see §8.
output logic [VREF_W-1:0] trained_vref,
output logic [DLY_W-1:0] trained_delay,
output logic trained_valid,
output logic train_done,
output logic train_failed,
// ── Observability: the per-axis regions that produced the centres.
output logic [VREF_W-1:0] vref_lo, vref_hi,
output logic [DLY_W-1:0] dly_lo, dly_hi,
output logic [VW_W-1:0] vref_width,
output logic [DW_W-1:0] dly_width,
output logic sweeping_vref, // which axis is active
output logic [2:0] state_out,
output logic err_axis_empty,
output logic err_reserved_code
);
initial begin
// JESD79-4 reserves VrefDQ codes 51..63. Driving one is not a
// subtle error: the device's behaviour is undefined.
if (VREF_MAX > 50)
$fatal(1, "eye_center_search: VREF_MAX (%0d) enters the reserved range", VREF_MAX);
if (VREF_MAX > (2**VREF_W - 1))
$fatal(1, "eye_center_search: VREF_MAX does not fit VREF_W");
if (DLY_MAX > (2**DLY_W - 1))
$fatal(1, "eye_center_search: DLY_MAX does not fit DLY_W");
if (PASSES < 1)
$fatal(1, "eye_center_search: PASSES must be at least 1");
if (MIN_VREF_WIDTH < 1 || MIN_DLY_WIDTH < 1)
$fatal(1, "eye_center_search: per-axis minimum widths must be at least 1");
end
typedef enum logic [2:0] {
S_IDLE = 3'd0,
S_ARM = 3'd1,
S_PROBE = 3'd2,
S_CLASS = 3'd3,
S_STEP = 3'd4,
S_AXIS = 3'd5, // finish one axis: select its centre, swap axes
S_COMMIT = 3'd6,
S_FAIL = 3'd7
} state_e;
state_e state, next;
logic on_vref; // currently sweeping the Vref axis
logic [PAS_W-1:0] pass_num;
logic [REP_W-1:0] reps;
logic all_passed;
// The swept coordinate, and the fixed one.
logic [VREF_W-1:0] v_cur, v_fixed;
logic [DLY_W-1:0] d_cur, d_fixed;
// Run tracking for the axis being swept. Widths are kept per axis
// so a stalled axis cannot inherit the other's region.
logic in_run;
logic [VW_W-1:0] run_w;
logic [VREF_W-1:0] run_v_lo;
logic [DLY_W-1:0] run_d_lo;
logic [VREF_W-1:0] v_lo, v_hi;
logic [DLY_W-1:0] d_lo, d_hi;
logic [VW_W-1:0] v_w;
logic [DW_W-1:0] d_w;
logic [VREF_W-1:0] c_vref;
logic [DLY_W-1:0] c_delay;
logic c_ok;
logic this_pass;
assign this_pass = all_passed;
logic axis_done;
assign axis_done = on_vref ? (v_cur >= VREF_W'(VREF_MAX))
: (d_cur >= DLY_W'(DLY_MAX));
// Effective widths, including a run still open at the top of the
// axis being swept. S_AXIS folds that run into v_w / d_w in its
// SEQUENTIAL block, which lands on the edge leaving the state --
// while the next-state logic below evaluates during it. Deciding on
// v_w / d_w alone would use pre-fold values and could FAIL an
// axis whose region reaches its domain limit.
logic [VW_W-1:0] eff_v_w;
logic [DW_W-1:0] eff_d_w;
assign eff_v_w = (on_vref && in_run && (run_w >= v_w)) ? run_w : v_w;
assign eff_d_w = (!on_vref && in_run && (DW_W'(run_w) >= d_w)) ? DW_W'(run_w) : d_w;
// Both axes must satisfy their OWN minimum width. Checked separately,
// never combined into a single area or score (§7).
logic widths_ok;
assign widths_ok = (eff_v_w >= VW_W'(MIN_VREF_WIDTH)) && (eff_d_w >= DW_W'(MIN_DLY_WIDTH));
logic last_pass;
assign last_pass = (pass_num + PAS_W'(1) >= PAS_W'(PASSES));
always_comb begin
next = state;
unique case (state)
S_IDLE : if (start) next = S_ARM;
S_ARM : next = S_PROBE;
S_PROBE : if (probe_done)
next = (reps + REP_W'(1) >= REP_W'(REPEATS)) ? S_CLASS : S_PROBE;
// Like 21.2 and unlike 21.3, each axis is swept to its end: a
// per-axis region can be fragmented for the same reason.
S_CLASS : next = axis_done ? S_AXIS : S_STEP;
S_STEP : next = S_ARM;
S_AXIS : if (on_vref) next = S_ARM; // now sweep delay
else if (!last_pass) next = S_ARM; // another pass
else next = widths_ok ? 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;
on_vref <= 1'b1;
pass_num <= '0;
reps <= '0;
all_passed <= 1'b1;
v_cur <= '0; d_cur <= '0;
v_fixed <= '0; d_fixed <= '0;
in_run <= 1'b0; run_w <= '0;
run_v_lo <= '0; run_d_lo <= '0;
v_lo <= '0; v_hi <= '0; d_lo <= '0; d_hi <= '0;
v_w <= '0; d_w <= '0;
// SAFE STATE: neither axis committed, valid low. A mismatched
// pair would be worse than none, because each half looks sane.
c_vref <= '0; c_delay <= '0; c_ok <= 1'b0;
end else begin
state <= next;
unique case (state)
S_IDLE : if (start) begin
on_vref <= 1'b1; // Vref axis first
pass_num <= '0;
reps <= '0;
all_passed <= 1'b1;
v_cur <= '0; // sweep Vref from the bottom
d_cur <= seed_delay; // with delay held at the seed
v_fixed <= seed_vref;
d_fixed <= seed_delay;
in_run <= 1'b0; run_w <= '0;
v_w <= '0; d_w <= '0;
c_ok <= 1'b0;
end
S_PROBE : if (probe_done) begin
reps <= reps + REP_W'(1);
if (!cmp_pass) all_passed <= 1'b0;
end
S_CLASS : begin
if (this_pass) begin
if (!in_run) begin
in_run <= 1'b1; run_w <= VW_W'(1);
run_v_lo <= v_cur; run_d_lo <= d_cur;
end else begin
run_w <= run_w + VW_W'(1);
end
end else if (in_run) begin
// Close the run; keep it if it is the widest on this axis.
if (on_vref) begin
if (run_w >= v_w) begin
v_lo <= run_v_lo; v_hi <= v_cur - VREF_W'(1); v_w <= run_w;
end
end else begin
if (DW_W'(run_w) >= d_w) begin
d_lo <= run_d_lo; d_hi <= d_cur - DLY_W'(1); d_w <= DW_W'(run_w);
end
end
in_run <= 1'b0; run_w <= '0;
end
reps <= '0;
all_passed <= 1'b1;
end
S_STEP : if (on_vref) v_cur <= v_cur + VREF_W'(1);
else d_cur <= d_cur + DLY_W'(1);
S_AXIS : begin
// Fold in a run still open at the top of this axis -- the
// same omission that 21.2 §10 flagged, once per axis here.
if (in_run) begin
if (on_vref) begin
if (run_w >= v_w) begin
v_lo <= run_v_lo; v_hi <= VREF_W'(VREF_MAX); v_w <= run_w;
end
end else begin
if (DW_W'(run_w) >= d_w) begin
d_lo <= run_d_lo; d_hi <= DLY_W'(DLY_MAX); d_w <= DW_W'(run_w);
end
end
end
in_run <= 1'b0; run_w <= '0;
if (on_vref) begin
// Vref axis finished: fix Vref at its own midpoint and
// sweep delay next. PER-AXIS selection -- §7.
v_fixed <= v_lo + VREF_W'((v_hi - v_lo) / 2);
v_cur <= v_lo + VREF_W'((v_hi - v_lo) / 2);
d_cur <= '0;
on_vref <= 1'b0;
end else begin
d_fixed <= d_lo + DLY_W'((d_hi - d_lo) / 2);
d_cur <= d_lo + DLY_W'((d_hi - d_lo) / 2);
if (!last_pass) begin
// Another descent pass, restarting on the Vref axis from
// the delay centre just found.
pass_num <= pass_num + PAS_W'(1);
on_vref <= 1'b1;
v_cur <= '0;
v_w <= '0;
d_w <= '0;
end
end
end
S_COMMIT : begin
// BOTH axes commit together, from the per-axis midpoints.
c_vref <= v_lo + VREF_W'((v_hi - v_lo) / 2);
c_delay <= d_lo + DLY_W'((d_hi - d_lo) / 2);
c_ok <= 1'b1;
end
default : ;
endcase
end
end
assign cand_vref = on_vref ? v_cur : v_fixed;
assign cand_delay = on_vref ? d_fixed : d_cur;
assign probe_req = (state == S_PROBE) && !probe_done;
assign trained_vref = c_vref;
assign trained_delay = c_delay;
assign trained_valid = c_ok;
assign train_done = (state == S_COMMIT) || (state == S_FAIL);
assign train_failed = (state == S_FAIL);
assign vref_lo = v_lo; assign vref_hi = v_hi; assign vref_width = v_w;
assign dly_lo = d_lo; assign dly_hi = d_hi; assign dly_width = d_w;
assign sweeping_vref = on_vref;
assign state_out = state;
// An axis that produced no passing cell at all: §6's stall.
assign err_axis_empty = (state == S_FAIL) && ((v_w == '0) || (d_w == '0));
// A reserved VrefDQ code would reach the device. Should be
// impossible given the elaboration guard; published as a belt.
assign err_reserved_code = probe_req && (cand_vref > VREF_W'(50));
endmoduleThe S_AXIS state is where the chapter's argument lives. It selects each axis's centre from that axis's own codes only — never from a combined score — because §7 established that no conversion between the axes exists inside the block. And it commits both coordinates in a single state, because a half-committed pair is the one failure mode that looks reasonable from either side.
10. One Descent Pass, Cycle by Cycle
Coordinate descent: sweep Vref, fix it, sweep delay
10 cyclesThe two cand_ rows show coordinate descent directly: exactly one of them moves at a time, and the other holds. The trained_ rows stay dashed until the final cycle, which is the both-or-neither commit of §9 — and the reason to read them together rather than separately.
11. What the Assertions Prove
// Bind unit note: these properties reference eye_center_search's
// internal state and are written as though bound into the module, so
// clk and rst_n are visible directly.
// P1 -- exactly one axis moves at a time. This IS coordinate descent,
// stated as a checkable invariant.
property p_one_axis_at_a_time;
@(posedge clk) disable iff (!rst_n)
((cand_vref != $past(cand_vref, 1)) && (cand_delay != $past(cand_delay, 1)))
|-> (state_out == 3'd5);
endproperty
assert property (p_one_axis_at_a_time);
// P2 -- both coordinates commit together. A half-committed pair is
// the failure mode of §9, and this is the property that forbids it.
property p_both_or_neither;
@(posedge clk) disable iff (!rst_n)
$rose(trained_valid)
|-> ((trained_vref == vref_lo + VREF_W'((vref_hi - vref_lo) / 2))
&& (trained_delay == dly_lo + DLY_W'((dly_hi - dly_lo) / 2)));
endproperty
assert property (p_both_or_neither);
// P3 -- each axis's own minimum width was satisfied. Checked per axis,
// never as a combined score (§7).
property p_per_axis_width;
@(posedge clk) disable iff (!rst_n)
trained_valid
|-> ((vref_width >= VW_W'(MIN_VREF_WIDTH))
&& (dly_width >= DW_W'(MIN_DLY_WIDTH)));
endproperty
assert property (p_per_axis_width);
// P4 -- candidate/committed separation, as in every chapter so far.
property p_commit_only_on_commit;
@(posedge clk) disable iff (!rst_n)
((trained_vref != $past(trained_vref, 1))
|| (trained_delay != $past(trained_delay, 1)))
|-> ($past(state_out, 1) == 3'd6);
endproperty
assert property (p_commit_only_on_commit);
// P5 -- no reserved VrefDQ code is ever driven at the device.
// JESD79-4 reserves 51..63, and undefined device behaviour is not a
// failure the search could diagnose.
property p_no_reserved_vref;
@(posedge clk) disable iff (!rst_n)
probe_req |-> (cand_vref <= VREF_W'(50));
endproperty
assert property (p_no_reserved_vref);
// P6 -- the committed point lies inside both regions it came from.
// Weaker than P2 and survives a change of rounding policy.
property p_commit_inside_both;
@(posedge clk) disable iff (!rst_n)
trained_valid
|-> ((trained_vref >= vref_lo) && (trained_vref <= vref_hi)
&& (trained_delay >= dly_lo) && (trained_delay <= dly_hi));
endproperty
assert property (p_commit_inside_both);
// P7 -- an axis is only ever declared finished at the top of its own
// domain. The per-axis analogue of 21.2's P5.
property p_axis_swept_fully;
@(posedge clk) disable iff (!rst_n)
(state_out == 3'd5)
|-> ($past(sweeping_vref, 1)
? ($past(cand_vref, 1) == VREF_W'(VREF_MAX))
: ($past(cand_delay, 1) == DLY_W'(DLY_MAX)));
endproperty
assert property (p_axis_swept_fully);
// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) $rose(trained_valid));
cover property (@(posedge clk) disable iff (!rst_n) err_axis_empty);
// A second descent pass actually ran -- PASSES > 1 exercised.
cover property (@(posedge clk) disable iff (!rst_n)
(state_out == 3'd5) && !sweeping_vref && !$past(train_done, 1)
&& (pass_num != '0));
// One axis satisfied its width and the other did not: the case a
// combined area score would have wrongly accepted.
cover property (@(posedge clk) disable iff (!rst_n)
train_failed && (vref_width >= VW_W'(MIN_VREF_WIDTH))
&& (dly_width < DW_W'(MIN_DLY_WIDTH)));
// An axis region touching the top of its domain: extent unknown.
cover property (@(posedge clk) disable iff (!rst_n)
trained_valid && (dly_hi == DLY_W'(DLY_MAX)));
// The whole Vref axis passed -- suspect the observable, per 21.2 §5.
cover property (@(posedge clk) disable iff (!rst_n)
trained_valid && (vref_width == VW_W'(VREF_MAX + 1)));P1 and P2 are the two worth keeping if the list has to shrink. P1 is the definition of the search strategy; P2 forbids the half-commit.
The fourth cover is the one that earns its place by argument rather than by catching a common bug. It exercises precisely the situation in which a combined figure of merit — an area, a cell count, a weighted score — would have accepted an operating point that one axis rejects. Having it in a regression is how the §7 policy stays honest under later modification.
12. Corner Cases
| Case | Behaviour | Why |
|---|---|---|
| Seed lands outside the region | First axis finds nothing; err_axis_empty | §6's stall — coordinate descent needs a starting point inside or near |
| One axis wide, the other narrow | train_failed; the fourth cover fires | Per-axis widths, never a combined score |
| Region open at an axis's top | Folded in at S_AXIS | Same omission 21.2 §10 flags, once per axis |
VREF_MAX > 50 | $fatal at elaboration | Codes 51–63 are reserved; device behaviour undefined |
| Whole Vref axis passes | Commits; the last cover fires | Suspect the observable's sensitivity, per 21.2 §5 |
| Fragmented region on one axis | Widest run wins on that axis | Per-axis, so one axis's islands cannot corrupt the other's |
PASSES = 1 | Legal; single cross through the region | Cheapest, and most exposed to §6's tilted-region problem |
probe_done with no request | Not counted | S_PROBE gates on its own request |
| Both axes' regions found, commit succeeds, traffic fails | Not contradictory | §11's callout — codes are not volts |
| Re-run after success | Both regions and the commit discarded | Level and timing both drift — 19.5 §4 |
The first row is the one that distinguishes this search from the previous three operationally. 21.1, 21.2 and 21.3 all start at a domain edge and need no seed. Coordinate descent needs a starting point on the axis it is not sweeping, and a bad one produces a search that fails on a perfectly good channel — which is why the seed is an input and why 21.5 has to supply it from an earlier step.
13. DV — Model a Region With Shape
The checker models a device and channel whose passing region has a deliberate shape, and verifies the search's selection against it. A circular or diamond region is a poor test, because coordinate descent handles symmetric regions well; the informative models are asymmetric.
// Independent two-axis model. Knows the region; the engine does not.
// SIMULATION-ONLY.
class eye_region_model;
// A parallelogram: the passing delay window SHIFTS with Vref, which
// is the shape coordinate descent handles worst (§6).
int unsigned v_lo, v_hi; // Vref extent
int d_lo_at_vlo; // delay window at the bottom of Vref
int d_width;
int tilt; // delay-window shift per Vref code
function bit pass(int unsigned v, int unsigned d);
int lo;
if (v < v_lo || v > v_hi) return 1'b0;
lo = d_lo_at_vlo + tilt * int'(v - v_lo);
return (int'(d) >= lo) && (int'(d) < lo + d_width);
endfunction
endclass| Check | What it establishes |
|---|---|
With tilt = 0, committed Vref is the midpoint of v_lo..v_hi | The level axis's selection is correct on the easy case |
With tilt = 0, committed delay is the midpoint of the window | The timing axis likewise |
With tilt != 0, the committed pair is a passing cell | The minimum bar: descent did not land outside the region |
With tilt != 0 and PASSES = 1, record the distance from the true best cell | Quantifies §6's cost rather than asserting it away |
With tilt != 0, increasing PASSES does not make it worse | The refinement claim, checked and not assumed |
Seed outside the region; expect err_axis_empty, not a commit | §12's first row |
A region 2 codes wide on one axis with MIN of 4; expect failure | Per-axis width filter |
Drive VREF_MAX = 51; expect elaboration failure | The reserved-code guard |
| Two runs, same model, same seed; identical commit | The search is deterministic given a deterministic observable |
The fourth check is the interesting one, because it measures the algorithm's weakness instead of hiding it:
COORDINATE DESCENT ON A TILTED REGION
Vref axis 20..40, delay window 8 wide, tilt = +1 delay code
per Vref code. True best cell (largest all-pass box centre):
Vref 30, delay 18.
PASSES = 1, seed_delay = 12
Vref sweep at delay 12 : passes for Vref 20..24 only
(above 24 the window has shifted
past delay 12)
Vref centre selected : 22
delay sweep at Vref 22 : passes 10..17, centre 13
committed : (22, 13) -- a PASSING cell
distance from true best : 8 Vref codes, 5 delay codes
PASSES = 3, same seed
pass 1 -> (22, 13)
pass 2 -> Vref sweep at delay 13 gives 20..25, centre 22;
delay sweep at 22 gives 10..17, centre 13
pass 3 -> unchanged
committed : (22, 13) -- CONVERGED, and still
8 codes from the true best
diagnosis : more passes did not help. Coordinate descent converged
to a FIXED POINT that is not the global best, because
on a tilted region each axis's midpoint is optimal
given the other axis's current value.
why it matters : the committed cell passes every property in §11
and every check above except the fourth. The search is
self-consistent and locally optimal and wrong.
the real fix : a better seed, or a search that samples the grid
more widely before descending -- not more passes.That report is the honest statement of what coordinate descent buys and costs. It reduced §6's probe count from a product to a sum, and on a tilted region it can converge to a stable answer well away from the best available cell — while satisfying every assertion in the chapter. This is the same category of finding as 20.3 §12's silent corruption: correct logic, correct policy, poor outcome, no observable complaint.
14. Debugging
| Symptom | Likely cause | How to confirm |
|---|---|---|
err_axis_empty on the first axis | Seed outside the region — §12 | Retry with a seed from 21.2's result |
| Commits, but the pair is near a region edge | Tilted region, descent converged locally — §13 | Probe a coarse full grid once, offline, and compare |
| Vref region wide, delay region narrow | The timing axis is binding | Expected at high rates; compare 20.5's published window |
| Delay region wide, Vref region narrow | The level axis is binding | A channel or termination question — Module 22 |
| Whole Vref axis passes | Observable insensitive to level | Check the stimulus and that the level is actually being applied |
| Trained Vref differs greatly between devices | Plausible — per-component property | JEDEC defines a per-component pin-average centre; expect spread |
| Trains fine, writes fail, reads pass | Vref trained is the device's — §1 | The write path's level is device-side; the read path's is in the PHY |
| Trains fine, reads fail, writes pass | The PHY-side reference is the untrained one | §1's other half; not an MR6 problem |
| Committed pair moves a lot between runs | Seed varying, or a marginal region | A deterministic observable should give a deterministic commit |
| Region shifts with temperature | Expected | Periodic retraining — 19.5 §4 |
Rows seven and eight are the payoff of §1, and together they are the most common real confusion in this area. “Vref training” in DDR4 means the DRAM's receiver reference, so it acts on writes. A read-direction level problem cannot be fixed through MR6 at all, and time spent sweeping MR6 to fix a read failure is time spent on the wrong side of the link.
Row six is worth expecting rather than investigating. The standard's own construct for the device-side level is a pin-average centre — defined as the midpoint between the largest and smallest per-pin Vref level across all DQ pins of a component — which exists precisely because per-pin levels are not identical.
15. Misconceptions
“A schmoo plot is an eye diagram.” §5. One is pass/fail outcomes on two integer code axes; the other is an analog superposition in volts against time. Neither of this grid's axes has a known scale, so its aspect ratio and area are not quantities.
“More passing cells means more margin.” §7. The cell count scales with the sweep's step sizes. Halving a step doubles the count on an unchanged channel.
“Vref training centres the read eye.” §1. DDR4's VrefDQ is the DRAM's DQ receiver reference, so it acts on writes. The read-side reference is inside the PHY and is not JEDEC-specified.
“The Vref code step is 0.65% of VDDQ.” That is the table's nominal mapping. The standard specifies the physical step only as 0.5%–0.8%, one value per design (§4), so 50 codes of travel span somewhere between 25% and 40% of VDDQ.
“The two Vref ranges overlap, so you can get a finer effective step.” §3 and §4. No voltage is reachable in both ranges — the 15.00% offset is not a multiple of 0.65% — and the resulting nominal 0.05% interleave is finer than the accumulated set tolerance needed to reach it.
“Reserved Vref codes are just unused.” Codes 51–63 are reserved, and device behaviour on them is undefined rather than merely unhelpful. The RTL treats driving one as an elaboration error.
“Coordinate descent finds the best cell.” §13. It finds a fixed point, which on a tilted region can be well away from the best cell — while every assertion passes.
“More descent passes fix a bad result.” §13's report shows convergence to the same wrong answer. The fix is a better seed or wider initial sampling, not iteration.
“A full grid is always better.” It is more informative and costs a product rather than a sum (§6). Whether that is affordable is a boot-time question — Module 23's — and the usual answer is to sample a coarse grid once and descend from it.
“Both axes can share one minimum-width threshold.” §7. That would assert a conversion between the axes which does not exist. The RTL keeps two.
16. Interview Reasoning
Whose reference level does DDR4's VrefDQ training set, and which direction does it affect? The DRAM's own DQ receiver reference, via MR6 — so it affects writes. The controller's read-side reference is internal to the PHY and has no standard interface.
Why does a second axis change the search so much? Cost goes from a sum to a product, the notion of a centre stops being well defined because distances along the two axes are not commensurable, and the region acquires a shape that the cheap search strategy is sensitive to.
What is a schmoo plot, precisely? A pass/fail map indexed by two integer code axes. It is a digital proxy for an eye, it has no voltage or time scale, and it has no error-rate dimension.
The Vref table says 0.65% per code. Can you convert a 10-code region to millivolts? No. The table is a nominal mapping; the standard specifies the implemented step only as 0.5%–0.8%, fixed per design, and set tolerance accumulates over steps. The region is 10 codes wide and its width in millivolts is not known.
Range 1 spans 60–92.5% and Range 2 spans 45–77.5%. Can you combine them for finer resolution? Not usefully. The 15% offset is not a multiple of the 0.65% step, so no value is common to both; the closest cross-range pair is 0.05% apart nominally, which is well inside the set tolerance accrued reaching it.
Why compute each axis's centre from its own codes only? Because comparing a distance in delay codes with a distance in Vref codes requires a conversion the search does not have. Any combined score smuggles one in.
Your descent commits a passing cell that sits near the region's edge. Is the search broken? Not necessarily — it may have converged to a local fixed point on a tilted region. More passes will not help; a better seed or a coarse grid sample first will.
Why is the seed an input rather than a constant? Because coordinate descent sweeping one axis needs the other axis held somewhere inside or near the region. A seed outside it yields no passing cells on the first sweep and therefore no gradient, so the search fails on a working channel.
Write levelling passes, read levelling passes, this passes, and writes still corrupt. Where do you look? At the device-side level and the write DQ timing together — this chapter's write-direction pair — and at whether the level committed is actually in effect, since MR6 has a separate training-mode enable from the operating value.
17. Exercises
-
Replace the two per-axis width thresholds with a single check on
vref_width × dly_width. Which cover property in §11 becomes reachable as a failure you would now wrongly accept? Explain what conversion that product silently assumes. -
Using §13's model with
tilt = +1,PASSES = 1and seeds of 12, 16 and 20, compute the committed pair for each. Which seed gets closest to the true best cell, and what does the spread tell you about how 21.5 should choose one? -
The RTL sweeps each axis from code 0 upward. Rewrite the Vref sweep to start at
seed_vrefand expand outward in both directions. What does this buy on a tilted region, and what new failure case does it introduce? -
Verify from the published table that Range 2 code 23 is 59.95% and Range 1 code 0 is 60.00%. Then compute how many steps of travel in Range 2 are needed to reach that point, and argue from §4 whether the 0.05% difference is usable.
-
Add a
region_touches_edgeoutput, asserted when either axis's selected region reaches its domain limit. Write the property relating it to the corresponding cover, and explain why a caller should treat a commit with it set differently from one without. -
The chapter claims a cell count is not a margin. Construct two channels — differing only in the sweep step sizes used to measure them — that produce the same physical region and different cell counts. State the general rule your example illustrates.
-
Specify the interface for a coarse full-grid pre-pass that runs once and supplies a seed to this block. What is its probe cost relative to §6's numbers, and how would you decide whether it is worth paying?
-
All four searches in this module have now had a failure mode in which the observable does not change across a sweep. For this chapter, that can happen on either axis independently. Write a single check, usable by any of the four engines, that flags a sweep whose observable never changed — and say why it should be a warning rather than an error.
18. Where This Goes
Four searches are built. A phase report, a correctness conjunction, a presence count, and now a two-dimensional grid — each with its own region shape, stopping condition and selection policy, and each committing a value that the normal datapath consumes only behind a validity flag.
This chapter added the module's sharpest honesty problem and answered it with the standard's own words. The grid a two-axis search produces looks exactly like an eye and is not one: its axes are code indices, one of them maps to voltage through a step the standard pins down only to a 0.5%–0.8% band, its boundary is a single threshold crossing rather than an error-rate contour, and JEDEC says of its own receiver mask that it is not the valid data eye.
What is still missing is the thing that makes any of it usable. Four searches exist; nothing has said in what order they run. And the dependencies are real and already visible: 21.2 §3 cannot begin until 21.3 has succeeded; 21.3 §3 needs MPR mode, which 21.2 §4 also needs; this chapter needs a seed that only an earlier search can supply; and 21.1 must precede any attempt to write known data at all.
Chapter 21.5 orders them, propagates their failures, and connects the whole flow to the lifecycle 19.5 built — the phy_operational and trained_valid that Module 19 supplied and never earned.
Continue learning
Related tutorials
- Related topic
Other Sideband Signals
The signals that fit no earlier family: command-path parity, the one output a DRAM drives back to the controller, the selector that made multi-function pins possible, and the references that stopped being designed.
- 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
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.
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.
