UCIe · Module 8
Link Training
How UCIe turns untrusted mainband wires into a qualified logical link — MBINIT and MBTRAIN, per-lane ID patterns, evidence accumulation, lane qualification, deskew, candidate-versus-active configuration with atomic commit, phase-local timers, and training debug.
Discovery ended with a partner. Two dies know each other exists, their sideband works, and both have agreed to proceed. What neither die knows is anything at all about the mainband.
Consider what is genuinely unknown at this point. Whether any given lane carries a signal. Which physical lane corresponds to which logical position — Chapter 7.3 established that identity lives in a table, and that table is empty. Whether the lanes arrive close enough together in time to be reassembled into a word. Whether the forwarded clock's phase sits usefully inside the data eye. Whether the width both ends configured is actually achievable on this assembly. Every one of those is a fact about this specific pair of dies in this specific package at this moment, and none of it can be assumed from a datasheet.
Training is where those facts get established. It is the largest, most stateful, and most failure-prone phase of bring-up, and it is where the majority of real integration debugging happens.
1. The One-Sentence Model
Training is evidence collection. The PHY assumes nothing about lane identity, timing, mapping, or health — it observes enough to decide which physical resources can be trusted, and then commits that decision.
Hold onto the word evidence, because it separates training from detection. Detection asks "did something arrive?" Evidence asks "did enough of the right thing arrive, often enough, to justify relying on this lane for the next hour?" Those are different questions, and the gap between them is §7's most common bug.
The second word to hold onto is commits. Everything training learns is provisional until it is applied, and how it is applied — atomically, at a safe boundary, all at once — turns out to matter as much as what was learned (§13).
2. What Training Must Establish
Six categories, and it is worth seeing them as a list because each fails differently:
| Category | Question training answers |
|---|---|
| Clock relationship | is the forwarded clock's phase usable against the data? |
| Signal presence | does anything arrive on each lane? |
| Lane identity | which physical lane is carrying which logical position? |
| Lane health | does it arrive correctly, repeatedly, not just once? |
| Relative timing | how far apart do lanes arrive, and can that be compensated? |
| Sufficiency | is the set of qualified lanes enough for the configured width? |
The last one is the only decision; the first five are measurements. That structure — measure, measure, measure, then decide — is why §13's shadow-then-commit architecture is the right shape, and why a design that lets each measurement immediately change live configuration is wrong before you look at any detail.
3. What UCIe Actually Does
Chapter 8.2 left the link exiting SBINIT to MBINIT. UCIe's mainband bring-up is not one monolithic phase but a structured sequence of named sub-states, and knowing their shape is worth more than memorising the list.
MBINIT — mainband initialisation. Its sub-states cover parameter exchange, on-die calibration, and repair-and-reversal work: repairing the clock and valid lanes where applicable, detecting lane reversal, and repairing mainband data lanes. This is the phase that establishes what physical resources exist and how they are arranged. Repair depends on redundancy being present, which is a package-class property — the advanced-package mainband interface is described as 68 lanes (64 data plus 4 redundant), while the standard-package interface is 16 data lanes, so a standard-package link has no mainband data-lane spares to repair into.
MBTRAIN — mainband training, where the operating point is established and the mainband moves to the highest data rate negotiated between the link partners. Its sub-states are named for the work they do: reference-voltage training for the valid and data lanes (VALVREF, DATAVREF), a speed-change step (SPEEDIDLE), transmitter and receiver clock calibration (TXSELFCAL, RXCLKCAL), centring (VALTRAINCENTER, DATATRAINCENTER1, DATATRAINCENTER2), and receive deskew (RXDESKEW). Not all of them run in every case — published descriptions note that additional calibrations such as receiver clock correction and transmit/receive deskew may be performed in sub-states at higher speeds, so the depth of the sequence is rate-dependent rather than fixed.
TRAINERROR — the error state. From it, the link can pass through SBINIT and MBINIT again to repair and/or retrain.
Four structural observations, each of which shapes the rest of this chapter:
Bring-up is staged, not monolithic. Each sub-state establishes one thing and hands the result forward. That is why §14's timers must be phase-local: a single global timer for a multi-stage process is a bug generator.
Repair and reversal are first-class, not exception handling. Chapter 7.3 argued that logical identity survives physical change because mapping is state; MBINIT is where that state gets built, including when a lane needs replacing.
Speed changes mid-bring-up. Chapter 8.1 noted that the mainband clock starts at the slowest supported rate; MBTRAIN is where the link moves to the highest negotiated rate. Everything established before the speed change must therefore be re-established or re-validated after it, which is also why the deeper calibration sub-states are the ones associated with higher speeds. Margin at 4 GT/s says nothing about margin at 32.
Calibration is interleaved with training. TXSELFCAL, RXCLKCAL, and the centring sub-states are calibration work sitting inside the mainband training sequence. Chapter 8.4 treats calibration as a distinct discipline — the search-and-commit control problem — and that separation is conceptual, not a claim about ordering. Stated carefully: training and calibration are conceptually separable mechanisms, even though the UCIe bring-up flow interleaves calibration-related sub-states inside training.
4. Why a Known Pattern
Training transmits data the receiver already knows. That sounds circular until you list what it buys:
- Error detection without a protocol. The receiver compares against an expected value, so it can count errors before any framing, CRC, or flit structure exists.
- Lane identification. If the pattern differs per lane, receiving it tells you which lane you are looking at (§6).
- Timing observation. A known transition density lets the receiver sweep its sampling point and see where the data is stable.
- Deskew measurement. Knowing what should arrive, and when, lets the receiver measure how far each lane's arrival differs from the others (§9).
- No chicken-and-egg. Real traffic cannot be used to train the link that would carry it.
The one thing a known pattern does not prove is that the link works for real data. A pattern is one traffic profile; Chapter 7.6 §4 explained that channel behaviour is pattern-dependent, so passing training is evidence of margin at the training pattern, not at every payload the link will ever carry. That is a genuine limitation, not a pedantic one — it is why Chapter 7.6's runtime telemetry exists at all.
// Illustrative training RTL — not normative UCIe pattern content or encoding.
// A known-sequence abstraction: the generator and the checker share one source
// of truth, which is the entire point.
localparam int PAT_W = 16;
logic [PAT_W-1:0] expected_word; // what this lane should carry, this beat
logic [PAT_W-1:0] lane_rx_word [NUM_LANES];
logic [NUM_LANES-1:0] lane_match;
always_comb
for (int l = 0; l < NUM_LANES; l++)
lane_match[l] = (lane_rx_word[l] == expected_word_for_lane(l));Architecture. Comparison against an expectation is the cheapest possible error detector, and it works before any higher-level structure exists.
State. None here — the comparison is combinational. The state is in §7, where the results accumulate.
Cycle behaviour. Evaluated every beat that training data arrives.
Contract. Both ends must derive expected_word_for_lane() from the same definition. If the generator and the checker disagree, training fails on every lane and looks exactly like a completely dead mainband.
Failure. A real PHY's observation is richer than a word compare — it may sample at multiple phases, count transitions, or observe over a defined window. This code is pedagogical: it shows what is being established, not how a production receiver does it.
DV. Inject a known corruption on one lane and confirm only that lane's match deasserts. Inject a generator/checker mismatch and confirm every lane fails — which is the signature §17 uses to separate "pattern problem" from "lane problem".
5. One Sample Is Not Evidence
The most common training bug, in one line:
// WRONG — a single observation treated as qualification.
lane_trained_q[l] <= lane_match[l];A lane that is marginal rather than dead will match sometimes. Chapter 7.6 established that electrical marginality is statistical — an error rate, not a binary condition — so a single sample is a coin flip weighted by margin. A lane with barely any margin will pass this check and then produce errors for the rest of the link's life.
Worse, it is not stable: lane_trained_q[l] follows lane_match[l], so a lane that passes on one beat and fails on the next de-qualifies itself, and any downstream reduction over the trained mask flickers.
Evidence means accumulation:
// Illustrative training RTL — not normative UCIe naming or thresholds.
localparam int OBS_W = 12;
localparam int GOOD_TARGET = 12'd2048; // illustrative, not a UCIe constant
localparam int BAD_LIMIT = 12'd4;
logic [OBS_W-1:0] lane_good_samples_q [NUM_LANES];
logic [OBS_W-1:0] lane_bad_samples_q [NUM_LANES];
always_ff @(posedge phy_clk or negedge rst_n) begin
if (!rst_n) begin
for (int l = 0; l < NUM_LANES; l++) begin
lane_good_samples_q[l] <= '0;
lane_bad_samples_q[l] <= '0;
end
end else if (clear_attempt_state) begin
// Per-attempt state: a retry starts from no evidence, not partial evidence.
for (int l = 0; l < NUM_LANES; l++) begin
lane_good_samples_q[l] <= '0;
lane_bad_samples_q[l] <= '0;
end
end else if (observing) begin
for (int l = 0; l < NUM_LANES; l++) begin
if (lane_match[l]) begin
if (!(&lane_good_samples_q[l])) lane_good_samples_q[l] <= lane_good_samples_q[l] + 1'b1;
end else begin
if (!(&lane_bad_samples_q[l])) lane_bad_samples_q[l] <= lane_bad_samples_q[l] + 1'b1;
end
end
end
endArchitecture. Lane health is a rate, so qualification must be a judgement over many observations rather than a reaction to one.
State. Two saturating counters per lane. Two rather than one because "how much good" and "how much bad" are different questions: a lane with 2048 good and 0 bad is not the same as one with 2048 good and 300 bad, and a single net score conflates them.
Cycle behaviour. Both increment only while observing, and both saturate. Both clear together on clear_attempt_state, which is the retry boundary of §15.
Contract. The qualification logic in §7 reads both. Nothing outside training does.
Failure. Without saturation, a long observation window wraps a counter and a bad lane reads as pristine — Chapter 7.6 §11's wrapping-counter bug, now deciding whether a lane gets used. Without the per-attempt clear, a retry inherits the previous attempt's evidence and qualifies a lane on data collected before whatever caused the retry.
DV. Drive a lane that matches 99% of the time and confirm it fails qualification. Drive more observations than a counter can hold and confirm saturation. Force a retry mid-observation and confirm both counters clear.
6. Lane Identity
Chapter 7.3's central claim was that logical identity and physical resource are connected by a table. Training is where the table's contents are discovered, and UCIe provides the mechanism directly: a Per Lane ID pattern, in which the pattern carried on each lane encodes that lane's identity.
The published description of its use in the lane-reversal sub-state is worth reading for its structure rather than its constants. The module sends the Per Lane ID pattern for 128 iterations, LSB first, on all mainband data lanes, with correct framing on the Valid lane and the forwarded clock running; the partner compares per lane and logs the result; and detection on a lane counts as successful if at least 16 consecutive iterations are detected. The result is then returned over the sideband so both ends share the same picture.
Three things in that sentence are worth extracting, because they are the general lesson rather than the specific numbers: identification is repeated rather than sampled once, success is defined by a consecutive-run criterion rather than a single match, and the outcome is exchanged over the sideband rather than assumed to be symmetric.
The elegance is worth appreciating. Because each lane carries a different known value, a receiver observing lane p and finding the pattern for identity n has learned logical n ↔ physical p — from the same observation that also tells it the lane is functional. Identity and health fall out of one measurement.
That mechanism is what makes lane reversal and repair tractable. If the package routed the lanes in reverse order, the receiver simply observes identity 15 on physical lane 0 and builds a reversed map. If a lane failed and a spare was substituted — where the package class provides one, per §3 — the receiver observes the identity on whichever physical resource actually carries it. Neither case requires the package to tell anyone anything — the evidence is in the data.
// Illustrative training RTL — not normative UCIe encoding.
// Observed identity per physical lane, with a validity bit. The map is built
// FROM this, not assumed.
localparam int LID_W = $clog2(NUM_LOGICAL_LANES);
logic [LID_W-1:0] observed_id_q [NUM_PHYS_LANES];
logic observed_id_valid_q [NUM_PHYS_LANES];Architecture. The correspondence between identity and resource is a property of this assembly, disturbed by routing, orientation, and repair. It must be measured.
State. One identity per physical lane plus a validity bit — sized by physical lanes, because spares have identities to observe too (or not, if unused).
Cycle behaviour. Written when a lane's observed pattern is consistently decodable; cleared on retry.
Contract. §12's candidate map is built from this. Nothing consumes it directly.
Failure. If a lane's identity is captured from a single ambiguous observation, the candidate map contains a wrong entry — and §12's uniqueness check is what catches it before it reaches the datapath.
7. Qualification
Turning counters into a decision:
// Illustrative training RTL — not normative UCIe thresholds.
typedef struct packed {
logic seen; // any activity observed at all
logic qualified; // enough good evidence, few enough errors
logic failed; // conclusively bad — do not use, consider repair
logic deskew_valid; // a timing offset has been measured for it
} lane_train_state_t;
lane_train_state_t lane_state_q [NUM_PHYS_LANES];
always_comb
for (int p = 0; p < NUM_PHYS_LANES; p++) begin
lane_qualify_now[p] = (lane_good_samples_q[p] >= OBS_W'(GOOD_TARGET))
&& (lane_bad_samples_q[p] < OBS_W'(BAD_LIMIT))
&& observed_id_valid_q[p];
lane_fail_now[p] = (lane_bad_samples_q[p] >= OBS_W'(BAD_LIMIT));
endArchitecture. A lane is usable when there is enough positive evidence and not too much negative evidence and its identity is known. All three, because each covers a different failure: a dead lane, a marginal lane, and a lane whose position cannot be determined.
State. Four bits per physical lane. seen is separate from qualified because nothing arriving and the wrong thing arriving are different findings — §17 uses exactly that distinction.
Cycle behaviour. Combinational over the accumulated counters; the struct bits latch when the conditions hold.
Contract. §16's readiness reduction consumes qualified; the repair logic consumes failed.
Failure. Qualifying without the error check accepts a marginal lane. Qualifying without the identity check accepts a working lane whose position is unknown — which produces a mapping failure later, in a completely different-looking part of the flow.
DV. Cover all four state-bit combinations that are reachable, and assert that the two impossible ones — qualified && failed — never occur.
8. Deskew
The most physically interesting part of training, and the one with the sharpest RTL consequence.
Chapter 7.5 established that lanes do not arrive together. Transmit paths differ, package routes differ, receive paths differ, and PVT affects them unequally. That mismatch is skew, and it is largely systematic — repeatable for a given lane at given conditions. Systematic is exactly what makes it correctable: measure the offset, store a compensation, apply it.
Deskew is therefore three things in sequence:
Measure. Determine, per lane, how far its arrival differs from the reference. This is what UCIe's receive-deskew sub-state (RXDESKEW) exists for, and the centring sub-states around it establish where the sampling point should be relative to the data — the same centre of the eye idea Chapter 7.5 §3 identified in the forwarded clock's phase placement.
Store. Hold the per-lane compensation as state.
Align. Apply it, so that the parallel word reassembles from samples that correspond to the same transmitted beat.
The last one is what a digital engineer should internalise. Skew is not merely a margin problem; it is a word-integrity problem. If lane 7 arrives one beat later than lane 0, then reassembling a word from the two takes bit 7 from a different transmitted word than bit 0 — and the result is not a marginally corrupted word, it is a blend of two words that never existed. Chapter 7.3 §7 identified the same class of failure from a mid-transfer map change; this is the timing version.
// Illustrative training RTL — not normative UCIe encoding or range.
localparam int DESKEW_W = 6;
logic signed [DESKEW_W-1:0] lane_offset_q [NUM_PHYS_LANES];
logic lane_offset_valid_q [NUM_PHYS_LANES];Architecture. Compensation is per lane, because the skew is per lane. A single global adjustment cannot align lanes that differ from each other.
State. A signed offset per physical lane — signed because a lane may be early or late relative to the reference — plus a validity bit per lane.
Cycle behaviour. Written when that lane's measurement completes. Not applied on write: applied at commit (§13).
Contract. The receive alignment logic consumes the committed offsets. Both ends must agree on what the reference is.
Failure. An offset applied while other lanes are still unmeasured produces a partially aligned word, which is worse than an unaligned one because it is harder to recognise.
9. Do Not Advance on Partial Measurement
// WRONG — any lane measured is treated as all lanes measured.
assign deskew_complete = |lane_offset_valid_q;This is Chapter 7.3 §12's reduction-OR bug in its timing form, and it is worth seeing twice because the two instances have different symptoms. There, an OR over trained lanes declared a module ready with one lane working. Here, an OR over measured lanes declares deskew complete with one lane measured — so the remaining lanes are aligned by whatever their compensation registers happened to contain.
The correct form is a reduction AND over the required set, not over all lanes:
// Illustrative — every REQUIRED lane must have a valid measurement.
logic [NUM_PHYS_LANES-1:0] offset_valid_vec;
logic deskew_complete;
always_comb
for (int p = 0; p < NUM_PHYS_LANES; p++)
offset_valid_vec[p] = lane_offset_valid_q[p];
assign deskew_complete =
((offset_valid_vec & required_lane_mask) == required_lane_mask);Why over the required set rather than all lanes. A spare lane that is not in use has no reason to be measured, and requiring it would make an otherwise healthy link fail to train. Chapter 7.3 §14's point that spares do not widen the link has a corollary here: spares do not gate readiness either. The mask is the mechanism that keeps optional resources optional.
// Illustrative — no commit before every required lane has been measured.
property p_no_commit_before_all_measured;
@(posedge phy_clk) disable iff (!rst_n)
commit_pulse |-> ((offset_valid_vec & required_lane_mask) == required_lane_mask);
endproperty
a_no_commit_before_all_measured :
assert property (p_no_commit_before_all_measured)
else $error("Commit with unmeasured required lanes: valid=%h required=%h",
offset_valid_vec, required_lane_mask);The bug this catches is subtle and real: a design that advances when most lanes are measured, because the last one is slow and the timeout is generous. It works in every simulation where all lanes behave identically, and fails on the one assembly where a lane is slightly slower to converge.
10. Candidate Versus Active
Everything training learns must be held somewhere before it is used. Chapter 7.3 §7 introduced the pattern; training is where it earns its keep, because training produces the values.
// Illustrative training RTL — not normative UCIe naming.
// SHADOW state — written freely throughout training.
logic [PHYS_IDX_W-1:0] candidate_map_q [NUM_LOGICAL_LANES];
logic signed [DESKEW_W-1:0] candidate_offset_q [NUM_PHYS_LANES];
logic [NUM_PHYS_LANES-1:0] candidate_usable_q;
// ACTIVE state — written only by an atomic commit at a safe boundary.
logic [PHYS_IDX_W-1:0] active_map_q [NUM_LOGICAL_LANES];
logic signed [DESKEW_W-1:0] active_offset_q [NUM_PHYS_LANES];
logic [NUM_PHYS_LANES-1:0] active_usable_q;Architecture. Training is a search: values are provisional, revised, and sometimes abandoned. The datapath needs a configuration that never changes underneath it. Those are irreconcilable requirements for one register, so there are two.
State. Two complete copies of the configuration. That is real area, and it is the price of correctness — a design that saves it by writing configuration in place has bought a hazard.
Cycle behaviour. Candidate state is written throughout training, whenever a measurement completes. Active state changes on exactly one cycle, the commit.
Contract. The datapath reads only the active copy. Training writes only the candidate copy. The commit is the only place they touch, which makes it the only place to look when configuration is wrong.
Failure. Without the split, every intermediate measurement — including the wrong ones a search passes through — becomes live configuration.
11. Atomic Commit
The implementation detail worth the most in practice.
// WRONG — configuration becomes active one lane at a time.
always_ff @(posedge phy_clk) begin
if (lane_offset_valid_q[lane_idx_q]) begin
active_map_q[lane_idx_q] <= candidate_map_q[lane_idx_q];
active_offset_q[lane_idx_q] <= candidate_offset_q[lane_idx_q];
lane_idx_q <= lane_idx_q + 1'b1; // next lane next cycle
end
endThis looks careful — each lane is only updated once its measurement is valid — and it produces deterministic corruption.
Walk it. On the cycle lane 0 is updated, lanes 1 through 63 still hold their previous configuration. If any transfer is reconstructed during those 64 cycles, it takes lane 0's contribution through the new map and offset and everything else through the old ones. The reassembled word never existed at the transmitter. It is not corrupted data — it is a blend of two configurations, which fails integrity checks in a way that looks exactly like a channel problem.
The correct form updates everything on one edge, gated on the datapath being quiesced:
// Illustrative — one commit, one edge, everything together.
always_ff @(posedge phy_clk or negedge rst_n) begin
if (!rst_n) begin
for (int l = 0; l < NUM_LOGICAL_LANES; l++)
active_map_q[l] <= PHYS_IDX_W'(l); // identity default (7.3 §9)
for (int p = 0; p < NUM_PHYS_LANES; p++)
active_offset_q[p] <= '0;
active_usable_q <= '0;
config_committed_q <= 1'b0;
end else if (commit_pulse && datapath_quiesced) begin
active_map_q <= candidate_map_q; // whole array, one edge
active_offset_q <= candidate_offset_q;
active_usable_q <= candidate_usable_q;
config_committed_q <= 1'b1;
end else if (leave_active) begin
config_committed_q <= 1'b0;
end
endArchitecture. A configuration that is consumed as a set must be replaced as a set. Partial replacement creates states that are internally inconsistent and externally indistinguishable from data corruption.
State. Three active arrays plus a committed flag. The flag matters: it lets assertions and diagnostics distinguish "configured" from "defaulted".
Cycle behaviour. Everything changes on the single cycle where commit_pulse && datapath_quiesced. Both terms are required — wanting to commit is not the same as it being safe to.
Contract. The datapath may assume the active configuration is stable while it is operating. §16 asserts it.
Failure. The lane-by-lane version above, whose signature is deterministic corruption appearing exactly at the moment training completed — which is the one clue that separates it from an electrical problem, and only if someone thought to correlate the two events.
DV. Assert that no active-state element changes except on the commit cycle. Force a commit request while the datapath is busy and confirm it is deferred rather than taken.
12. Validation Before Commit
Between measuring and committing sits a step that is easy to omit: checking that what was measured is self-consistent.
// Illustrative — the candidate configuration must be a legal configuration.
logic candidate_legal;
logic map_is_injective;
logic enough_lanes;
// Every logical lane maps to a distinct, usable physical lane (7.3 §8).
assign map_is_injective = check_injective(candidate_map_q, candidate_usable_q);
assign enough_lanes = ((candidate_usable_q & required_lane_mask)
== required_lane_mask);
assign candidate_legal = map_is_injective && enough_lanes && deskew_complete;Architecture. Measurements are made independently per lane, so nothing in the measurement process guarantees the collection is coherent. Two lanes could report the same identity. A required lane could be missing. Deskew could be incomplete. Each is legal in isolation and fatal in combination.
State. None — all combinational over candidate state.
Cycle behaviour. Evaluated in the validate phase, before the commit pulse is generated.
Contract. The commit may only fire when candidate_legal. That is the single gate between a search and a live configuration.
Failure. Committing a non-injective map delivers real data to the wrong logical position — Chapter 7.3 §8's point that duplication is uniquely nasty because it moves no error counter. Committing with insufficient lanes produces a link that trains "successfully" and cannot carry the configured width.
// Illustrative — commit only a validated candidate.
property p_commit_requires_legal_candidate;
@(posedge phy_clk) disable iff (!rst_n)
commit_pulse |-> candidate_legal;
endproperty13. The Training Controller
// Illustrative training FSM — NOT UCIe normative state names or encoding.
// A teaching-scale model of a much larger real flow (§3).
typedef enum logic [3:0] {
TR_IDLE = 4'd0, // waiting for discovery to complete
TR_SEND = 4'd1, // begin transmitting training patterns
TR_OBSERVE = 4'd2, // accumulate per-lane evidence
TR_DESKEW = 4'd3, // measure per-lane timing offsets
TR_VALIDATE = 4'd4, // check the candidate configuration is coherent
TR_COMMIT = 4'd5, // atomically apply it
TR_DONE = 4'd6, // trained; the mainband may carry traffic
TR_RETRY = 4'd7, // clear per-attempt state and try again
TR_FAIL = 4'd8 // attempts exhausted; record cause and stop
} train_state_t;// Illustrative training RTL — not normative UCIe naming or encoding.
train_state_t train_state_q, train_state_d;
logic [PHASE_TMR_W-1:0] phase_timer_q;
logic [RETRY_W-1:0] train_retry_q;
logic phase_timeout;
assign phase_timeout = (&phase_timer_q);
always_comb begin
train_state_d = train_state_q; // explicit default: hold
unique case (train_state_q)
TR_IDLE : if (discovery_complete) train_state_d = TR_SEND;
TR_SEND : if (patterns_active) train_state_d = TR_OBSERVE;
TR_OBSERVE : if (phase_timeout) train_state_d = TR_RETRY;
else if (all_required_seen) train_state_d = TR_DESKEW;
TR_DESKEW : if (phase_timeout) train_state_d = TR_RETRY;
else if (deskew_complete) train_state_d = TR_VALIDATE;
TR_VALIDATE : train_state_d = candidate_legal ? TR_COMMIT : TR_RETRY;
TR_COMMIT : if (datapath_quiesced) train_state_d = TR_DONE;
TR_DONE : if (peer_restart_seen) train_state_d = TR_RETRY;
TR_RETRY : train_state_d = (train_retry_q >= RETRY_W'(MAX_TRAIN_RETRY))
? TR_FAIL : TR_SEND;
TR_FAIL : ; // terminal until reset
default : train_state_d = TR_IDLE; // illegal encoding recovers
endcase
end
always_ff @(posedge phy_clk or negedge rst_n) begin
if (!rst_n) begin
train_state_q <= TR_IDLE;
phase_timer_q <= '0;
train_retry_q <= '0;
end else begin
train_state_q <= train_state_d;
// PHASE-LOCAL timer: cleared on every state change, not just at the start.
if (train_state_d != train_state_q) phase_timer_q <= '0;
else if (!phase_timeout) phase_timer_q <= phase_timer_q + 1'b1;
if ((train_state_q == TR_RETRY) && !(&train_retry_q))
train_retry_q <= train_retry_q + 1'b1;
end
end
assign commit_pulse = (train_state_q == TR_COMMIT) && datapath_quiesced;
assign clear_attempt_state = (train_state_q == TR_RETRY);
assign training_complete = (train_state_q == TR_DONE);Architecture. Training is a staged measurement process where each stage can stall independently, so it needs staged control with per-stage bounds.
State. A nine-value FSM, a phase timer, a retry counter. Note TR_COMMIT waits for datapath_quiesced rather than assuming it — the commit is requested by reaching the state and taken when it is safe.
Cycle behaviour. One transition per clock. commit_pulse is a level that is true only in COMMIT with the datapath quiesced, and the machine leaves COMMIT on the same condition — so the commit happens exactly once.
Contract. The Adapter must not see the mainband as usable before training_complete. §16 asserts it.
Failure. Without the default branch, an illegal encoding has no recovery. Without peer_restart_seen on DONE, a peer that restarts after training leaves this side believing it has a trained link to a die that is back in sideband initialisation.
14. Phase-Local Timers
The timer line above is small and carries a real lesson:
// Illustrative — the timer resets on EVERY state change.
if (train_state_d != train_state_q) phase_timer_q <= '0;
else if (!phase_timeout) phase_timer_q <= phase_timer_q + 1'b1;// WRONG — one timer for the whole flow, started once.
if (train_state_q == TR_IDLE) global_timer_q <= '0;
else if (!global_timeout) global_timer_q <= global_timer_q + 1'b1;Why the second is a bug generator. Each phase gets whatever time the earlier phases left over. If observation is slow on a particular assembly — a marginal lane taking longer to accumulate evidence — then deskew inherits a nearly-expired timer and times out, and the recorded failure cause is DESKEW when the actual problem was in OBSERVE. The failure moves depending on how long an unrelated earlier phase took.
That produces the worst diagnostic property a bug can have: the reported cause is wrong, and it is wrong differently on different parts. A team chasing intermittent deskew failures across a population will find no pattern, because the pattern is in a phase the failure report does not mention.
// Illustrative — the phase timer is genuinely phase-local.
property p_phase_timer_clears_on_transition;
@(posedge phy_clk) disable iff (!rst_n)
(train_state_d != train_state_q) |=> (phase_timer_q == '0);
endproperty
// Illustrative — a timeout must produce a state change, not a longer wait.
property p_timeout_leaves_phase;
@(posedge phy_clk) disable iff (!rst_n)
(phase_timeout && (train_state_q inside {TR_OBSERVE, TR_DESKEW}))
|=> (train_state_q != $past(train_state_q));
endpropertyBoth timer widths and thresholds here are illustrative. Real bounds must cover the legitimate worst case for each phase — which differ by orders of magnitude between, say, pattern observation and a PLL reprogram at a speed change — and that is another argument for per-phase timers: one bound cannot be right for phases with different natural durations.
15. What Retry Clears and What It Must Not
Bring-up state divides into classes with different lifetimes, and getting the division wrong is how retries stop being independent.
| Class | Examples | Cleared on retry? |
|---|---|---|
| Per-cycle | lane_match, candidate_legal | n/a — combinational |
| Per-attempt | observation counters, candidate map, candidate offsets, observed identities, phase timer | yes, all of it |
| Per-link | discovery completion, negotiated parameters | no — survives into the next attempt |
| Diagnostic | retry count, failure cause, per-lane historical error counts | no — deliberately sticky |
| Reset-lifetime | reset cause, reset count | cleared only by a broader reset |
// Illustrative — per-attempt state clears together, at one defined moment.
property p_retry_clears_attempt_state;
@(posedge phy_clk) disable iff (!rst_n)
(train_state_q == TR_RETRY) |=>
((phase_timer_q == '0) &&
(candidate_usable_q == '0) &&
all_observation_counters_zero);
endpropertyWhy this assertion is worth writing even though it restates the code: per-attempt cleanup is spread across several always blocks — the counters in §5, the identities in §6, the candidate state in §10, the timer in §13. A refactor that moves one is exactly how attempt n+1 inherits evidence from attempt n, and the resulting bug is a lane that qualifies on data collected before whatever broke it. Naming every piece in one place makes the tool responsible for the invariant.
And the diagnostic row matters just as much in the other direction. Chapter 8.1 §18 established that a counter cleared by the event it counts counts to one. Here: if the retry counter or the failure cause is cleared by the retry, a link that trains on the third attempt every single time is indistinguishable from one that trains first time.
// Illustrative training RTL — sticky failure cause, not normative.
typedef enum logic [2:0] {
TRAIN_FAIL_NONE = 3'd0,
TRAIN_FAIL_PATTERN = 3'd1, // no expected pattern seen on most lanes
TRAIN_FAIL_LANE = 3'd2, // a required lane never qualified
TRAIN_FAIL_DESKEW = 3'd3, // a required lane's offset never converged
TRAIN_FAIL_MAP = 3'd4, // candidate map incoherent (duplicate/missing)
TRAIN_FAIL_TIMEOUT = 3'd5, // a phase bound expired
TRAIN_FAIL_PEER = 3'd6 // peer restarted mid-attempt
} train_fail_cause_t;
train_fail_cause_t train_fail_cause_q;
logic [NUM_PHYS_LANES-1:0] lanes_failed_ever_q; // sticky across attemptsArchitecture. After a training failure, three questions matter: which phase failed, why, and which lanes were implicated. Only hardware present at the moment of failure can answer them.
State. A cause enum plus a sticky per-lane failure vector. lanes_failed_ever_q accumulates across attempts, which is what distinguishes "one lane is consistently bad" from "a different lane failed each time" — two findings with completely different causes.
Cycle behaviour. The cause is written on entry to RETRY or FAIL; the sticky vector ORs in each attempt's failures.
Contract. Firmware and diagnostics read them. Nothing functional depends on them — the wide observability path again.
Failure. With no cause register, every training failure reports as "training failed", and §17's classification table becomes unusable because the evidence it needs was never recorded.
16. Readiness and Traffic Gating
Chapter 7.3 §12 established that module readiness is a mask comparison. Training is where the masks get their values:
// Illustrative — readiness over the REQUIRED set, not any lane.
assign module_trained =
config_committed_q &&
((active_usable_q & required_lane_mask) == required_lane_mask);// Illustrative — ready implies every required lane is trained.
property p_ready_implies_required_trained;
@(posedge phy_clk) disable iff (!rst_n)
module_trained |-> ((active_usable_q & required_lane_mask) == required_lane_mask);
endproperty
// Illustrative — a failed lane cannot contribute to readiness.
property p_failed_lane_not_usable;
@(posedge phy_clk) disable iff (!rst_n)
module_trained |-> ((active_usable_q & lanes_failed_now) == '0);
endproperty
// Illustrative — no mainband payload before training completes.
property p_no_mainband_traffic_before_trained;
@(posedge phy_clk) disable iff (!rst_n)
mainband_tx_fire |-> training_complete;
endproperty
// Illustrative — the active configuration is stable while operating.
property p_active_cfg_stable_when_trained;
@(posedge phy_clk) disable iff (!rst_n)
(training_complete && !commit_pulse) |=> $stable(active_map_q);
endpropertyThe last two are the ones that catch integration errors rather than training errors. p_no_mainband_traffic_before_trained is a safety property guarding the boundary the Adapter sees — it is the training-phase counterpart of Chapter 7.1 §10's acceptance gating. p_active_cfg_stable_when_trained is what makes the atomic-commit argument enforceable rather than aspirational.
17. A Cycle Walkthrough
Illustrative timing — real phase durations differ by orders of magnitude and are specification- and implementation-dependent.
| Cycle | State | Required mask | Usable (candidate) | Deskew valid | Phase timer | Meaning |
|---|---|---|---|---|---|---|
| 100 | TR_SEND | FFFF | 0000 | 0000 | 0 | patterns beginning to transmit |
| 101 | TR_OBSERVE | FFFF | 0000 | 0000 | 0 | timer cleared on entry |
| 400 | TR_OBSERVE | FFFF | 7FFF | 0000 | 299 | 15 lanes qualified; lane 15 still accumulating |
| 480 | TR_OBSERVE | FFFF | FFFF | 0000 | 379 | all required lanes seen and qualified |
| 481 | TR_DESKEW | FFFF | FFFF | 0000 | 0 | phase timer cleared again — §14 |
| 620 | TR_DESKEW | FFFF | FFFF | FFFE | 139 | lane 0's offset still converging |
| 655 | TR_DESKEW | FFFF | FFFF | FFFF | 174 | every required lane measured |
| 656 | TR_VALIDATE | FFFF | FFFF | FFFF | 0 | map injective? enough lanes? deskew complete? |
| 657 | TR_COMMIT | FFFF | FFFF | FFFF | 0 | waiting for datapath_quiesced |
| 659 | TR_COMMIT | FFFF | FFFF | FFFF | 2 | quiesced — commit fires this edge |
| 660 | TR_DONE | FFFF | FFFF | FFFF | 0 | active config valid; mainband may carry traffic |
Three rows repay attention. Cycle 400 shows why a single sample is not evidence — fifteen lanes have qualified and one has not, and with §5's wrong version all sixteen would have qualified and de-qualified repeatedly for the previous 300 cycles. Cycle 481 is the phase-timer clear that §14's global-timer bug omits; without it, deskew would begin with 379 of its budget already spent. And cycles 657 to 659 show the commit being requested and then taken — two different events, separated by whatever it takes for the datapath to be safe.
18. Verifying Training
The scoreboard checks configuration, not payload. This is the point most training testbenches miss. A scoreboard that only compares transmitted and received data will pass a link whose lane map is wrong in a way that happens to be self-consistent, and will fail to notice that training took four attempts. A training scoreboard maintains a reference model of:
- the expected lane map, derived from how the testbench wired and impaired the lanes;
- the expected qualified set, given which lanes were made to fail;
- the expected deskew offsets, given the delays injected;
- the expected outcome — success, retry-then-success, or failure with a specific cause;
- the expected number of attempts.
Then it compares the committed configuration against that model. A design that reaches the right data through the wrong configuration is a design that will fail on the next assembly.
Error injection is where the value is. Each of these exercises a distinct path:
| Injection | What it must produce |
|---|---|
| One lane stuck at a constant | that lane fails qualification; others unaffected |
| One lane intermittent (99% good) | fails qualification — proves §5's accumulation works |
| Two lanes swapped | map reflects the swap; data still correct after commit |
| All lanes reversed | reversed map built; no failure |
| One lane delayed beyond the deskew range | deskew fails for that lane, cause recorded as DESKEW |
| Pattern corrupted on every lane | cause recorded as PATTERN, not LANE |
| Two lanes reporting the same identity | validation rejects; commit does not fire |
| Observation phase stalled | OBSERVE times out; deskew's timer still starts full |
| Peer restarts mid-attempt | attempt abandoned; no commit on stale evidence |
| Optional lane absent | trains successfully — spares do not gate readiness |
| First attempt fails, second succeeds | committed config correct; retry count reads 1 |
The last three are the ones regressions usually lack, and they are where §15's state-lifetime bugs live.
// Illustrative training coverage — not UCIe-defined.
covergroup cg_training @(posedge phy_clk iff train_state_change);
cp_outcome : coverpoint train_state_q {
bins done = {TR_DONE};
bins retry = {TR_RETRY};
bins failed = {TR_FAIL};
}
cp_attempts : coverpoint train_retry_q {
bins first = {0}; bins retried = {[1:MAX_TRAIN_RETRY-1]}; bins last = {MAX_TRAIN_RETRY};
}
cp_bad_lanes : coverpoint failed_lane_count {
bins none = {0}; bins one = {1}; bins few = {[2:3]}; bins many = {[4:$]};
}
cp_cause : coverpoint train_fail_cause_q;
cp_remap : coverpoint (active_map_is_non_identity);
cp_deskew : coverpoint deskew_spread_class; // none / small / near-limit
// Did training ever SUCCEED on a retry, and with how many bad lanes?
x_outcome_by_attempt : cross cp_outcome, cp_attempts;
x_outcome_by_lanes : cross cp_outcome, cp_bad_lanes;
// Was a remapped configuration ever trained with near-limit deskew?
x_remap_by_deskew : cross cp_remap, cp_deskew;
endgroupWhy these crosses. Success on the first attempt with no bad lanes and an identity map is what every regression hits immediately, and it exercises none of the interesting code. The valuable points are success after a retry (proving cleanup works), success with bad lanes (proving repair and required-mask logic work), and a remapped configuration at near-limit deskew (where two independently-correct mechanisms interact, which is where the reasoning is thinnest).
19. Failure Signatures
| Pattern failure | Single-lane electrical | Mapping mismatch | Deskew failure | Peer restart | Timeout bug | |
|---|---|---|---|---|---|---|
| Lanes affected | most or all | one | all positions, but data arrives | one or a few | all | n/a |
| Reproducible | yes | intermittent | fully deterministic | rate-dependent | no | depends on prior phase |
| Temperature / voltage | no effect | strong effect | none | strong effect | none | none |
| Rate sensitivity | none | worse when faster | none | worse when faster | none | none |
| Recorded cause | PATTERN | LANE | MAP | DESKEW | PEER | often the wrong phase |
| Retry helps | no | sometimes | no | sometimes | maybe, or livelocks | masks it |
| First move | generator/checker agreement, both ends | that lane's channel and calibration | both ends' maps and identities | offsets, rate, Chapter 7.5 skew | both ends' timelines | timer scoping |
Two columns deserve comment. Pattern failure is distinctive because it takes down most lanes at once — which almost never happens electrically, since channel impairments are per-lane. Many lanes failing simultaneously usually means the two ends disagree about what should be sent, not that the package fell apart.
And timeout bug is the row worth remembering: its signature is that the recorded cause is wrong, and it varies across parts with no physical correlate. If failure causes are distributed across phases with no pattern, suspect timer scoping before suspecting the phases.
20. Debug Checklist
- Did discovery complete on both ends? Chapter 8.2 — training against a partner still in SBINIT looks like total pattern failure.
- Did both endpoints enter training? Not just the one you are looking at.
- Are training patterns being transmitted? Check the transmit side's activity, not its intent.
- Are they being received?
seenversusqualifiedis the distinction — nothing arriving and the wrong thing arriving are different findings (§7). - Which lanes show activity? A lane with zero observations is a different problem from one with observations and errors.
- Which lanes qualified, and which failed? Read the counters, not just the resulting mask.
- Is the required mask right? A required mask that includes a spare will never be satisfiable.
- Are the observed identities correct and distinct? Duplicate identities point at pattern generation; missing ones at observation.
- Does every required lane have a valid deskew measurement? Check the vector, not
deskew_complete. - Is the candidate map complete and injective?
- Did validation pass, and if not, which check failed? Three independent checks with three different causes.
- Was the commit actually taken? Reaching COMMIT and committing are separated by
datapath_quiesced. - Did the active configuration change atomically? A partially committed configuration is deterministic corruption dated to the moment training finished.
- Did a timeout fire, in which phase, and was that phase's timer freshly started?
- What cause was recorded, and which lanes are in the sticky failed vector?
- Did the peer restart mid-attempt? Compare both ends' timelines.
Steps 1 to 6 are readable from state and resolve most cases. Steps 7 to 13 need the internal training state to be observable — which, like Chapter 6.5's buried-die access, must be designed in before it is needed.
21. On "Equalisation"
An accuracy note, because the shorthand invites a wrong mental model.
If your instincts come from long-reach serial links, "training" is strongly associated with equalisation — negotiating transmitter FFE coefficients and adapting receiver CTLE and DFE to compensate a lossy channel. UCIe's training does not have that shape, and the reason is Chapter 7.2's design point: over a few millimetres of package routing, the transmitter is described as a CMOS driver with programmable drive strength and no feed-forward equaliser. There is much less to equalise.
What UCIe's training does do, from §3's sub-state list, is establish the operating point: reference-voltage training for the valid and data lanes, transmitter and receiver clock calibration, data-to-clock centring, and receive deskew. That is calibration and centring — choosing where to sample and against what threshold — rather than compensating a channel's frequency response.
The right mental model is "find the best place to sample", not "reshape the signal until it is samplable". Importing the PCIe model leads engineers to look for coefficient exchanges that are not there, and to miss that the centring sub-states are the real equivalent.
That said, the picture is rate-dependent: at the highest UCIe 3.0 rates the margin situation tightens considerably, and published PHY work at 48 and 64 GT/s discusses enhanced equalisation among the techniques for managing crosstalk and inter-symbol interference. Check what your target revision and rate actually specify rather than assuming either extreme.
22. Common Misconceptions
"Training just checks whether data toggles." It establishes clock relationship, presence, identity, health, relative timing, and sufficiency — six different facts, five of them measurements (§2).
"One good sample is enough." Marginality is statistical. A single observation is a coin flip weighted by margin, and it makes the trained mask flicker as well (§5).
"If any lane trains, the module is ready." Readiness is a mask comparison over the required set — Chapter 7.3 §12's reduction-OR bug, which reappears here as a partial-deskew bug (§9, §16).
"Lane mapping can be updated live during training." Every intermediate measurement then becomes live configuration, including the wrong ones a search passes through (§10).
"Deskew is one global adjustment." It is per lane, because lanes differ from each other. A global value cannot align them (§8).
"One timer for the whole FSM is fine." Each phase then inherits the leftovers, so the reported failure cause is wrong and wrong differently on different parts (§14).
"Retry can keep the candidate state — it is probably still valid." It was collected before whatever caused the retry. Per-attempt state clears; diagnostic state deliberately does not (§15).
"A trained lane must be enabled." Chapter 7.3 §4 — a healthy spare is trained-capable and deliberately unused, and requiring it would make a healthy link fail (§9).
"Training success proves margin at all PVT." It proves margin at the training pattern, at bring-up conditions, at the rate trained. Chapter 7.6 exists because none of those generalises (§4).
"UCIe equalisation works like PCIe equalisation." UCIe's short channel means centring and calibration rather than channel compensation; the sub-states are *TRAINCENTER and DESKEW, not coefficient negotiation (§21).
"If every lane passes individually, the parallel word must be correct." Per-lane health says nothing about whether the lanes are aligned or correctly identified. Both are word-level properties (§6, §8).
23. Understanding Check
24. Summary and What Comes Next
Training is evidence collection. The PHY assumes nothing about the mainband and measures six things: clock relationship, presence, identity, health, relative timing, and sufficiency. Five are measurements; only the last is a decision — which is why the architecture is measure, validate, then commit.
UCIe structures this as MBINIT (parameter exchange, on-die calibration, clock and valid repair where applicable, reversal detection, mainband repair) followed by MBTRAIN, where the link moves to the highest negotiated rate and works through reference-voltage training, clock calibration, centring, and receive deskew — with the deeper calibration sub-states associated with higher speeds rather than run unconditionally. TRAINERROR is the error state from which the link can pass through SBINIT and MBINIT again to repair and retrain. Four structural lessons: bring-up is staged, repair and reversal are first-class but package-class dependent, the rate changes mid-flow so nothing established before survives unexamined, and calibration is interleaved with training rather than following it.
The digital mechanisms to keep: a known per-lane ID pattern yields identity and health from one observation, which is what makes reversal and repair tractable. Evidence accumulates in two saturating counters per lane, because one sample is a coin flip. Deskew is per lane and is a word-integrity problem, not merely a margin one. Configuration is built in shadow and applied by atomic commit at a quiesced boundary, because partial commit produces a blend of two configurations that looks exactly like channel corruption. Validation sits between measurement and commit, checking injectivity, sufficiency, and completeness. Timers are phase-local, or the recorded failure cause is wrong in a way that varies across parts. And per-attempt state clears on retry while diagnostic state deliberately does not — a retry counter cleared by the retry counts to one.
Training establishes which resources exist, which can be trusted, and how to align them. It leaves a continuous-valued question open: within the margin that remains, where should the PHY actually operate — which threshold, which phase, which drive strength — and how should that choice be maintained as temperature and voltage move?
- 8.4 — Link Calibration — candidate, best, and active settings; apply, settle, and measure; searching for a passing window and choosing its centre; runtime recalibration and the hysteresis that keeps it from oscillating.
Browse the full path on the UCIe tutorials index.