DDR · Module 21
Write Leveling Algorithm
Chapter 20.4 built a probe that asks the device one yes-or-no question. Turning it into a search means sweeping a delay, distrusting any single answer near the boundary, confirming a transition, and keeping the candidate out of the production datapath.
Chapter 20.4 ended with a mechanism and an open question. The mechanism: the device samples CK with the write strobe and reports what it saw on DQ, one bit. The open question, in that chapter's own words, was how far from the boundary the final setting should sit — which 20.4 could not answer, because answering it requires a search.
This chapter is that search. It is also the first place in the curriculum where the training loop appears in full, so it carries more of the module's general machinery than the four chapters after it.
The central law, stated once and then qualified:
Training is controlled search over configurable PHY state, using observable pass/fail information to discover an operating point with usable timing margin.
And immediately: training does not create margin. It finds a setting inside margin that already exists physically. If the channel has no valid operating region, a correct algorithm must fail rather than return a number.
1. What the Algorithm Is Allowed to Know
Before any search, it is worth being precise about how little information this algorithm has.
It can set one number — a delay applied to the write strobe. It can ask for one probe. It gets back one bit. That is the entire interface, and 20.4 §8 built it deliberately narrow.
What the algorithm cannot do is worth listing, because every wrong mental model of training comes from assuming one of these:
| The algorithm cannot | Why not |
|---|---|
| Measure the actual CK-to-strobe phase | Nothing returns a phase. One bit returns a side. |
| Read a margin value | No margin is published to the digital domain at all. |
| Observe the eye | There is no eye in this procedure. The observable is a sampled logic level. |
| Know the physical size of one delay step | The step is a technology-specific analog quantity (19.1 §5). |
| Verify its own answer independently | The only checker available is the same one-bit probe. |
So the algorithm is not measuring anything. It is bisecting a space using a comparator, and the comparator is the device itself.
2. The Experiment Loop
Every training mechanism in this module is the same loop. It is worth naming its stages once, here, because the following four chapters vary the contents and not the structure.
configure ──► stimulate ──► observe ──► classify
▲ │
│ ▼
└────────────── update search ◄──── commit or failConfigure — apply a candidate setting to the configurable resource.
Stimulate — cause the interface to do something observable.
Observe — capture whatever the hardware reports.
Classify — reduce that report to a decision. For write levelling, to one of 0 or 1.
Update search — choose the next candidate, or stop.
Commit or fail — install a setting, or declare no setting exists.
For write levelling the loop instantiates as follows, and this table is the one to carry into the rest of the module.
| Element | Write levelling |
|---|---|
| Controllable variable | The write strobe delay, in device-agnostic delay codes |
| Stimulus | A single strobe edge, with the device in levelling mode |
| Observable | The level the device drives on DQ after tWLO |
| Pass criterion | Not pass/fail — a side: the sampled CK level, 0 or 1 |
| Search domain | The delay codes the hardware implements, 0 to DELAY_MAX |
| Stopping condition | A confirmed 0-to-1 transition, or domain exhausted |
| Selection policy | The first code whose filtered answer is 1, then an offset |
| Committed result | trained_delay, with trained_valid asserted |
| Failure result | train_failed, trained_valid not asserted, safe state held |
Note the fourth row. Write levelling is the one training step in this module whose observable is not a correctness result. Nothing here is being read back and compared. The device is acting as a phase comparator and reporting which side of CK the strobe edge landed on. 21.2 onward, the observable becomes did the data come back correct, which is a genuinely different kind of information — and §14 returns to why that distinction matters.
3. What the Standard Actually Specifies
The procedure is defined by JEDEC, and the details constrain the algorithm more than they first appear to.
Entry is a mode register write: MR1 bit A7 is Write Leveling Enable, 0 to disable and 1 to enable. Inside that mode the sequence the standard describes runs as follows.
The controller may drive the strobe low after a delay of tWLDQSEN, by which time the device has applied on-die termination to those signals. After tDQSL and tWLMRD, the controller provides a single strobe edge, which the device uses to sample CK. The standard notes that tWLMRD maximum is controller dependent — the device does not impose an upper bound on how long the controller takes.
The device then samples CK with the rising strobe edge and provides feedback on all the DQ bits asynchronously after tWLO. Two consequences follow, and both shape the RTL.
First: the feedback is not a read. The standard is explicit that no read strobes are needed for these DQ bits. There is no burst, no strobe to gate, no capture in the 20.3 sense. The feedback is a level driven on DQ, and the PHY samples it with its own clock after waiting. This is why 20.4 §7 insisted that write levelling is not read capture turned around: the read capture machinery is not involved.
Second: tWLO is nearly unbounded. For DDR4-1600, 1866 and 2133 the standard gives tWLO as minimum 0 ns and maximum 9.5 ns — the same pair at each of those grades. A window that wide is not a latency the algorithm can pipeline against; it is a settling time to wait out. This is precisely why 20.4's interface carries FEEDBACK_LATENCY as a caller-supplied parameter rather than a constant.
The standard also defines tWLOE, the write levelling output error, as the interval from the transition of the earliest DQ bit to the transition of the latest — an explicit allowance for mismatch between DQ bits. §6 is about what that allowance does to an algorithm that believes DQ.
Finally, the standard describes the search itself in one sentence worth paraphrasing carefully: the controller samples the incoming DQ bits and decides to increment or decrement the strobe delay, launching the next strobe pulse after a controller-dependent interval; once a 0-to-1 transition is detected, the controller locks the delay setting.
4. Why the Answer Is a Step Function
The device reports which side of CK the strobe edge landed on. Sweep the delay from low to high and the reported bit traces a step.
delay code 0 1 2 3 4 5 6 7 8 9 10 11
reported 0 0 0 0 0 0 1 1 1 1 1 1
▲
│
the 0-to-1 transition:
the strobe edge has crossed CKThe transition is the whole signal. Its position is the measurement — the delay code at which the strobe edge coincides with the CK edge at that device. Because 20.4 §2 established that fly-by routing makes that coincidence occur at a different delay at every device on the module, this search runs per rank and per byte lane, and returns a different answer each time. That is not error; it is the quantity being measured.
Two things about the shape matter.
The step is monotonic in principle and not in practice. In an idealised sweep the bit is 0 below the boundary and 1 above it, once. Real sweeps show the transition smeared across two or three codes, and can show isolated flips well away from the boundary. §5 explains the first and §6 the second.
The absolute delay code is meaningless; only the transition is not. A code is an index into a chain of technology-specific delay elements. It has no defined size (19.1 §5), it is not guaranteed linear, and it does not compare across designs or even across process corners. What transfers is the code at which this device's boundary was found, in this system, at this moment.
5. Why One Probe Is Not an Answer
Here is the part that separates a working write-levelling implementation from one that appears to work.
The device samples CK using the strobe edge. That is a flip-flop, and the quantity being sampled is a clock edge. At the boundary, the setup and hold conditions of that sampling element are violated by definition — the boundary is the delay at which the two edges coincide.
So near the transition the device is not reporting a stable side. It is resolving a metastable sample, and the value it resolves to is not a function of the delay code alone. Probe the same code ten times and get a mix of 0 and 1.
code 4 5 6 7 8
probe 1 0 0 1 1 1
probe 2 0 0 0 1 1
probe 3 0 1 1 1 1
probe 4 0 0 1 1 1
probe 5 0 0 0 1 1
└─────┬─────┘
the uncertain band: the answer
here is a distribution, not a valueAn algorithm that takes one probe per code and stops at the first 1 will stop somewhere inside that band, at a position that depends on which resolution it happened to catch. Run it twice and get two answers. That is the classic signature: write levelling that returns a different value on every boot, by a few codes, with no other symptom.
The fix is to stop treating the observable as a value and treat it as a sample from a distribution. Probe each candidate PROBES times and require AGREE of them to read 1 before calling the candidate a 1.
This does two things. It makes a single flip in the uncertain band unable to terminate the search, and — more usefully — it makes the width of the band observable. A candidate whose probes disagree is a candidate near the boundary, and that is information the algorithm can report even though it cannot measure a phase.
6. When the DQ Bits Disagree
tWLOE exists because the DQ bits do not all transition together. The standard defines it as earliest-transition to latest-transition and calls it an allowance for mismatch.
For an algorithm that samples DQ as a vector, this is a decision it cannot avoid making. At a candidate near the boundary, the byte lane may return 8'b0001_1111 — five bits saying one side, three saying the other.
There are only a few defensible policies, and the right one depends on what the value will be used for:
| Policy | Behaviour | When it is right |
|---|---|---|
Unanimous 1 | Treat as 1 only if all bits agree | Conservative; pushes the answer later, past the whole spread |
| Majority | Vote across bits | Robust to one outlier bit; the common choice |
| Single reference bit | Believe one nominated bit | Matches hardware that only wires one bit back |
Any bit 1 | Treat as 1 if any bit is high | Pushes the answer earliest; rarely what is wanted |
The block in §8 takes the majority across probes on an already-reduced bit, and pushes the cross-bit reduction out to the caller. That is a deliberate boundary: the number of DQ bits, whether all of them are even wired to the feedback path, and which policy the system wants are all integration facts, and a block that hard-coded one of them would be wrong in most systems that used it.
What must not happen is for the reduction to be implicit. A design that samples dq_feedback[0] because that is what the testbench drove has chosen the single-reference-bit policy without knowing it.
7. Candidate Is Not Committed
This is the discipline that makes training safe, and it is easy to get wrong because the natural implementation gets it wrong.
During the search the block must program the hardware with each candidate delay. That is not optional — the only way to observe a candidate is to apply it. So at any instant during training, the write strobe path is configured with an experimental value that has not been validated and may be badly wrong.
The normal datapath must not consume that value. It must consume a separate, committed one:
during search cand_delay ──► strobe delay hardware
(experimental, unvalidated)
after commit trained_delay ──► normal write path
trained_valid = 1
after failure trained_delay unchanged from its safe value
trained_valid = 0Three rules follow, and the RTL enforces all three.
A candidate never becomes the committed value merely because it was the last one tested. A search that ends in failure has tested a final candidate, and that candidate is very often the worst setting in the domain — the far end of an exhausted sweep. Leaving it installed and simply not raising a flag is the single most dangerous shortcut available here, because the interface then appears configured.
trained_valid gates use, and it is not the same signal as train_done. A search that finished and failed is done. It is not valid. 19.5 already established this shape for phy_operational versus trained_valid, for the same reason.
On failure the committed state must be defined. Not last-candidate, not undefined — a stated safe value. The block below holds the reset value, and says so at the line.
8. The Search, in Sequence
Two boundaries are crossed in that diagram and neither is visible in it. The apply at message two hands a code to a resource whose step size is a technology-specific analog quantity (19.1 §5) — the engine is choosing an index, not a delay. And the commit at the final message is the only point at which anything the search touched becomes production configuration; the nine messages before it all operate on an experimental value.
The repeat at message nine is the part most implementations omit and then rediscover as a boot-to-boot variance bug.
9. The Search Engine
// ---------------------------------------------------------------------
// wl_search_engine -- write-levelling search over a one-bit observable.
//
// CLASSIFICATION: educational, synthesisable. Drives the probe
// interface of Chapter 20.4 (write_level_feedback_if) and implements
// the algorithm that chapter deliberately excluded.
//
// WHAT IT DOES NOT MODEL:
// - any delay element, phase, or analog quantity
// - the DRAM's sampling of CK, or its metastability near the
// boundary (§5) -- that behaviour is on the far side of the probe
// - tWLO / tWLOE, which the probe interface absorbs
// - the cross-DQ-bit reduction of §6, which is the caller's policy
//
// The observable is one bit. Everything physical is outside.
// ---------------------------------------------------------------------
module wl_search_engine #(
// Width of a delay code. A code is an index into a technology-
// specific delay resource; it has no defined physical size.
parameter int DELAY_W = 6,
// Inclusive top of the search domain. The hardware may implement
// fewer codes than DELAY_W can express, so this is separate.
parameter int DELAY_MAX = 63,
// Probes taken per candidate, and votes needed to call it a 1.
// PROBES > 1 is what makes the §5 uncertain band survivable.
parameter int PROBES = 3,
parameter int AGREE = 2,
// A COUNT of votes up to PROBES needs clog2(PROBES+1), not
// clog2(PROBES): three votes need two bits to express the value 3.
parameter int VOTE_W = (PROBES <= 1) ? 1 : $clog2(PROBES + 1),
// Where the committed value sits relative to the boundary. The
// boundary is the first code reporting 1; a real system offsets
// past it. This is a POLICY value -- see §11.
parameter int COMMIT_OFFSET = 0
) (
input logic clk,
input logic rst_n,
// ── From the training sequencer (21.5 owns the ordering).
input logic start,
// ── The device must already be in levelling mode. This block does
// not enter or exit that mode: mode entry is an MRS command and
// belongs to whatever issues commands, not to a search engine.
input logic wl_mode_active,
// ── To Chapter 20.4's probe interface.
output logic [DELAY_W-1:0] delay_setting,
output logic probe_req,
// ── From Chapter 20.4's probe interface. ONE bit per probe, already
// reduced across DQ bits by the caller's §6 policy.
input logic probe_done,
input logic probe_result,
// ── THE COMMITTED RESULT. Consumed by the normal write path only
// when trained_valid is high (§7).
output logic [DELAY_W-1:0] trained_delay,
output logic trained_valid,
// ── Completion. done and valid are DIFFERENT signals: a search that
// finished and failed is done, and is not valid.
output logic train_done,
output logic train_failed,
// ── Observability. cand_delay is the EXPERIMENTAL setting; it is
// published so a checker can prove it never leaks into the
// committed value.
output logic [DELAY_W-1:0] cand_delay,
output logic [VOTE_W-1:0] votes,
output logic [2:0] state_out,
// Asserted for a candidate whose probes did not agree unanimously:
// the §5 uncertain band, made visible. Not an error.
output logic near_boundary,
output logic err_probe_outside_mode,
output logic err_done_while_busy
);
// ── Elaboration guards. A misparameterised search is not a subtle
// bug: AGREE > PROBES can never be satisfied, so the machine
// would classify every candidate as 0 and always fail.
initial begin
if (AGREE > PROBES)
$fatal(1, "wl_search_engine: AGREE (%0d) exceeds PROBES (%0d)", AGREE, PROBES);
if (AGREE < 1)
$fatal(1, "wl_search_engine: AGREE must be at least 1");
if (DELAY_MAX > (2**DELAY_W - 1))
$fatal(1, "wl_search_engine: DELAY_MAX (%0d) exceeds DELAY_W", DELAY_MAX);
if (COMMIT_OFFSET < 0)
$fatal(1, "wl_search_engine: COMMIT_OFFSET must not be negative");
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_COMMIT = 3'd5,
S_FAIL = 3'd6
} state_e;
state_e state, next;
logic [DELAY_W-1:0] cand;
logic [VOTE_W-1:0] ones; // probes at this candidate reading 1
logic [VOTE_W-1:0] taken; // probes taken at this candidate
logic prev_class; // previous candidate's classification
logic prev_valid; // is prev_class meaningful yet?
logic [DELAY_W-1:0] committed;
logic committed_ok;
// Classification of the candidate just finished.
logic this_class;
assign this_class = (ones >= VOTE_W'(AGREE));
// A candidate is "near the boundary" when its probes disagreed.
assign near_boundary = (state == S_CLASS) && (ones != '0) && (ones != VOTE_W'(PROBES));
// The transition we are searching for: previous candidate read 0,
// this one reads 1. prev_valid excludes the first candidate, which
// has no predecessor -- see §12 for why that case is a FAILURE and
// not a commit.
logic transition_found;
assign transition_found = prev_valid && (prev_class == 1'b0) && (this_class == 1'b1);
logic domain_exhausted;
assign domain_exhausted = (cand >= DELAY_W'(DELAY_MAX));
always_comb begin
next = state;
unique case (state)
S_IDLE : if (start && wl_mode_active) next = S_ARM;
// ARM applies the candidate and gives the delay resource a cycle
// to take it before any edge is driven.
S_ARM : next = S_PROBE;
S_PROBE : if (probe_done)
next = (taken + VOTE_W'(1) >= VOTE_W'(PROBES)) ? S_CLASS : S_PROBE;
S_CLASS : if (transition_found) next = S_COMMIT;
else if (domain_exhausted) next = S_FAIL;
else next = S_STEP;
S_STEP : next = S_ARM;
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;
ones <= '0;
taken <= '0;
prev_class <= 1'b0;
prev_valid <= 1'b0;
// THE SAFE STATE. On failure the committed value holds this,
// not the last candidate tested (§7). Zero is chosen because it
// is the domain's defined start, and because it is wrong in an
// obvious direction rather than a plausible one.
committed <= '0;
committed_ok <= 1'b0;
end else begin
state <= next;
unique case (state)
S_IDLE : if (start && wl_mode_active) begin
// A new search discards every previous result, including a
// previously committed one: the relationship it measured may
// no longer hold (19.5 §4).
cand <= '0;
ones <= '0;
taken <= '0;
prev_valid <= 1'b0;
prev_class <= 1'b0;
committed_ok <= 1'b0;
end
S_PROBE : if (probe_done) begin
taken <= taken + VOTE_W'(1);
if (probe_result) ones <= ones + VOTE_W'(1);
end
S_CLASS : begin
prev_class <= this_class;
prev_valid <= 1'b1;
ones <= '0;
taken <= '0;
end
S_STEP : cand <= cand + DELAY_W'(1);
S_COMMIT : begin
// The boundary is the FIRST code classified 1, which is the
// candidate just classified. The offset is added here and
// saturated, because a policy offset must never wrap the
// code into the bottom of the domain.
committed <= (cand + DELAY_W'(COMMIT_OFFSET) > DELAY_W'(DELAY_MAX))
? DELAY_W'(DELAY_MAX)
: cand + DELAY_W'(COMMIT_OFFSET);
committed_ok <= 1'b1;
end
// S_FAIL deliberately writes nothing: committed and
// committed_ok keep the values they already hold.
default : ;
endcase
end
end
// The experimental setting goes to the hardware. The committed one
// never does -- the normal path reads trained_delay.
assign cand_delay = cand;
assign delay_setting = cand;
assign probe_req = (state == S_PROBE) && !probe_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 votes = ones;
assign state_out = state;
// Probing outside levelling mode is a sequencing error by 21.5, and
// the resulting bit would be a normal read, not a phase report.
assign err_probe_outside_mode = probe_req && !wl_mode_active;
// done must not be claimed while the machine is still mid-candidate.
assign err_done_while_busy = train_done && (taken != '0);
endmodule10. A Search, Cycle by Cycle
Three candidates, one transition, one commit
10 cyclesThe row to watch is trained_valid. It is low for nine of ten cycles while cand_delay is being driven into the strobe path. During that whole interval the write path is configured with an unvalidated value and the committed value is untouched — which is §7 made concrete.
11. Where the Final Value Sits
The boundary is not the answer. It is the worst place to operate: it is by definition the delay at which the device's sampling of CK is marginal.
20.4 §16 left this question open, and 20.5 supplied the missing half — the published window figures that say how much there is to spend. The selection policy has to place the committed value away from the boundary, and COMMIT_OFFSET is where that policy lives.
What the offset cannot be is computed by this block, and the reason is §4: a delay code has no defined physical size. An offset of two codes might be a large fraction of a bit interval or a negligible one. So the offset is a parameter supplied by whoever integrated the delay resource and knows its granularity.
What the algorithm can contribute is the uncertain band from §5. A candidate whose probes disagreed is near the boundary; the width of the disagreeing region is a code-space measure of how sharp the transition is, and a system that records it has a genuinely useful diagnostic — not a margin, but a repeatable number that changes when the channel changes.
12. Failure Is a Result
A search that finds no transition must fail. Three distinct situations produce that, and conflating them costs real debug time.
No transition anywhere: all codes report 0. The strobe edge never reaches CK within the domain. Either the delay range is too short for the board's skew, or the strobe is not arriving at all. Distinguishable from the next case by the constant 0.
No transition anywhere: all codes report 1. The strobe edge is already past CK at code zero. The domain starts too late. Note that a naive implementation searching for a 0-to-1 transition cannot commit here even though every probe passes, and that is correct behaviour: the boundary is outside the searchable range, so its position is unknown. The RTL's prev_valid term is what enforces this — the first candidate has no predecessor and therefore cannot itself constitute a transition.
A transition, but not a usable one. Multiple transitions across the sweep, or a transition whose uncertain band is very wide. The algorithm above stops at the first one and would take a spurious flip as the boundary; a production implementation continues the sweep and requires the 1 region to be contiguous to the top of the domain. That refinement belongs to the same family as 21.2 §10's fragmented-region handling.
In all three cases trained_valid stays low and the committed value holds its safe reset value. The interface is not configured, and it does not pretend to be.
13. What the Assertions Prove
// Bind unit note: these properties reference wl_search_engine's
// internal state and are written as though bound into the module, so
// they see clk and rst_n directly.
// P1 -- the central safety property of §7. The committed value only
// ever changes on a commit. If this holds, no candidate can leak into
// the production setting by any path.
property p_commit_only_on_commit;
@(posedge clk) disable iff (!rst_n)
(trained_delay != $past(trained_delay, 1))
|-> ($past(state_out, 1) == 3'd5);
endproperty
assert property (p_commit_only_on_commit);
// P2 -- validity is never claimed by a failed search.
property p_fail_never_valid;
@(posedge clk) disable iff (!rst_n)
train_failed |-> !$rose(trained_valid);
endproperty
assert property (p_fail_never_valid);
// P3 -- done and valid are different signals, and this is the case
// that proves it: a done cycle may carry either polarity of valid.
property p_done_is_not_valid;
@(posedge clk) disable iff (!rst_n)
train_done |-> (train_failed != trained_valid);
endproperty
assert property (p_done_is_not_valid);
// P4 -- the terminating condition actually held. A commit implies the
// classification just made was 1 and its predecessor was 0. Written
// over a two-cycle window because both terms are sampled in S_CLASS,
// the cycle before S_COMMIT is entered.
property p_commit_implies_transition;
@(posedge clk) disable iff (!rst_n)
($past(state_out, 1) == 3'd3) && (state_out == 3'd5)
|-> $past(transition_found, 1);
endproperty
assert property (p_commit_implies_transition);
// P5 -- no probe is ever requested outside levelling mode. The error
// output is the mechanism; this proves it is never needed.
property p_probe_inside_mode;
@(posedge clk) disable iff (!rst_n)
probe_req |-> wl_mode_active;
endproperty
assert property (p_probe_inside_mode);
// P6 -- every candidate gets the full probe count. This is the §5
// property: a classification that used fewer probes than PROBES would
// be a single-sample decision wearing a filter's clothing.
property p_full_probe_count;
@(posedge clk) disable iff (!rst_n)
(state_out == 3'd3) |-> ($past(taken, 1) == VOTE_W'(PROBES));
endproperty
assert property (p_full_probe_count);
// P7 -- the search only ever moves up. A sweep that could step
// backwards could revisit a code and terminate on a different
// resolution of the same metastable sample.
property p_monotonic_sweep;
@(posedge clk) disable iff (!rst_n)
(cand_delay != $past(cand_delay, 1)) && (cand_delay != '0)
|-> (cand_delay == $past(cand_delay, 1) + 1);
endproperty
assert property (p_monotonic_sweep);
// ── Cover: the states worth knowing were reached.
cover property (@(posedge clk) disable iff (!rst_n) train_failed);
cover property (@(posedge clk) disable iff (!rst_n) $rose(trained_valid));
// The §5 band was actually exercised -- probes disagreed at some
// candidate. A regression that never covers this has not tested the
// filter at all.
cover property (@(posedge clk) disable iff (!rst_n) near_boundary);
// Commit at the top of the domain: the offset saturation path.
cover property (@(posedge clk) disable iff (!rst_n)
(state_out == 3'd5) && (cand_delay == DELAY_W'(DELAY_MAX)));
// A failure with every probe reading 1 -- §12's second case, which is
// the one most likely to be mistaken for success.
cover property (@(posedge clk) disable iff (!rst_n)
train_failed && $past(probe_result, 1));P1 is the property worth keeping if only one survives review. It is a complete statement of the candidate/committed separation, and it fails loudly for the most tempting wrong implementation — assigning trained_delay <= cand inside the sweep and setting a flag at the end.
P6 is the one most likely to be omitted, and it is the guard against §5's failure mode being reintroduced by a later optimisation. An engineer shortening the search by exiting early on a confident-looking probe breaks P6 immediately.
14. The Training State Machine
Two structural points, which recur in every training machine in this module.
PROBE has a self-loop and CLASSIFY does not. The repeat lives inside the observation stage, not around the whole loop. That is what makes the filter a filter over one candidate rather than an average across candidates — §5's distinction, in the state graph.
COMMIT and FAIL are separate states rather than one done state with a flag. They behave differently: one writes the committed registers, the other writes nothing. Merging them and distinguishing by a flag is how the §7 failure gets built by accident.
15. Corner Cases
| Case | Behaviour | Why |
|---|---|---|
start while already searching | Ignored — S_IDLE is the only state that accepts it | A restart mid-sweep would mix votes from two searches |
wl_mode_active drops mid-search | err_probe_outside_mode fires on the next request | The bit returned would be array data, not a phase report |
Boundary at code 0 | Fails, does not commit | No predecessor, so no transition is observable — §12 |
Boundary at DELAY_MAX | Commits, offset saturates | Saturation is explicit so a policy offset cannot wrap |
PROBES = 1, AGREE = 1 | Legal, and reintroduces §5's bug | Permitted deliberately: the parameters make the filter's presence a visible choice |
AGREE > PROBES | $fatal at elaboration | Unsatisfiable: every candidate would classify 0 and every search would fail |
All probes read 1 throughout | train_failed | §12's second case — passing probes, unknown boundary |
probe_done with no request | Not counted; S_PROBE gates on its own request | A stray completion must not become a vote |
| Re-run after a successful search | Previous result discarded at S_IDLE | The measured relationship may have drifted — 19.5 §4 |
The third row is the one that surprises people. A working interface whose boundary sits at the bottom of the delay range trains unsuccessfully, and that is right: the algorithm has not located a boundary, it has merely failed to see one below where it started.
16. DV — Model the Device, Not the Engine
A checker for this block must not re-implement the sweep, because then it agrees with the engine's bugs. What it should do is model the device and independently confirm the property the search claims.
The device model is small: pick a hidden boundary code, and return 1 for any probe at or above it, 0 below — with a band of width w around it where the returned bit is random.
// Independent device model. Knows the answer; the engine does not.
// SIMULATION-ONLY -- it contains a random resolution, and nothing
// with a random output belongs in synthesisable RTL.
class wl_device_model;
int unsigned boundary; // hidden truth
int unsigned band; // width of the metastable region (§5)
function bit probe(int unsigned code);
if (code >= boundary + band) return 1'b1;
else if (code + band < boundary) return 1'b0;
// Inside the band the device resolves a marginal sample. The
// model returns a coin flip, which is the honest representation:
// the value is not a function of the code.
else return bit'($urandom_range(1));
endfunction
endclassThe checks the testbench then makes are about the relationship between the hidden truth and the committed value, not about the sweep:
| Check | What it establishes |
|---|---|
trained_valid implies abs(trained_delay − boundary) <= band + COMMIT_OFFSET | The engine landed near the real boundary, within the uncertainty the device actually has |
Run the same model twice with different seeds; committed values differ by at most band | The filter suppressed §5's variance instead of inheriting it |
Set boundary = 0; expect train_failed | §12's boundary-at-zero case, which a naive engine commits |
Set boundary > DELAY_MAX; expect train_failed and constant 0 probes | §12's first case |
Set band wider than the domain; expect train_failed or a near_boundary report at every candidate | A channel with no usable region must not yield a confident answer |
Count probes; expect exactly PROBES per candidate visited | Independent confirmation of P6 |
The second check is the one that catches the real bug. A single-probe engine passes the first check most of the time — it does land near the boundary — and fails the second immediately, because its answer moves with the seed. That is the boot-to-boot variance of §5, reproduced deterministically in a testbench, which is the only comfortable place to meet it.
17. Debugging
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Trained value differs by a few codes every boot | PROBES = 1, or an early exit — §5 | Log the per-candidate vote counts; a healthy log shows a disagreeing band |
| Trained value differs by many codes every boot | A spurious flip is being taken as the boundary — §12 | Log the full sweep; look for a 1 with 0s above it |
train_failed with every probe reading 1 | Boundary below the domain — §12 | Check the constant 0/1 distinction in the sweep log |
train_failed with every probe reading 0 | Strobe not arriving, or domain too short | Confirm levelling mode was actually entered (MR1 A7) |
| Trains successfully, writes still fail | Only the CK relationship was trained — §11 | The data-to-strobe requirement is a separate search, 21.4 |
| Trains differently per byte lane by a large amount | Expected on a fly-by module — 20.4 §2 | Compare against the module's routing, not against the other lanes |
| Trains differently per rank | Expected; ranks are different loads and positions | Per-rank storage is required, not optional |
err_probe_outside_mode during a sweep | Sequencing bug in 21.5 | Mode entry and search start are separate events |
| Sweep never terminates | probe_done never returns | The probe's FEEDBACK_LATENCY is shorter than the real tWLO |
The fifth row is the most common real-world confusion, and it comes from believing write levelling trains “the write path.” It trains one of two independent requirements.
The last row is worth dwelling on: tWLO has a maximum of 9.5 ns and a minimum of 0, so a FEEDBACK_LATENCY chosen from a simulation model rather than from the device's datasheet is a plausible way to build an interface that works on one part and hangs on another.
18. Misconceptions
“Write levelling measures the CK-to-DQS skew.” It locates a boundary in code space. The physical skew is never produced as a number by anything in the procedure, and a delay code has no defined size (§4).
“The device tells you whether the alignment is correct.” It tells you which side of CK the strobe edge landed on. That is a comparator output, not a verdict. A 1 does not mean “good.”
“One probe per delay setting is enough.” §5. Near the boundary the device is resolving a metastable sample and the answer is a distribution.
“If every probe passes, training succeeded.” §12's second case: all-1 means the boundary is below the searchable range, so its position is unknown. This is a failure that looks maximally like success.
“Training creates margin.” It finds a setting within margin that already exists. If no valid region exists, the correct outcome is failure.
“The last tested setting can stay installed if training fails.” §7. It is usually the far end of an exhausted sweep — the worst code in the domain.
“It is one algorithm, so every PHY does it the same way.” §3: the standard fixes the mode, the stimulus, the feedback and the terminating condition. Step size, sweep direction, probe count, cross-bit policy and final offset are all open.
“Write levelling is read capture in reverse.” 20.4 §7 refuted this, and §3 gives the standard's own reason: the feedback needs no read strobe, so the capture path is not involved.
“One trained value serves the whole module.” Fly-by makes the boundary per-device. The search runs per rank and per byte lane, and the answers legitimately differ.
19. Interview Reasoning
Why does the device sample CK with DQS rather than the other way round? Because the quantity in question is where the controller's strobe lands relative to the device's clock, and only the device can observe that. Sampling CK with DQS puts the comparator on the side that has both signals in their arrived form.
Why is one probe insufficient? The comparator is a flip-flop sampling a clock edge, and at the boundary its setup and hold conditions are violated by construction. The output there is a metastable resolution, so the observable is a random variable whose distribution shifts across the band.
You find the transition at code 6. Why not commit 6? Because 6 is where the device's sampling became marginal — the worst operating point found. The commit must sit away from it, by an offset whose size only the delay resource's integrator can choose, since a code has no defined physical size.
Every probe at every code returns 1. Has training passed? No. It means the boundary lies below the domain's start, so its location is unknown. Committing here would install code 0 as though it had been validated.
What is the difference between train_done and trained_valid? done says the search terminated. valid says it terminated with a usable answer. A failed search is done and not valid, and the normal datapath must gate on valid.
Why must the candidate setting be applied to the hardware during the search? Because applying it is the only way to observe it. Which is exactly why the committed value has to be a separate register — during the whole search the live configuration is unvalidated.
Two boards, same design; one trains to code 12 and the other to code 31. Which is broken? Neither, necessarily. The boundary is a property of the routing, the load and the device position. A large difference is a question for the layout, not a training bug. What would indicate a bug is the same board training to two different codes on consecutive boots.
Why does this procedure say nothing about whether writes will work? It trains one of the write direction's two independent requirements — strobe to CK. The strobe's placement within the write data is a different relationship, trained by a different search with a different observable.
20. Exercises
-
Set
PROBES = 1andAGREE = 1and run §16's model withband = 3across twenty seeds. Record the committed value each time. Then setPROBES = 5, AGREE = 3and repeat. Report the spread in both cases, and explain which check in §16's table distinguishes them. -
The RTL fails when the boundary is at code 0. Modify it to report that case distinctly — an error output separate from
train_failed— without allowing it to commit. Why is a distinct report more useful than a generic failure here? -
COMMIT_OFFSETsaturates atDELAY_MAX. Argue for and against instead treating offset-past-the-top as a failure. Which choice would you defend in a product, and what does the answer depend on? -
Write the property that P7 (monotonic sweep) would need if the engine were changed to search downward from
DELAY_MAX. Then explain what P4 would have to become, given that the terminating condition is no longer a0-to-1transition. -
The engine takes an already-reduced
probe_result. Write the cross-bit reduction for the majority policy of §6 as a separate module, with an output that flags when the bits disagreed. Why does making the disagreement visible matter more here than the reduction itself? -
Extend §16's device model so that the boundary drifts by one code every 10,000 probes. Which of the table's checks begins to fail, and what does that tell you about how often a trained value should be revisited? Relate the answer to 19.5 §4.
-
A colleague proposes speeding up the search with a binary search over the delay domain instead of a linear sweep. Identify the assumption binary search requires, and use §4 and §5 to explain when that assumption fails. Under what added condition would binary search be safe?
-
The chapter claims no assertion in §13 can establish margin. Construct the strongest property you can that uses only the module's outputs and would fail for a committed setting with no margin. Then explain why it cannot exist, in terms of the three columns of §1's callout.
21. Where This Goes
The loop is built. A controllable variable, a stimulus, a one-bit observable, a filter over repeats, a terminating condition, a committed result and a defined failure — and a state machine whose shape the rest of the module reuses.
What made this chapter the simplest of the five is the observable. The device told us a side. One bit, directly reporting the quantity of interest, from a procedure the standard defines.
The remaining searches do not get that. Their observable is did the data come back correct, which is a much weaker signal: it conflates every possible cause of corruption into a single bit, it depends on the pattern used to produce it, and a pass does not localise anything. Chapter 21.2 takes up the read side with that weaker observable, and has to find two edges rather than one transition — which is where the pass-region map, and the question of what to do when the region is not contiguous, first become the central problem.
21.3 then searches for something the standard does not define at all.
Continue learning
Related tutorials
- Related topic
Training Question
Eight named fields answer a question about a training step you have not memorised, which is what the follow-up is for. Training finds margin and never creates it, and a pass bitmap is one classifier's answer on one pattern.
- Related topic
DDR3
DDR3 took prefetch to 8n and paid twice: a fly-by command and clock bus that deliberately skews the clock at every device, and termination that had to calibrate itself against an external reference. Between them they made training mandatory.
- Related topic
Timing Alignment
A write command and its data leave the PHY on different paths and must arrive in a defined relationship. The device specifies the relationship; the PHY absorbs the difference between the paths.
- Related topic
Write Leveling
On a write the controller already centres the strobe in its data. The device imposes a second requirement — the strobe must meet CK — and on a fly-by module that relationship differs at every DRAM.
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.
