Skip to content
VLSI Mentor

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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:

FieldMeaning
A7VrefDQ Training Enable — 0 = normal operation, 1 = training mode
A6VrefDQ Training Range — 0 = Range 1, 1 = Range 2
A5:A0VrefDQ Training Value — the code

The code table is published, and it is exactly linear in the code:

BaseStepCodesSpan
Range 160.00% of VDDQ0.65%000000110010 (0–50)60.00% – 92.50%
Range 245.00% of VDDQ0.65%000000110010 (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%:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   15.00 / 0.65 = 23.0769...      not an integer

So no voltage is reachable in both ranges. Not one. The grids interleave instead of coinciding:

CodePublished
Range 22359.95%
Range 1060.00%
Range 22460.60%
Range 1160.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 VDDQ

So 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                    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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 it

What 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.”

PolicyDefinitionProblem
Per-axis midpointMidpoint of the widest row, midpoint of the widest columnThe two may not intersect inside the region
Largest inscribed rectangleThe biggest axis-aligned all-pass box; commit its centreDepends on the axes' unknown relative scales (§5)
Maximum-margin cellThe passing cell furthest from any failing cellRequires a distance, and there is no metric across two unscaled axes
Widest-row midpointSweep the level axis, pick the row with the most passes, take its midpointIgnores 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

The architecture of a two-axis centring search, arranged in four rows. The top row holds the two configurable resources the search drives: the device side reference level, programmed through mode register six in the case of DDR4 and therefore governing the write direction, and the controller side sampling delay, which is a technology-specific PHY resource. The second row shows the search engine in the middle, flanked on the left by the axis sequencer that decides which of the two axes is currently being swept, and on the right by the candidate register pair that holds the experimental level code and the experimental delay code. The third row shows the observation path: a stimulus source supplying a known pattern, the link and capture path under test, and a comparison stage returning a single pass or fail bit. The fourth row holds the committed outputs, kept deliberately separate from the candidate registers, namely the trained level code, the trained delay code, and a trained valid flag, together with a node marking the region record from which the per axis centres are computed. Two annotations are attached: one noting that neither axis has a known physical scale, so distances along them cannot be compared, and one noting that the comparison bit is a conjunction which localises nothing.Device VrefMR6 — write sideSampling delayPHY resourceAxis sequencerwhich axis nowSearch enginesweep, classify, stepCandidate pairexperimental codesKnown patternMPR — 21.2 §4Link + capture20.3's pathCompareone bit, a conjunctionRegion recordper-axis edgestrained_vrefcommittedtrained_delaycommittedtrained_validgates normal use12

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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));

endmodule

The 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 cycles
Ten cycles of one coordinate descent pass, compressed to one candidate per cycle for legibility rather than the four repeats the parameters default to. The clock runs throughout. For the first five cycles the engine sweeps the reference level axis with the sampling delay held at its seed value of eight: level codes twenty through twenty-eight are probed in steps of two, and the comparison passes at codes twenty-two, twenty-four and twenty-six while failing at twenty and twenty-eight. At the fifth cycle the level axis is finished and its own midpoint, code twenty-four, is selected and held. The engine then switches axes: for the last five cycles the level stays fixed at twenty-four while the delay axis is swept over codes four through twelve, passing at six, eight and ten and failing at four and twelve. On the final cycle the delay midpoint of eight is selected, both coordinates are committed together, and trained valid rises. Note that each midpoint is computed only from its own axis's codes, because no conversion between the two axes exists. Nothing in this diagram indicates the physical size of either step.Vref region 22-26, centre 24Vref region 22-26, centre24axis switch: Vref fixed, sweep delayaxis switch: Vref fixed,sweep delaydelay region 6-10, centre 8 — commitdelay region 6-10, centre 8— commitCKsweeping_vrefcand_vref20222426282424242424cand_delay888884681012cmp_passtrained_vref------------------24trained_delay------------------8trained_validt0t1t2t3t4t5t6t7t8t9

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

CaseBehaviourWhy
Seed lands outside the regionFirst axis finds nothing; err_axis_empty§6's stall — coordinate descent needs a starting point inside or near
One axis wide, the other narrowtrain_failed; the fourth cover firesPer-axis widths, never a combined score
Region open at an axis's topFolded in at S_AXISSame omission 21.2 §10 flags, once per axis
VREF_MAX > 50$fatal at elaborationCodes 51–63 are reserved; device behaviour undefined
Whole Vref axis passesCommits; the last cover firesSuspect the observable's sensitivity, per 21.2 §5
Fragmented region on one axisWidest run wins on that axisPer-axis, so one axis's islands cannot corrupt the other's
PASSES = 1Legal; single cross through the regionCheapest, and most exposed to §6's tilted-region problem
probe_done with no requestNot countedS_PROBE gates on its own request
Both axes' regions found, commit succeeds, traffic failsNot contradictory§11's callout — codes are not volts
Re-run after successBoth regions and the commit discardedLevel 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
CheckWhat it establishes
With tilt = 0, committed Vref is the midpoint of v_lo..v_hiThe level axis's selection is correct on the easy case
With tilt = 0, committed delay is the midpoint of the windowThe timing axis likewise
With tilt != 0, the committed pair is a passing cellThe minimum bar: descent did not land outside the region
With tilt != 0 and PASSES = 1, record the distance from the true best cellQuantifies §6's cost rather than asserting it away
With tilt != 0, increasing PASSES does not make it worseThe 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 failurePer-axis width filter
Drive VREF_MAX = 51; expect elaboration failureThe reserved-code guard
Two runs, same model, same seed; identical commitThe 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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

SymptomLikely causeHow to confirm
err_axis_empty on the first axisSeed outside the region — §12Retry with a seed from 21.2's result
Commits, but the pair is near a region edgeTilted region, descent converged locally — §13Probe a coarse full grid once, offline, and compare
Vref region wide, delay region narrowThe timing axis is bindingExpected at high rates; compare 20.5's published window
Delay region wide, Vref region narrowThe level axis is bindingA channel or termination question — Module 22
Whole Vref axis passesObservable insensitive to levelCheck the stimulus and that the level is actually being applied
Trained Vref differs greatly between devicesPlausible — per-component propertyJEDEC defines a per-component pin-average centre; expect spread
Trains fine, writes fail, reads passVref trained is the device's — §1The write path's level is device-side; the read path's is in the PHY
Trains fine, reads fail, writes passThe PHY-side reference is the untrained one§1's other half; not an MR6 problem
Committed pair moves a lot between runsSeed varying, or a marginal regionA deterministic observable should give a deterministic commit
Region shifts with temperatureExpectedPeriodic 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

  1. 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.

  2. Using §13's model with tilt = +1, PASSES = 1 and 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?

  3. The RTL sweeps each axis from code 0 upward. Rewrite the Vref sweep to start at seed_vref and expand outward in both directions. What does this buy on a tilted region, and what new failure case does it introduce?

  4. 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.

  5. Add a region_touches_edge output, 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.

  6. 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.

  7. 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?

  8. 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

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.