UCIe · Module 8
Link States
The UCIe link state machine as a set of contracts — what each state guarantees, state retention across transitions, quiescence before low power, recovery ownership, transition priority, legal-transition assertions, and the ACTIVE-to-recovery transfer race.
Chapter 8.5 followed a link to the moment it reports itself operational. That moment is not the end of the link's state machine — it is roughly the beginning of the part that runs for the lifetime of the system.
A link that is up will, over the hours and years that follow, be asked to enter low power and come back, will lose trust in its physical layer and have to recover it, will be retrained, and may be reset out from under whatever was using it. Each of those is a state transition, and each one raises the same three questions: what is guaranteed now, what may be retained across the change, and who is allowed to make it happen.
Getting those answers wrong does not produce a link that fails to come up. It produces a link that comes up perfectly and then loses data three hours later during a power transition — which is a much worse bug to have.
1. The One-Sentence Model
A link state is a contract, not a label.
Each state is a set of promises. Which physical resources are valid. Whether traffic may move. Which clocks are running. What configuration is retained. Which transitions out are legal. What it costs to leave.
Treating a state as a label — a value software reads to know "where we are" — produces designs where the state register and the actual behaviour disagree, because nothing enforced the promises. Treating it as a contract produces the assertions in §11 and the retention table in §6, both of which are enforceable.
2. What the Specification Names
UCIe's link training state machine is described with these top-level states:
| State | Role as described | Traffic? |
|---|---|---|
| RESET | entered from a primary physical-layer reset, from TRAINERROR, or from the deeper low-power state | no |
| SBINIT | sideband initialisation (Chapter 8.2) | no |
| MBINIT | mainband initialisation — calibration, repair, reversal (Chapter 8.3) | no |
| MBTRAIN | mainband training; the link moves to the highest negotiated rate | no |
| LINKINIT | link initialisation work following training | no |
| ACTIVE | physical-layer initialisation complete; the link is usable | yes |
| L1 | a standby that disables much of the connection, especially across the mainband | no |
| L2 | a deeper standby, including clock shutdown, saving more power | no |
| PHYRETRAIN | retraining the link | no |
| TRAINERROR | handling an error during training | no |
Grouping them pedagogically — and this grouping is mine, not the specification's:
- Initialisation — RESET, SBINIT, MBINIT, MBTRAIN, LINKINIT. Chapters 8.1 to 8.4 own these.
- Operational — ACTIVE. The only traffic-bearing state in the list above.
- Low power — L1, L2.
- Recovery and error — PHYRETRAIN, TRAINERROR.
Two structural observations worth extracting immediately.
RESET has three named entry paths, and one of them is from the deeper low-power state. That is not an error path — it says exiting L2 goes back through initialisation rather than straight to ACTIVE, which is §9's whole point about wake cost.
There are two distinct non-ACTIVE ways to be unhealthy. PHYRETRAIN is for retraining a link that was working; TRAINERROR is for an error during training. Those are different situations with different amounts of surviving context, and §10 develops why that distinction matters.
3. Representing State in RTL
// Illustrative RTL representation. A compliant implementation uses the
// specification's own states and encodings — this is a teaching abstraction
// grouped by CONTRACT rather than by name.
typedef enum logic [2:0] {
LINK_RESET = 3'd0, // nothing assumed
LINK_TRAINING = 3'd1, // initialisation and training in progress
LINK_ACTIVE = 3'd2, // traffic permitted
LINK_LP_LIGHT = 3'd3, // standby, context largely retained
LINK_LP_DEEP = 3'd4, // deeper standby, more torn down
LINK_RECOVERY = 3'd5, // was working; restoring physical trust
LINK_FAILED = 3'd6 // cannot establish or restore
} link_state_t;
link_state_t link_state_q, link_state_d, prev_state_q;Architecture. The specification's ten states collapse, for most upper-layer purposes, into a much smaller set of contracts. Logic that only needs to know "may I send?" should not be coupled to the full training sub-state structure — that is Chapter 7.1 §16's evidence-versus-conclusions rule at state granularity.
State. Current state, next state, and previous state. prev_state_q exists for §16 and costs three flops.
Cycle behaviour. One transition per clock.
Contract. Upper layers consume a derived traffic_allowed (§12), never the raw state. Diagnostics consume the full state.
Failure. Exposing the detailed state upward couples the Adapter to the PHY's state machine, so a specification revision that adds a sub-state becomes an Adapter change.
A boundary worth stating. This enum is a view, not a replacement. A real PHY implements the specification's state machine; this abstraction sits above it. Do not treat the two as interchangeable — the mapping is one-way.
4. Next-State Discipline
// Illustrative link-state control — not UCIe's normative transition logic.
always_comb begin
link_state_d = link_state_q; // explicit default: hold
unique case (link_state_q)
LINK_RESET : if (bringup_start) link_state_d = LINK_TRAINING;
LINK_TRAINING : if (training_failed) link_state_d = LINK_FAILED;
else if (training_done) link_state_d = LINK_ACTIVE;
LINK_ACTIVE : if (lp_entry_agreed) link_state_d = LINK_LP_LIGHT;
else if (link_fault) link_state_d = LINK_RECOVERY;
LINK_LP_LIGHT : if (lp_deeper_agreed)link_state_d = LINK_LP_DEEP;
else if (wake_complete) link_state_d = LINK_ACTIVE;
LINK_LP_DEEP : if (wake_request) link_state_d = LINK_RESET; // §9
LINK_RECOVERY : if (recovery_failed) link_state_d = LINK_FAILED;
else if (recovery_done) link_state_d = LINK_ACTIVE;
LINK_FAILED : ; // terminal until reset
default : link_state_d = LINK_RESET; // illegal encoding recovers
endcase
// --- priority overrides, applied AFTER the case (§13) -----------------
if (fatal_error) link_state_d = LINK_FAILED;
if (reset_assert) link_state_d = LINK_RESET;
endArchitecture. A state machine with several independent event sources needs an unambiguous resolution order, and it needs one place where that order is expressed.
State. None beyond the registers in §3.
Cycle behaviour. The case computes the normal-operation next state; the overrides below it then take precedence, in a written order. Note LINK_LP_DEEP exits to LINK_RESET rather than to LINK_ACTIVE — modelling the described behaviour that leaving the deeper low-power state goes back through initialisation.
Contract. Everything downstream may assume exactly one next state is computed per cycle.
Failure. Without the explicit default, an unhandled condition infers a latch. Without the default: arm, an illegal encoding has no recovery. And without the override structure, §13's ambiguity appears.
5. Entry Versus Residence
A distinction that produces a real and common bug.
Some actions belong to entering a state; others to being in it. Conflating them means a one-time action runs every cycle.
// WRONG — intended as "on entry", executes every ACTIVE cycle.
if (link_state_q == LINK_ACTIVE)
clear_error_history <= 1'b1;If the intent was to clear counters once when the link becomes usable, this instead clears them continuously for as long as the link is up — so every error counter reads zero forever, and the telemetry Chapter 7.6 built is silently destroyed. The link works; the diagnostics are gone; nobody notices until something goes wrong and there is nothing to look at.
// Illustrative — a genuine one-cycle entry pulse, from registered state.
logic entered_active;
assign entered_active = (prev_state_q != LINK_ACTIVE) &&
(link_state_q == LINK_ACTIVE);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) prev_state_q <= LINK_RESET;
else prev_state_q <= link_state_q;
endWhy registered rather than comparing against link_state_d. Writing (link_state_q != LINK_ACTIVE) && (link_state_d == LINK_ACTIVE) also produces a pulse, but it is combinational on the next-state function — so it glitches as the case statement's inputs settle, and it fires a cycle earlier than the state actually changes. For a pulse that gates a real action, deriving from two registered values is safer: it costs three flops and cannot glitch.
DV. Assert the pulse is exactly one cycle wide, and cover that each state's entry action fired once per entry rather than once per residence — a distinction ordinary state coverage does not make.
6. The State-Retention Matrix
The table this chapter exists for. What survives a transition is the difference between a low-power state that saves energy and one that loses data.
| State element | ACTIVE | Light low power | Deeper low power | Recovery | RESET |
|---|---|---|---|---|---|
| Lane map | in use | retain | spec/impl dependent | retain if the lane set is unchanged | invalidate |
| Deskew settings | in use | retain | spec/impl dependent | re-establish if retrained | invalidate |
| Calibration settings | in use | retain | likely re-established (§9) | re-establish if disturbed | invalidate |
| Negotiated parameters | in use | retain | spec/impl dependent | retain | invalidate — renegotiate |
| Accepted-but-unsent payload | in flight | must be none (§8) | must be none | must not vanish (§10) | architecture decides (Ch 8.1 §9) |
| Credit / flow-control state | in use | retain, or re-advertise on exit | re-advertise | re-advertise on re-entry | reset to advertised |
| Diagnostic counters | accumulating | retain | retain | retain | retain — broader domain (Ch 8.1 §18) |
| State history | accumulating | retain | retain | retain | retain |
Three rows carry most of the weight.
Accepted-but-unsent payload. In the low-power columns the entry is not "retain" or "discard" — it is must be none, because §8 requires quiescence before entry. That is how the problem is avoided rather than solved.
Credit state. Chapter 8.1 §9 noted that UCIe/LPIF material describes credit counters being reassigned to their initially advertised values on transitions away from Active. Re-advertising is the safe default precisely because it makes both ends agree by construction rather than by both having remembered correctly.
Diagnostic counters retain everywhere. They are the only row that is uniform, and deliberately so.
The "spec/impl dependent" entries are honest rather than evasive: how much context a deep low-power state preserves is exactly the kind of thing that differs by revision and implementation, and inventing a value here would be the pre-audit mistake.
7. ACTIVE Is a State You Can Leave
ACTIVE means the physical layer is initialised and the link is usable under the defined contract. It does not mean healthy forever, and designs that treat it as an absorbing state handle degradation badly.
Entering it requires everything Chapter 8.5 assembled:
// Illustrative — ACTIVE requires every prerequisite, currently.
property p_active_requires_prerequisites;
@(posedge clk) disable iff (!rst_n)
(link_state_q == LINK_ACTIVE) |-> (training_done_q && calibration_valid_q
&& peer_operational_q && !fatal_fault_q);
endpropertyNote this holds for every ACTIVE cycle, not just on entry. Chapter 7.2 §14 made the same choice for electrical readiness and for the same reason: a prerequisite that dies underneath a running link must pull the link out, and a transition-only check cannot see that.
8. Quiescence Before Low Power
A low-power state is not "turn the clocks off". It is a contract about what is quiesced, what context remains, what must be restored, and what the wake costs.
// WRONG — a request treated as a transition.
if (low_power_req)
link_state_d = LINK_LP_LIGHT;Three things are wrong at once. Pending traffic is still in flight and will be stranded or dropped. The peer has not agreed — a link is two ends, and one end powering down its mainband while the other transmits is a data-loss event. And resources may be torn down while still in use, because the request says nothing about local readiness.
A request is the beginning of a negotiation:
// Illustrative low-power sequencing — not UCIe normative signal naming.
logic local_quiescent;
logic lp_entry_agreed;
assign local_quiescent =
(tx_occupancy_q == '0) && // nothing waiting to transmit
(rx_occupancy_q == '0) && // nothing received and unconsumed
!retry_pending_q && // no unacknowledged work outstanding
!accepted_unsent_q; // nothing accepted that has not left
assign lp_entry_agreed = low_power_req && local_quiescent && peer_lp_ack;Architecture. Entry must be safe locally and agreed remotely. Either alone is insufficient — local quiescence with an unaware peer loses the peer's traffic, and peer agreement without local quiescence loses your own.
State. Occupancy counters and pending flags that already exist for flow control; the low-power logic consumes rather than duplicates them.
Cycle behaviour. Combinational over registered occupancy. Entry occurs on the cycle all three terms hold.
Contract. The peer relies on your not tearing down resources it is still using. Upper layers rely on nothing accepted being lost.
Failure. Entering on the request alone strands in-flight work — and it is the same violation as Chapter 7.1 §12's accepted-data rule, arriving through a power transition instead of a fault.
A caution. local_quiescent above is illustrative and almost certainly incomplete for a real design: retimers, replay buffers, outstanding sideband transactions, and in-progress calibration all have their own notions of "busy". The lesson is the shape — enumerate every resource that can hold state, and require all of them idle — not this particular conjunction.
// Illustrative — no low-power entry while work is outstanding.
property p_no_lp_entry_with_outstanding_traffic;
@(posedge clk) disable iff (!rst_n)
$rose(link_state_q inside {LINK_LP_LIGHT, LINK_LP_DEEP}) |->
$past(local_quiescent);
endproperty9. Wake Is Not the Reverse of Entry
Symmetry is the intuition and it is wrong. Entering a low-power state tears down; waking must restore, then re-establish trust, then coordinate — and how much of each depends on how deep the state was.
The described distinction between the two low-power states is instructive. The lighter one disables much of the connection, especially across the mainband. The deeper one goes further, including clock shutdown. And RESET's described entry paths include the deeper low-power state — meaning exit from it goes back through initialisation rather than directly to ACTIVE.
That single fact is the entire wake-cost argument:
| Light standby | Deeper standby | |
|---|---|---|
| Torn down | much of the mainband | more, including clocks |
| Context retained | more | less |
| Exit path | back toward ACTIVE | back through initialisation |
| Wake latency | lower | substantially higher |
| Power saved | less | more |
Low-power states are a latency-versus-power ladder, and the rung you choose is a system decision. A deeper state saves more and costs a full re-initialisation to leave — which for a latency-sensitive coherent link may be unacceptable, and for an idle accelerator may be free.
The digital consequence: wake is not a single transition to model. It is clocks restored, PLL relocked and requalified (Chapter 7.5 §11), retained state validated rather than assumed, the peer coordinated with, and only then traffic permitted. Each of those can fail, which means wake needs the same bounded-wait discipline as bring-up.
10. Recovery, and Who Owns What
Recovery exists because a link can lose trust while the system still has state depending on it.
That sentence separates recovery from initialisation. At initialisation nothing depends on the link yet. At recovery, transactions are outstanding, buffers hold accepted work, and upper layers have made commitments. The physical problem is often the easy part.
Ownership divides cleanly, and this is where Modules 5 and 7 tie together:
| Layer | Owns during recovery |
|---|---|
| Physical | restoring physical trust — retrain, recalibrate, repair, requalify lanes |
| Adapter | preserving or resolving transport state — what was accepted, what must be replayed, what is reported lost |
| Protocol | semantic correctness — outstanding transactions, ordering, completion obligations |
Each layer must not solve another's problem. A PHY that discards Adapter state during recovery has made a transport decision it is not entitled to make.
// WRONG as a default policy — recovery erases accepted work.
if (enter_recovery) begin
tx_fifo_valid_q <= '0;
retry_valid_q <= '0;
endThis is the recovery-shaped instance of a bug this curriculum has now met four times — at the Adapter boundary (5.5 §7), at the PHY boundary (7.1 §12), at a clock loss (7.5 §14), and at a partial reset (8.1 §17). The rule does not change:
A flush is acceptable. An unannounced flush is not. If the recovery contract permits abandoning accepted work, it must be reported so the layer above can act.
Recovery also needs a generation, for the same reason bring-up does:
// Illustrative implementation technique — NOT a UCIe-defined field.
logic [GEN_W-1:0] link_generation_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) link_generation_q <= '0;
else if (recovery_complete) link_generation_q <= link_generation_q + 1'b1;
end
// A completion from before the link dropped cannot satisfy post-recovery state.
assign completion_is_stale = (cmpl_generation != link_generation_q);Failure without it: an acknowledgement issued before the link dropped arrives afterwards and is matched against a reused tag — real data credited to the wrong transaction, which is corruption rather than loss.
11. Legal Transitions as a Function
A pure predicate answering "is from → to legal?" serves assertions, formal, and the scoreboard from one definition.
// Illustrative — one definition of legality, reused three ways.
function automatic logic legal_transition(link_state_t from, link_state_t to);
if (from == to) return 1'b1; // holding is legal
if (to == LINK_RESET) return 1'b1; // reset from anywhere
if (to == LINK_FAILED) return 1'b1; // fatal from anywhere
unique case (from)
LINK_RESET : return (to == LINK_TRAINING);
LINK_TRAINING : return (to == LINK_ACTIVE);
LINK_ACTIVE : return (to inside {LINK_LP_LIGHT, LINK_RECOVERY});
LINK_LP_LIGHT : return (to inside {LINK_ACTIVE, LINK_LP_DEEP});
LINK_LP_DEEP : return (to == LINK_RESET);
LINK_RECOVERY : return (to == LINK_ACTIVE);
LINK_FAILED : return 1'b0;
default : return 1'b0;
endcase
endfunction
property p_only_legal_transitions;
@(posedge clk) disable iff (!rst_n)
legal_transition($past(link_state_q), link_state_q);
endproperty
a_only_legal_transitions :
assert property (p_only_legal_transitions)
else $error("Illegal transition %0s -> %0s", $past(link_state_q).name(),
link_state_q.name());Why a function rather than an expression. Three consumers need the same answer — the assertion, a formal property, and the scoreboard in §15 — and three copies of a transition table drift. It is also readable, which a single flattened boolean would not be.
Note the top three lines. Reset-from-anywhere and fatal-from-anywhere are legitimate and must be encoded, or the assertion fires on correct behaviour and gets waived — which is worse than not having it.
12. Traffic Permission Is Derived
// Illustrative — one definition of "may traffic move", not a state comparison.
assign traffic_allowed = (link_state_q == LINK_ACTIVE) && link_operational_sync;
property p_traffic_only_when_allowed;
@(posedge clk) disable iff (!rst_n)
payload_transfer |-> traffic_allowed;
endpropertyWhy derive it rather than compare against ACTIVE inline. Two reasons. If a revision or configuration permits traffic in another state, one definition changes and every consumer follows. And it composes the state with Chapter 8.5's operational conjunction, so a prerequisite dying withdraws permission even while the state register still reads ACTIVE — which is precisely the window §14's race lives in.
13. Simultaneous Requests Need Explicit Priority
Real systems present a low-power request, an error, and a reset on the same cycle.
// WRONG — two independent ifs; behaviour depends on source order.
if (low_power_req) link_state_d = LINK_LP_LIGHT;
if (link_fault) link_state_d = LINK_RECOVERY;Whichever assignment appears later wins. That is a real decision — should a fault beat a power request? — being made by text ordering rather than by design, and it changes silently when someone reorders the block.
// Illustrative — priority stated once, explicitly, highest first.
always_comb begin
link_state_d = normal_next_state; // from the case in §4
if (lp_entry_agreed) link_state_d = LINK_LP_LIGHT;
if (link_fault) link_state_d = LINK_RECOVERY; // beats power
if (fatal_error) link_state_d = LINK_FAILED; // beats recovery
if (reset_assert) link_state_d = LINK_RESET; // beats everything
endA defensible ordering — reset > fatal > recovery > power > normal — with the reasoning: reset is unconditional by construction; an unrecoverable fault must not be masked by a recovery attempt; a fault must not be masked by a power transition, because powering down a faulty link hides the fault; and power transitions are discretionary.
This ordering is not normative. Where the specification defines precedence, use that. What is universal is that the priority must be written once, explicitly, rather than emerging from statement order.
14. The Race: ACTIVE → Recovery With a Transfer In Flight
The chapter's sharpest corner case, and the one that connects to Chapter 5.5.
Same cycle: the Adapter presents valid, the PHY's ready is asserted, and a fault is detected that will move the link toward recovery.
The question is exact: did the handshake occur? Ready-and-valid on the same edge is a transfer. If both were high, the item is the PHY's, and the state transition cannot silently delete it.
| Cycle | State | valid | ready | Transfer? | Occupancy | Action |
|---|---|---|---|---|---|---|
| n | ACTIVE | 1 | 1 | yes | 0 → 1 | item accepted; PHY now owns it |
| n | ACTIVE | — | — | — | — | fault detected in the same cycle |
| n+1 | ACTIVE | 1 | 0 | no | 1 | ready withdrawn — no new accepts |
| n+1 | ACTIVE | — | — | — | 1 | accepted item retained, not cleared |
| n+2 | RECOVERY | 0 | 0 | no | 1 | state moves; item still accounted for |
| n+3 | RECOVERY | — | — | — | 1 | either it survives recovery… |
| n+3′ | RECOVERY | — | — | — | 1 → 0 | …or it is reported lost to the Adapter |
The design rules the trace encodes:
Withdraw ready first, transition second. Stopping new acceptance is a separate action from changing state, and doing them in that order closes the window in which an item is accepted into a link that is already leaving ACTIVE.
A transfer that completed is owned. The transition may not clear it. It survives or it is reported — never neither.
Do not retroactively un-accept. Deasserting ready in cycle n to "cancel" a handshake that already occurred is not possible; the Adapter has already retired its copy.
// Illustrative — a transfer that completed is not erased by a transition.
property p_accepted_survives_state_change;
@(posedge clk) disable iff (!rst_n)
(payload_transfer && $changed(link_state_q)) |=>
(item_retained || item_loss_reported);
endproperty
// Illustrative — recovery does not silently reduce occupancy.
property p_recovery_no_silent_discard;
@(posedge clk) disable iff (!rst_n)
($rose(link_state_q == LINK_RECOVERY) && (tx_occupancy_q != '0)) |->
##[0:$] (item_loss_reported || (tx_occupancy_q == '0 && drain_complete));
endpropertyThe second is a liveness property and needs care: it is only meaningful with a fairness assumption that recovery eventually resolves, and it should be bounded in practice rather than left as ##[0:$]. Written unbounded it is a formal-tool property, not a simulation one — worth knowing which you are writing.
15. State Timeouts and Debug History
Transitional states should not persist indefinitely; stable states should not be timed out. Attaching a watchdog to ACTIVE is wrong — a healthy link stays there for years. Attaching one to RECOVERY or a wake sequence is right.
// Illustrative — state age, derived from REGISTERED state only.
logic [AGE_W-1:0] state_age_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) state_age_q <= '0;
else if (link_state_q != prev_state_q) state_age_q <= '0; // just entered
else if (!(&state_age_q)) state_age_q <= state_age_q + 1'b1;
endNote the comparison. It uses link_state_q against prev_state_q — two registered values — rather than link_state_q != link_state_d, which compares a register against a combinational function and clears the age a cycle early while glitching as the next-state logic settles. The same subtlety as §5.
For silicon debug, a current-state register alone is nearly useless — it tells you where the machine ended, not how it got there:
// Illustrative debug state — small, and worth far more than its area.
link_state_t prev_state_q; // already needed for §5
link_state_t state_before_fail_q; // state at the first failure
transition_cause_t last_cause_q; // what caused the last transition
logic [CNT_W-1:0] recovery_count_q; // saturating — link flapping?
logic [CNT_W-1:0] lp_entry_count_q; // saturatingWhy recovery_count_q matters most. A link that recovers successfully every few minutes is working by every functional measure and is a serious problem. Without a saturating count in a domain that survives recovery, it is invisible — Chapter 8.1 §18's rule that observability state must outlive the events it observes.
16. Verifying the State Machine
Transition coverage matters more than state coverage. Visiting every state is easy and proves little; traversing every legal edge is what exercises the entry actions, the retention policy, and the priority logic.
// Illustrative link-state coverage — not UCIe-defined.
covergroup cg_link_state @(posedge clk iff state_changed);
cp_from : coverpoint prev_state_q;
cp_to : coverpoint link_state_q;
cp_cause: coverpoint last_cause_q;
// Every legal edge, with illegal ones excluded rather than left unreachable.
x_transition : cross cp_from, cp_to {
ignore_bins illegal = x_transition with (!legal_transition(cp_from, cp_to));
}
// Was an error injected from every state that can see one?
x_error_from : cross cp_from, cp_cause;
endgroupThe scoreboard checks what coverage cannot: no illegal transition occurred, traffic moved only when permitted, required state was retained across each transition, and the exit reason matches the injected event. That last check is the one that catches a state machine which reaches the right state for the wrong reason — which passes every functional test and misleads every debug session.
The error-injection list: fault from each state that can see one; reset from each state; low-power request while busy; low-power request racing a fault; wake failure; recovery failure; a transfer completing on the exact cycle of a fault (§14); and an illegal state encoding forced directly.
17. Debug Checklist
- What state is it in, and what was the previous state? Current alone is nearly useless (§15).
- What caused the last transition? If unrecorded, add it — it is a few flops.
- Is
traffic_allowedconsistent with the state? Disagreement between the state register and the derived permission points at §12. - If it is in ACTIVE but not carrying traffic, check Chapter 8.5's prerequisite bits — the state can be ACTIVE while operational is false.
- If it will not leave a low-power state, check the wake sequence stage: clocks, PLL requalification, retained-state validation, peer coordination.
- If it will not enter low power, check quiescence term by term — one non-empty queue blocks it, and the conjunction hides which (§8).
- How many recoveries have occurred? A flapping link looks healthy at any instant (§15).
- Did a transfer complete on the cycle of a state change? §14 — and check the item was retained or reported, not dropped.
- Is state retained across transitions per the intended policy? Compare against §6's matrix explicitly.
- Was the transition legal? Run the predicate against the observed pair (§11).
- Do both ends agree on the state? A link is two state machines, and only one is in front of you.
18. Common Misconceptions
"A link state is a software-visible label." It is a contract: which resources are valid, whether traffic may move, what is retained, which exits are legal, and what leaving costs (§1).
"ACTIVE means no failure can occur." It means the physical layer is initialised and the link is usable now. Prerequisites can die underneath it, which is why the prerequisite assertion holds every cycle rather than only on entry (§7).
"L1 and L2 work like PCIe's." Do not import them. The published UCIe descriptions distinguish a standby that disables much of the mainband from a deeper one including clock shutdown, with exit from the deeper state described as going back through RESET — and the specification is authority for the rest (§2, §9).
"A low-power request means low-power entry." A request begins a negotiation requiring local quiescence and peer agreement. Entering on the request alone strands in-flight work (§8).
"Recovery may clear all state." Recovery restores physical trust; transport and semantic state belong to layers that did not ask for it to be discarded. A flush is acceptable; an unannounced flush is not (§10).
"State transitions do not interact with payload ownership." A transfer that completed on the cycle of a transition is owned by the receiver, and the transition cannot delete it (§14).
"State coverage means the transitions were tested." Visiting states is easy; traversing every legal edge is what exercises entry actions, retention, and priority (§16).
"Two transition requests can be handled by independent if-statements." Then source order decides precedence, silently, and changes when someone reorders the block (§13).
"One state timeout should apply everywhere." Transitional states need bounds; stable states must not have them, or a healthy link times out (§15).
"The current state is enough for silicon debug." It tells you where the machine ended, not how it got there or how often it has been there (§15).
19. Understanding Check
20. Summary and What Comes Next
A link state is a contract, not a label — a set of promises about valid resources, permitted traffic, running clocks, retained configuration, legal exits, and the cost of leaving.
Published descriptions of UCIe's link state machine name RESET, SBINIT, MBINIT, MBTRAIN, LINKINIT, ACTIVE, L1, L2, PHYRETRAIN, and TRAINERROR, with RESET entered from a primary reset, from TRAINERROR, or from the deeper low-power state — and the specification is the authority for the exact set and its transitions.
The engineering that transfers regardless: entry actions are not residence actions, and deriving the entry pulse from two registered states avoids both the glitch and the off-by-one-cycle. Retention is a policy per state element per transition, and accepted payload is handled by requiring quiescence rather than by deciding what to discard. ACTIVE can be left, so its prerequisites are asserted every cycle rather than at entry. Quiescence and peer agreement both gate low-power entry. Wake is not entry reversed — the deeper state costs a re-initialisation, which makes low power a latency-versus-power ladder rather than a switch. Recovery restores physical trust only; transport and semantic state belong to layers that did not consent to losing them, and a flush must be announced. Priority is written once, explicitly, or statement order decides it silently.
Two things to carry into the rest of the curriculum. The ACTIVE-to-recovery race: a transfer that completed on the cycle of a transition is owned, so it survives or is reported — this is the same ownership rule the curriculum has now met at four different boundaries. And transition coverage over state coverage, with a legality predicate shared by assertion, formal, and scoreboard.
Module 8 is complete: the link comes up, and its state machine is understood. Everything so far has been about making a link exist. The next module is about what runs across it — and specifically about UCIe's own lightweight answer for protocols that are neither PCIe nor CXL:
- 9.1 — The Streaming Model — lightweight packet-stream transport over UCIe, and why it is a core differentiator from PCIe-over-UCIe.
Browse the full path on the UCIe tutorials index.