UCIe · Module 5
The Physical Layer
The digital state inside a UCIe PHY — bring-up state machines, lane health versus lane enable, safe clock-domain crossing, training timeouts — and why a link can train perfectly and still corrupt every transfer.
The word PHY misleads. It suggests analog circuitry — drivers, receivers, equalisation — and an RTL engineer who accepts that framing concludes there is nothing here to design or verify at register-transfer level. That conclusion is badly wrong, and this chapter exists to replace it.
A die-to-die PHY contains a substantial amount of digital logic: the state machine that walks a link from reset to operational, lane health and configuration state, mapping, status reporting, timeout and recovery handling, and the clock-domain crossings where all of that meets the rest of the die. Most bring-up debugging happens in that logic, and most of the bugs are ordinary RTL bugs with ordinary RTL fixes.
1. Turning a Possibility Into a Resource
The Adapter of Chapter 5.2 consumes one fact from below: phy_link_up. Producing that fact honestly is the entire job of this layer.
Before bring-up, a die-to-die connection is only a physical possibility — conductors exist, but nothing is known about whether the partner is present, which lanes work, what configuration both sides support, or whether timing can be established. Afterwards it is a digital resource the Adapter can treat as operational.
The PHY converts an unreliable physical possibility into a digital resource the layer above can treat as operational — and abstracts away everything it took to get there.
The Adapter must not need to know about eye margin, individual training attempts, or per-lane tuning. It needs a truthful answer to is this usable? and, where the architecture defines one, is it degraded?
2. Digital and Analog, Divided
Concretely, the two sides:
Analog and electrical — drivers and receivers, termination and signal conditioning, clocking circuits, and whatever tuning the implementation performs. This is where signal integrity lives, and its correctness is measured in margin.
Digital — the training and bring-up state machine, lane health and enable state, lane mapping and any remapping, module enable and configuration, the logic driving the sideband, status generation and reporting upward, serialisation and deserialisation where architecturally relevant, configuration registers, and the clock-domain crossings between all of that and the rest of the die.
PHY is a functional layer, not a synonym for analog circuitry.
Two practical consequences. The digital portion is verifiable at RTL — FSMs, masks, counters, and status logic, all amenable to assertions and directed testing. And it is where a specific and nasty class of bug lives: the link that trains perfectly and then corrupts everything (§9), which no amount of analog margin analysis will explain.
3. Bring-Up Is a Sequence, Not an Event
Chapter 4.2 established that reset deassertion is not link readiness. Here is what fills the gap.
Releasing reset gives you logic in a known state and nothing else. Before the link is usable the PHY must establish a control path to the partner, confirm the partner is present and responsive, determine which physical resources are usable, apply configuration, complete training so timing is established, and only then report operational.
The sideband (Chapter 4.4) is what makes the early steps possible: a narrow, separate path that comes up first and carries training, register access, diagnostics, and management. Without it there would be no way to coordinate bringing up the mainband, because the mainband is the thing being brought up. Its detailed protocol belongs to a later chapter; what matters here is that it is a prerequisite, not optional debug wiring.
4. The Bring-Up State Machine
Illustrative architecture states — not UCIe normative state names or training sequence.
// Illustrative PHY bring-up FSM -- not UCIe normative states.
typedef enum logic [2:0] {
PHY_RESET,
PHY_SIDEBAND, // establishing the control path to the partner
PHY_TRAIN, // training the mainband
PHY_CHECK, // confirming usable resources meet the configuration
PHY_ACTIVE, // usable for transport
PHY_RECOVER, // transient fault; re-establishing
PHY_FAILED // gave up; requires intervention
} phy_state_t;
phy_state_t phy_state_q;
always_ff @(posedge clk) begin
if (!rst_n) begin
phy_state_q <= PHY_RESET;
end else begin
unique case (phy_state_q)
PHY_RESET : phy_state_q <= PHY_SIDEBAND;
PHY_SIDEBAND : if (partner_seen) phy_state_q <= PHY_TRAIN;
else if (timeout) phy_state_q <= PHY_FAILED;
PHY_TRAIN : if (train_done) phy_state_q <= PHY_CHECK;
else if (timeout) phy_state_q <= PHY_FAILED;
PHY_CHECK : phy_state_q <= resources_ok ? PHY_ACTIVE : PHY_FAILED;
PHY_ACTIVE : if (link_fault) phy_state_q <= PHY_RECOVER;
PHY_RECOVER : phy_state_q <= PHY_TRAIN;
PHY_FAILED : /* held until reset or software intervention */ ;
default : phy_state_q <= PHY_FAILED;
endcase
end
end
// The single fact the Adapter consumes.
assign phy_link_up = (phy_state_q == PHY_ACTIVE);Architecture. A sequence with prerequisites and escapes. PHY_CHECK exists deliberately: training completing is not the same as enough usable resources existing for the configured link, and separating them makes the failure distinguishable.
State. One enum register plus whatever each state's sub-logic holds. Everything the Adapter needs is compressed into phy_state_q == PHY_ACTIVE.
Cycle behaviour. Reset forces PHY_RESET; each waiting state advances on its prerequisite or escapes on timeout; PHY_RECOVER returns to PHY_TRAIN, never directly to PHY_ACTIVE.
Contract. The Adapter's ADP_WAIT_PHY → ADP_NEGOTIATE transition (Chapter 5.2 §7) depends on this signal being truthful. Reporting active early causes the Adapter to negotiate over a link that cannot carry the exchange.
Failure/DV.
// SAFETY: active requires training complete and sufficient resources.
property p_active_requires_prereqs;
@(posedge clk) disable iff (!rst_n)
(phy_state_q == PHY_ACTIVE) |-> (train_done_q && resources_ok_q);
endproperty
assert property (p_active_requires_prereqs)
else $error("PHY_ACTIVE entered without training and resource prerequisites");
// SAFETY: recovery must retrain -- never jump straight back to active.
property p_recover_retrains;
@(posedge clk) disable iff (!rst_n)
(phy_state_q == PHY_RECOVER) |=> (phy_state_q != PHY_ACTIVE);
endproperty
assert property (p_recover_retrains)
else $error("returned to ACTIVE from RECOVER without retraining");The second catches an optimisation someone will eventually propose — "the fault was transient, just go back to active" — which leaves the link running on timing and resource assumptions that the fault may have invalidated.
5. Lane Health Is Not Lane Enable
Chapter 4.4 introduced these as two facts. Here is why keeping them separate is load-bearing.
// Two independent views of every lane.
parameter int unsigned NUM_LANES = 16;
logic [NUM_LANES-1:0] lane_good_q; // observed: physically usable
logic [NUM_LANES-1:0] lane_enabled_q; // configured: participating in the link
logic [NUM_LANES-1:0] usable_lane_mask;
logic [$clog2(NUM_LANES+1)-1:0] usable_lane_count;
assign usable_lane_mask = lane_good_q & lane_enabled_q;
always_comb begin
usable_lane_count = '0;
for (int i = 0; i < NUM_LANES; i++) begin
usable_lane_count += {{($clog2(NUM_LANES+1)-1){1'b0}}, usable_lane_mask[i]};
end
end
assign resources_ok = (usable_lane_count >= MIN_LANES_REQUIRED);Architecture. lane_good_q is discovered during training — an observation about physical reality. lane_enabled_q is decided by configuration. They arrive from different places at different times and can legitimately disagree: a healthy lane may be disabled by configuration, which is fine. The dangerous direction is an enabled lane that is not good.
State. Two masks plus a derived count. The count is what converts a bitwise physical picture into the single resource fact PHY_CHECK needs.
Cycle behaviour. lane_good_q updates as training progresses and monitoring continues; lane_enabled_q changes only on configuration.
Failure/DV. Transmitting on an enabled-but-unhealthy lane loses or corrupts the bits placed there, producing persistent errors that resemble a marginal channel while actually being a configuration bug.
// SAFETY: never have a lane enabled that is not usable.
property p_no_enabled_bad_lane;
@(posedge clk) disable iff (!rst_n)
(phy_state_q == PHY_ACTIVE) |-> ((lane_enabled_q & ~lane_good_q) == '0);
endproperty
assert property (p_no_enabled_bad_lane)
else $error("an enabled lane is not healthy while the link is active");Note the qualification to PHY_ACTIVE — during training the two masks are legitimately inconsistent, and asserting unconditionally would produce false failures. Scoping a property to the state where its invariant actually applies is a discipline worth carrying generally.
Repair, briefly. Where an architecture supports removing or remapping a bad physical resource, the result must appear upward as changed abstract capability, not as lane detail. The mechanism belongs to a later chapter; the ownership rule is this chapter's.
6. What Must Not Cross Upward
The anti-pattern, in its PHY form:
// BAD: physical lane detail escaping into a higher layer's decision.
assign protocol_degraded = !lane_good_q[7];This makes an upper layer depend on lane 7 existing, on its numbering, and on repair never remapping it. Change the width, change the packaging class (Chapter 4.4: 16 lanes per module standard, 64 advanced), or enable remapping, and the consumer breaks.
The correct structure reduces physical state to abstract facts at the layer that owns it:
// Physical conditions reduced to what higher layers can act on.
assign phy_link_up = (phy_state_q == PHY_ACTIVE);
assign phy_degraded = (usable_lane_count < NOMINAL_LANES);The Adapter consumes those. It never learns which lane failed, and it does not need to — that is physical uncertainty, and resolving it is what this layer is for.
7. Crossing Clock Domains Safely
The PHY is where die-to-die timing meets the rest of the die, so CDC is unavoidable — and it is where genuinely dangerous mistakes hide.
Here is the naive version:
// UNSAFE: capturing an asynchronous signal directly into a local domain.
always_ff @(posedge core_clk) begin
phy_ready_q <= phy_ready_async;
endWhy it fails. If phy_ready_async changes near core_clk's sampling edge, the flop can enter a metastable state — neither 0 nor 1 for an unbounded time — and downstream logic may sample it inconsistently. One consumer sees 1 and another 0 in the same cycle, so the state machine and the datapath disagree about whether the link is up. The symptom is a rare, unreproducible bring-up failure that survives thousands of clean runs.
For a single-bit, level-stable status signal the standard fix is a two-flop synchroniser:
// Two-flop synchroniser: for a SINGLE-BIT, level-stable status signal.
logic phy_ready_meta_q, phy_ready_sync_q;
always_ff @(posedge core_clk) begin
if (!rst_n) begin
phy_ready_meta_q <= 1'b0;
phy_ready_sync_q <= 1'b0;
end else begin
phy_ready_meta_q <= phy_ready_async;
phy_ready_sync_q <= phy_ready_meta_q;
end
endThis is generic CDC practice, not a UCIe requirement, and not the right mechanism for every signal crossing a PHY boundary. What each specific interface requires depends on the implementation and on what the specification defines.
8. The Multi-Bit CDC Trap
The mistake that follows naturally from §7, and it is worse than the problem it tries to solve:
// WRONG: synchronising a multi-bit value bit-by-bit.
logic [NUM_LANES-1:0] lane_good_meta_q, lane_good_sync_q;
always_ff @(posedge core_clk) begin
lane_good_meta_q <= lane_good_async; // each bit resolves independently
lane_good_sync_q <= lane_good_meta_q;
endWhy it fails. Each bit passes through its own synchroniser and resolves independently. If the source value changes from 0011 to 1100 near a sampling edge, different bits may settle on different cycles, so the destination can observe 0000, 1111, 0111, or any other intermediate — a value that never existed at the source. The word is destroyed even though every individual bit is metastability-safe.
For a lane mask that means the receiver may briefly compute a usable-lane count that is wrong in either direction, and a state machine keyed off it can make a decision no consistent view of the source would justify.
Correct approaches for multi-bit data crossing a domain:
- Asynchronous FIFO for streaming data.
- Handshake with a stable payload — the source holds the value steady, synchronises a single-bit request, and the destination samples the (now stable) bus before acknowledging. Only the control bit crosses asynchronously.
- Gray coding, where the value is a counter and only one bit changes per increment.
- Source-synchronous capture where the interface provides a clock alongside the data — the arrangement a forwarded-clock interface uses.
The rule to carry: synchronise control, not data. Get one bit safely across and use it to qualify a bus that is already stable.
DV. Assertions can check the protocol around a synchronised signal — that a consumer waits for the synchronised version, that a handshake's payload is stable while its request is asserted:
// The consumer must use the synchronised status, not the raw one.
property p_uses_synchronised_status;
@(posedge core_clk) disable iff (!rst_n)
consume_link_up |-> phy_ready_sync_q;
endproperty
assert property (p_uses_synchronised_status);But be honest about the limit: SVA does not prove metastability safety. Simulation does not model metastability, and an RTL assertion cannot see it. Structural CDC analysis tools, correct synchroniser inference, and constraint review remain necessary. An assertion suite that passes tells you the logical protocol is right, not that the crossing is safe.
9. Trained Perfectly, Corrupting Everything
The diagnostic case this chapter is built around.
Suppose training completes, phy_link_up is asserted, every configured lane reports healthy, and there is ample electrical margin — and every transport unit fails the Adapter's integrity check.
The cause is digital: the transmit-side lane mapping and the receive-side reassembly disagree (Chapter 4.4 §10). Every bit crosses correctly; they are reassembled in the wrong positions. The physical link is perfect and the data is wrong.
The diagnostic signature is what makes this worth teaching:
| Symptom | Points at |
|---|---|
| Persistent, deterministic errors — essentially every unit fails, immediately after a successful train | Digital: mapping, reassembly, configuration, lane ordering |
| Intermittent, rate- or temperature-dependent errors — occasional failures that worsen with conditions | Analog: margin, signal integrity, a marginal lane |
An engineer who does not know this distinction responds to "every transfer fails" by investigating signal integrity — the most expensive and slowest possible path — when the deterministic character of the failure already indicates a configuration or mapping bug findable in RTL in an afternoon.
The general principle: failure character is diagnostic before failure content is. Deterministic implicates digital; stochastic implicates physical.
10. Timeouts Turn Hangs Into Diagnosable Failures
Every waiting state in §4 needs an escape, because the partner may never respond.
// Illustrative training watchdog. Actual UCIe timeout values are
// specification matters and are not claimed here.
parameter int unsigned TIMEOUT_W = 20;
logic [TIMEOUT_W-1:0] train_timer_q;
logic timeout;
logic in_waiting_state;
assign in_waiting_state = (phy_state_q == PHY_SIDEBAND) || (phy_state_q == PHY_TRAIN);
assign timeout = (train_timer_q == {TIMEOUT_W{1'b1}});
always_ff @(posedge clk) begin
if (!rst_n) begin
train_timer_q <= '0;
end else if (!in_waiting_state) begin
train_timer_q <= '0; // cleared outside waiting states
end else if (!timeout) begin
train_timer_q <= train_timer_q + 1'b1;
end
endArchitecture. Without this, a missing or unresponsive partner produces a link stuck in a waiting state forever, with no error and no diagnostic — indistinguishable from "still working on it".
Cycle behaviour. Increments while waiting, saturates at the limit rather than wrapping (wrapping would restart the wait silently), and clears whenever the machine leaves a waiting state.
Failure/DV. Rather than an unbounded liveness assertion — which is usually unprovable in simulation and needs fairness assumptions in formal — check the mechanics, which are concrete and cheap:
// The timer must clear whenever we are not waiting.
property p_timer_clears_outside_waiting;
@(posedge clk) disable iff (!rst_n)
!in_waiting_state |=> (train_timer_q == '0);
endproperty
assert property (p_timer_clears_outside_waiting);
// A timeout while waiting must drive the failure state.
property p_timeout_drives_failed;
@(posedge clk) disable iff (!rst_n)
(in_waiting_state && timeout) |=> (phy_state_q == PHY_FAILED);
endproperty
assert property (p_timeout_drives_failed);Together these prove the watchdog cannot silently fail to fire — which is the property that actually matters, and it is checkable.
11. Module Enable, Tied to State
Chapter 4.4 asserted that a disabled module must not transmit. With a state machine in hand, the property gets sharper:
// A module may transmit only if it is enabled AND the link is active.
generate
for (genvar m = 0; m < NUM_MODULES; m++) begin : g_mod
property p_no_tx_unless_enabled_and_active;
@(posedge clk) disable iff (!rst_n)
module_tx_valid[m] |-> (module_enable_q[m] && (phy_state_q == PHY_ACTIVE));
endproperty
assert property (p_no_tx_unless_enabled_and_active)
else $error("module %0d transmitted while disabled or link not active", m);
end
endgenerateThe addition matters. A module can be enabled by configuration while the link is training, and transmitting then injects traffic into a link the partner is not yet ready to receive on — a bring-up failure that looks like a training problem and is actually a gating bug.
12. Datapath Shape
Transmit maps a wide transport unit onto lanes; receive does the inverse. The asymmetry is the interesting part.
Transmit is deterministic: take the transport unit, slice it by the fixed mapping (Chapter 4.4 §10), hand each slice to its lane's transmit logic. Little state beyond the mapping itself.
Receive must reconstruct. It samples per-lane, aligns the lanes with one another, and reassembles a transport unit — which requires alignment state that transmit has no counterpart for, because lanes may not present their data with identical timing. This is the same asymmetry Chapter 4.3 identified: transmit adds deterministically, receive recovers and can fail. The specific alignment mechanism belongs to a later chapter.
13. Verifying and Debugging a PHY
Verification structure. FSM: legal transitions only, prerequisites enforced, illegal states recover, every waiting state escapes on timeout. Lane and module state: enable/health consistency scoped to active, mapping determinism, disabled resources silent. CDC: structural analysis with tools, plus assertions on the logical protocol around synchronised signals — never claiming SVA proves metastability safety. Datapath: known patterns, deliberate lane swaps, a stuck lane, a disabled lane participating. Error injection: transient versus persistent lane faults, training failure, loss of the control path. End-to-end: the Adapter observes correct abstract status and never sees raw lane detail.
If the Adapter says the link never became operational:
- Did the PHY leave reset?
- Did the control path (sideband) become usable — was the partner seen?
- Did the training FSM advance past
PHY_SIDEBAND, and pastPHY_TRAIN? - Which prerequisite is not asserting —
partner_seen,train_done, orresources_ok? - Are enough lanes healthy for
MIN_LANES_REQUIRED? - Is the module configuration valid and consistent with what trained?
- Is the status crossing into the consuming domain correctly synchronised?
- Did a timeout fire and move the machine to
PHY_FAILED? - Is
phy_link_upgenerated from the right state? - Did the Adapter actually observe the synchronised version?
If the link is operational but the data is wrong, the order is different and much shorter — start from §9's signature:
- Are the errors deterministic (every unit) or intermittent? Deterministic goes digital, intermittent goes analog.
- Lane mapping: does the receive reassembly match the transmit slicing?
- Lane ordering and any remapping applied after repair.
- Is a disabled lane participating, or an enabled lane unhealthy?
- Receive alignment across lanes.
- Only then: signal-integrity margin.
14. Common Misconceptions
15. Understanding Check
16. Summary and What Comes Next
The PHY converts an unreliable physical possibility into a digital resource the Adapter can treat as operational — and it is a functional layer, not a synonym for analog circuitry. Its digital half holds the bring-up FSM, lane health and enable state, mapping, module configuration, status generation, timeouts, and clock-domain crossings, all of it ordinary synthesisable RTL and all of it where bring-up debugging happens.
Bring-up is a sequence with prerequisites, made possible by the sideband coming up first, with a CHECK step separating "training completed" from "enough usable resources exist" and a timeout escape from every waiting state so a missing partner becomes a diagnosable failure rather than a hang. Recovery re-enters training rather than jumping back to active.
Lane health and lane enable are different facts — one observed, one decided — and the dangerous disagreement is an enabled lane that is not good, asserted only where the invariant applies. Physical detail is reduced here into abstract status; lane identity never crosses upward.
CDC is where the sharpest traps are. A two-flop synchroniser handles a single-bit, level-stable signal and nothing more; synchronising a bus bit-by-bit produces values that never existed at the source. Synchronise control, not data — and remember that SVA proves the protocol around a crossing, never its metastability safety.
Finally, the diagnostic habit: deterministic failures implicate digital mapping and configuration; intermittent ones implicate analog margin. A link that trains flawlessly and corrupts every transfer is a digital bug, and reading that signature correctly is worth days of debugging time.
All three layers have now been examined individually. What remains is to place them side by side and remove the last ambiguity about which function belongs where:
- 5.4 — Layer Responsibilities — per-layer ownership of payload, framing, reliability, and bring-up, compared directly.
Browse the full path on the UCIe tutorials index.