Skip to content
VLSI Mentor

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)
ObservableThe CK level the device sampledWhether returned data matched
What it reportsA side of a boundaryA conjunction of everything working
Bits per probe1, about one quantity1, about all quantities at once
A failing result localisesThe strobe is on the other sideNothing
A passing result establishesThe edge order at the deviceThat this pattern survived this path today
Shape of the answerOne transitionTwo 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:

FieldMeaning
A2MPR Operation — 0 = normal, 1 = dataflow from/to MPR
A1:A0MPR page selection — 00 = Page 0, 01 = Page 1, 10 = Page 2, 11 = Page 3
A12:A11MPR 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:

LocationDefault
MPR001010101
MPR100110011
MPR200001111
MPR300000000

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:

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

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

LaneMPR0MPR1MPR2MPR3
DQ01110
DQ10110
DQ21010
DQ30010
DQ41100
DQ50100
DQ61000
DQ70000

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.

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

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

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

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

PolicyChoiceConsequence
Widest regionThe region with the most passing codesThe usual choice; most tolerance
First regionThe lowest-code regionFast, and wrong whenever islands exist
Require a minimum widthReject regions narrower than MIN_WIDTHTurns 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:

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

With 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

One candidate evaluation during read levelling, across five participants: the training sequencer, the read levelling edge finder of this chapter, the controller command path, the DRAM, and the capture and compare path. The sequencer starts the search. The edge finder applies a candidate read delay code to the capture path's technology-specific delay resource. It then requests a multi-purpose register read. The controller issues that read command to the DRAM, which returns the stored training pattern on the data bus together with its strobe, taking the pattern from its registers rather than from the memory array. The capture path samples the data at the candidate delay and the compare stage checks the received beats against the pattern the engine expects. The compare stage returns one bit, pass or fail, which is a conjunction of everything in the path working rather than a report about the sampling point. The edge finder repeats the read several times at the same candidate, because a single passing burst is weak evidence. It then records the candidate as passing or failing, steps to the next code, and continues across the entire domain rather than stopping at the first failure, because the pass region is not guaranteed to be contiguous. Only after the full sweep does it select a region, check it against a minimum width, and commit a centre code.One candidate: apply, read the pattern, compare, repeatSequencerEdge finderControllerDRAMCapture+cmpstartapply cand readdelayrequest MPR readRD in MPR modepattern + DQS, notarray datasample at cand delayone bit: matched ornotrepeat at samecandidaterecord pass or failsweep the WHOLEdomaincommit centre, orfail

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

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

endmodule

The 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 cycles
Ten cycles of a read levelling sweep, compressed to one candidate per cycle for legibility rather than the four repeats the parameters default to. The clock runs throughout. The candidate delay code advances from zero to nine, one per cycle. The comparison result fails for codes zero, one and two, then passes for codes three through seven, then fails again for codes eight and nine. The recorded left edge takes the value three as soon as the first passing code is classified and holds it. The recorded right edge takes the value seven at the point the run closes, which is when code eight fails. The committed centre appears only on the final cycle, with the value five, which is the midpoint of codes three to seven. Trained valid is low for the entire sweep and rises only on that final cycle. Nothing in this diagram indicates the physical width of the region; the width is a count of five codes and codes have no defined size.left edge — first passing codeleft edge — first passingcoderight edge — the run closed at 7right edge — the run closedat 7commit centre 5 of region 3 to 7commit centre 5 of region 3to 7CKcand_delay0123456789cmp_passin_runbest_lo------3333333best_hi----------------77trained_validtrained_delay0000000005t0t1t2t3t4t5t6t7t8t9

The 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

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

CaseBehaviourWhy
No code passes anywheretrain_failed, err_empty_region§3's six causes, indistinguishable here
Every code passesCommits the domain midpoint; the full-domain cover firesLegal, but §5 says suspect the stimulus first
Region open at DELAY_MAXFolded in at S_SELECTOtherwise silently discarded — §10
Region of width 1 with MIN_WIDTH = 4train_failed, best_width non-zeroA one-code region is not an operating point
Two equal-width regionsThe higher-code one wins (>=)Either bias is fine; consistency across lanes is not optional
cmp_pass glitches high once in a failing runCandidate still failsall_passed clears on any failing repeat and never re-sets
read_done with no requestNot countedS_READ gates on its own request
start mid-sweepIgnored outside S_IDLEWould merge two sweeps' regions
MIN_WIDTH larger than the domain$fatal at elaborationNo region could ever satisfy it, so every search would fail
Re-run after successPrevious region and commit discardedThe 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
CheckWhat it establishes
trained_valid implies best_lo == lo and best_hi == hiThe edges found are the real edges, not off by one
Committed code is within one of (lo+hi)/2The 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 selectedConfirms “widest” and not “first” or “lowest”
Set hi = DELAY_MAX; expect the region still foundThe S_SELECT fold-in of §10
Set lo = hi; expect train_failed with MIN_WIDTH > 1The 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:

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

SymptomLikely causeHow to confirm
Empty region at every laneGate misplaced — §321.3 has not run or failed; check gate_trained
Empty region, gate known goodBurst misassembled — 20.3 §13Use parallel format and the §5 lane signatures
Region found but very narrow everywhereChannel, not searchModule 22; compare against 20.5's published width
Whole domain passesStimulus has no transitions — §5Confirm MR3 A12:A11 is serial, not parallel
Region differs a lot between lanesExpected — per-bit skew, 20.5 §5Per-lane sweep; consider per-bit delays (§8)
Centre differs by one between otherwise identical lanesEven-width rounding — §6Check the rounding is consistent, not per-lane
Training passes, traffic fails laterRegion was real at train time and has driftedPeriodic retraining — 19.5 §4
fragmented set with a healthy wide regionGenuinely informative, not an errorLog it; a newly fragmented sweep is a channel change
Passes with MPR, fails on array readsThe array path adds what MPR skippedRow timing, refresh, or the write path that placed the data
Region moves between consecutive sweepsREPEATS too low, or a marginal channelRaise 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

  1. Remove the S_SELECT fold-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 touch DELAY_MAX.

  2. The engine treats a candidate as passing only if all REPEATS reads 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?

  3. run_is_best uses >=, 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.

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

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

  6. MIN_WIDTH is 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 about COMMIT_OFFSET.

  7. Add a region_count output and the property that it equals one when fragmented is low. Then argue whether fragmented should ever block a commit, given §7's claim that it is a diagnostic rather than an error.

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

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.