UCIe · Module 4
UCIe Layered Architecture
Where UCIe responsibilities belong and what contract holds between Protocol, Adapter, and PHY — state ownership, backpressure and decoupling, the combinational-path trap, reset versus operational, and the assertions that hold each boundary together.
Chapter 4.1 gave the picture: three layers, two specified boundaries, mirrored across a link. This chapter turns that picture into architecture — the decisions an engineer actually makes when building or verifying one of these layers.
The question changes from what is the stack? to where exactly should each responsibility live, and what must be true at each boundary? Those are different questions, and the second one has consequences you can see in waveforms: state in the wrong layer, backpressure that does not propagate, a combinational path that will not close timing, or a protocol implementation that has to be redesigned because a physical detail leaked upward.
1. Each Layer Owns One Kind of Uncertainty
The sharpest framing of the split, and the one to reason with when deciding where a new function belongs:
- Protocol Layer owns uncertainty in transaction semantics — what a payload means, what ordering must hold, what a response signifies.
- D2D Adapter owns uncertainty in reliable transport across the link — whether the payload arrived intact, in order, without overrunning the receiver, and whether the link is in a state that can carry it.
- Physical Layer owns uncertainty in the physical channel — whether bits survive the package, whether lanes are working, whether the link has trained.
Use it as a placement test. Faced with "where does function X belong?", ask which uncertainty it resolves. Error detection over a transport unit resolves channel-induced uncertainty about data integrity — Adapter. Deciding whether two requests may complete out of order resolves semantic uncertainty — Protocol. Deciding whether a lane is usable resolves physical uncertainty — PHY.
2. Protocol Layer Responsibilities
The Protocol Layer implements a protocol: it deals in that protocol's transactions and messages, its ordering rules, and its semantics. UCIe carries established protocols — PCIe and CXL — along with a streaming/raw option for traffic that suits neither.
What it should not do is manage the physical link. Lane training, bump or lane repair, electrical signalling, clock recovery, and physical link state are not its concerns, and §8 shows concretely what breaks when they become so.
One responsibility genuinely does move depending on configuration, and getting this right early prevents a common misconception. In the mode where the Adapter handles flit framing, the Adapter inserts and checks CRC and can provide link-level retry. In a raw mode, the payload is populated entirely by the Protocol Layer, and error protection — CRC, retry, and any forward error correction — becomes the Protocol Layer's responsibility. So:
The layers are fixed; the assignment of some responsibilities depends on the configured mode.
That is worth internalising because it means "which layer does error protection?" has the answer it depends on the mode, and an engineer who memorised a single fixed table will be wrong half the time.
3. D2D Adapter Responsibilities
The Adapter is the layer that makes the link usable by something that does not want to know how the link works. Its documented responsibilities include:
- Link state coordination and bring-up. Running the higher-level link state machine and getting the link from reset to operational, in cooperation with the PHY below.
- Parameter negotiation with the remote partner. Establishing the operating point both sides support — the capability-intersection problem of Chapter 3.2, now a concrete responsibility with an owner.
- Reliability, when configured. CRC insertion and checking, and link-level retry, so an upper layer does not have to treat the physical channel as unreliable.
- Protocol arbitration and multiplexing. Where more than one protocol shares a link, deciding what goes when and keeping the streams separated.
- Power-state coordination with the remote partner, since low-power states are only meaningful if both ends agree.
Notice the common property: every one of these requires knowing about the partner and the link, and none requires knowing what the payload means. That is precisely why they belong together in a layer between the two.
The details of each — retry mechanics, flit formats, the state machine's states, negotiation encoding — belong to later modules. What belongs here is the ownership claim and its consequence for where state lives (§5).
4. Physical Layer Responsibilities — and Why "PHY" Is Not "Analog"
The Physical Layer owns lanes and modules, electrical signalling, clocking, training the link into a usable state, and the sideband path used to coordinate that.
Now the correction that matters most to an RTL engineer, because the word misleads:
A PHY is a functional layer, not a synonym for analog circuitry.
A die-to-die PHY typically contains a substantial amount of digital logic: the training state machine that walks the link from reset to operational, lane mapping and repair state, serialisation and deserialisation, clocking control, status reporting upward, and the logic driving the sideband. The analog front end is one part of it, and often not the part with the most state.
This matters practically. If you assume "PHY = analog", you will assume there is nothing to verify at RTL level, no state machine to reason about, and nothing to model — all three wrong. The digital portion of a PHY is regular RTL with regular bugs, and it is where a large share of bring-up debugging happens.
5. Where State Lives
The most useful single table in this module. State placement follows from §1: the layer that owns an uncertainty owns the state that resolves it.
| State | Owner | Why |
|---|---|---|
| Transaction/message state, ordering context | Protocol | Semantic — only Protocol knows what the payload means |
| Transport buffer occupancy at a boundary | Adapter (and each boundary) | Whoever accepts a payload must retain it |
| Retry/reliability bookkeeping | Adapter in the mode where it provides CRC and retry; Protocol in raw mode | Mode-dependent, per §2 |
| Remote-partner negotiated parameters | Adapter | It runs the negotiation |
| Higher-level link state | Adapter, coordinating with PHY | It owns bring-up and link-state coordination |
| Lane training state | PHY | Physical uncertainty |
| Lane health / repair state | PHY | Physical uncertainty |
| Serialisation and clocking state | PHY | Physical uncertainty |
Two honest notes. The exact boundary between "higher-level link state" in the Adapter and physical link state in the PHY is a specification detail with real nuance, and later modules treat it properly — the architectural point here is that both exist and they are not the same state. And the retry row is genuinely mode-dependent rather than a simplification.
6. The Boundary as an Interface
Both internal boundaries carry the same shape — payload down, backpressure up — so it is worth naming that shape once.
Illustrative transport abstraction — not UCIe normative interface contents. The real FDI and RDI carry considerably more than this; the reduction to payload plus backpressure isolates the behaviour this chapter is about.
interface layer_stream_if #(parameter int unsigned W = 256) (input logic clk);
logic [W-1:0] data;
logic valid; // driven by the upstream (producing) side
logic ready; // driven by the downstream (consuming) side
modport up (output data, output valid, input ready);
modport down (input data, input valid, output ready);
endinterfaceArchitecture. One abstraction reused at Protocol↔Adapter and Adapter↔PHY, which is why the same reasoning and the same assertions apply at both.
Contract. A transfer occurs on a cycle where valid && ready. While valid && !ready, data and valid must hold. Whoever asserts ready takes responsibility for retaining what it accepts.
Why modports matter here. They encode direction of ownership: the upstream side drives payload and qualification, the downstream side drives acceptance. Getting that backwards is a whole class of integration bug that the type system can prevent.
7. The Combinational-Path Trap
An architecture lesson that costs real schedule time when missed.
It is tempting to compute readiness combinationally through the whole stack — each layer's ready derived directly from the layer below, and each layer's valid derived from above:
// RISKY COMPOSITION: readiness threaded combinationally through layers,
// and valid derived from ready in the same cycle.
assign proto_ready = adapter_cond && phy_ready; // ready flows up, combinationally
assign phy_valid = proto_valid && proto_ready; // valid derived from readyTwo distinct problems, and they are worth separating.
A long combinational path. phy_ready now feeds proto_ready, which the Protocol Layer's logic consumes to decide what it does this cycle. Across three layers — potentially three separately designed blocks, physically placed apart — this becomes a timing path spanning the endpoint. It will be among the first paths to fail closure, and it constrains where the blocks can be placed. Chapter 2.6's point that physical reality constrains architecture applies inside the die too.
A loop risk when combined carelessly. If the downstream ready is computed from the upstream valid — a plausible optimisation, since "I can accept if you are not sending" seems reasonable — while the upstream valid is computed from ready, the result is a combinational cycle. Each signal depends on the other within the same cycle, and the design either fails elaboration, fails timing, or simulates unpredictably.
To be precise about what is not being claimed: ready/valid does not have to be registered, and a combinational ready is entirely legitimate in many designs. The discipline is to be deliberate about how far a combinational path is allowed to run and which direction each signal is derived from — not to ban combinational readiness.
8. Decoupling With Storage at the Adapter
The standard remedy is to give the Adapter explicit state at its boundary. A skid buffer is the canonical structure: it accepts from above even when the layer below is stalling, so the upstream readiness need not depend combinationally on the downstream one.
// Adapter-side decoupling buffer. Breaks the combinational path between
// the PHY-facing ready and the protocol-facing ready, and gives the
// Adapter explicit ownership of an in-flight payload.
logic [W-1:0] out_data_q, skid_data_q;
logic out_valid_q, skid_valid_q;
always_ff @(posedge clk) begin
if (!rst_n) begin
out_valid_q <= 1'b0; out_data_q <= '0;
skid_valid_q <= 1'b0; skid_data_q <= '0;
end else begin
// Output stage advances when it is empty or being drained.
if (!out_valid_q || dn_ready) begin
if (skid_valid_q) begin
out_valid_q <= 1'b1;
out_data_q <= skid_data_q;
skid_valid_q <= 1'b0;
end else begin
out_valid_q <= up_valid;
out_data_q <= up_data;
end
end
// Downstream stalled while upstream is still offering: absorb one beat.
if (out_valid_q && !dn_ready && up_valid && !skid_valid_q) begin
skid_valid_q <= 1'b1;
skid_data_q <= up_data;
end
end
end
assign up_ready = !out_valid_q || dn_ready || !skid_valid_q;
assign dn_valid = out_valid_q;
assign dn_data = out_data_q;Architecture. The Adapter now holds up to two beats, decoupling the two boundaries so a PHY stall does not have to propagate combinationally to the Protocol Layer in the same cycle.
State. out_valid_q/out_data_q is the beat presented downstream; skid_valid_q/skid_data_q absorbs one more when the downstream stalls mid-flow. That is the entire state, and it is genuinely the Adapter's — not the Protocol Layer's and not the PHY's.
Cycle behaviour. When the output stage is empty or draining, it reloads from the skid stage if occupied, otherwise directly from upstream. When the output is stalled and upstream is still offering, the skid stage captures one beat so up_ready need not drop in the same cycle dn_ready did.
Contract. Both boundaries keep the §6 rule: a presented beat is held until accepted. The buffer is what lets the Adapter honour it toward both neighbours simultaneously.
Failure/DV. Get the up_ready expression wrong and you either accept when full (§9, overflow → corruption) or refuse when space exists (throughput loss, no correctness failure). Get the reload priority wrong — taking from upstream while the skid holds an older beat — and you reorder payloads, which is a correctness failure the assertions in §9 do not catch and which needs a scoreboard.
9. Two Assertions the Buffer Needs
Illustrative buffer invariants — conceptual, not UCIe normative properties.
// Overflow: never claim readiness upstream when there is nowhere to put
// the beat. Violation means an accepted payload overwrites an
// unconsumed one -- silent corruption, no wire-level rule broken.
property p_no_accept_when_full;
@(posedge clk) disable iff (!rst_n)
(out_valid_q && skid_valid_q && !dn_ready) |-> !up_ready;
endproperty
assert property (p_no_accept_when_full)
else $error("adapter accepted a beat with no storage available");
// Underflow: never present a beat downstream that the buffer does not
// hold. Violation means transmitting stale or undefined payload.
property p_no_send_when_empty;
@(posedge clk) disable iff (!rst_n)
dn_valid |-> out_valid_q;
endproperty
assert property (p_no_send_when_empty)
else $error("adapter asserted valid downstream with an empty output stage");The overflow property catches the dangerous case: the buffer says "send me more" while both stages are occupied and nothing is draining. The beat that arrives overwrites one that was never consumed — the payload count stays plausible and the contents are wrong, which is the hardest class of bug to trace back. The underflow property is cheaper insurance against a control bug presenting garbage as valid data.
10. Reset Deasserted Is Not Link Operational
A distinction that causes real bring-up bugs, and the layered form of Chapter 3.2's reset argument.
rst_n rising means each layer's logic has left reset. It does not mean the link works. Between those two states sit: the PHY training the physical link, the Adapter running bring-up and negotiating parameters with the remote partner, and both sides agreeing an operating point. Until that completes, there is nothing for a payload to travel over.
So the Protocol Layer must not send merely because reset has deasserted. It needs an abstracted operational indication:
// Illustrative adapter-facing link state -- NOT UCIe normative states.
typedef enum logic [1:0] {
ADP_RESET, // out of reset, nothing established
ADP_WAIT_PHY, // waiting for the physical link to become usable
ADP_READY, // negotiated and usable for protocol traffic
ADP_ERROR // fault detected; traffic not permitted
} adapter_state_t;
adapter_state_t adp_state_q;
always_ff @(posedge clk) begin
if (!rst_n) begin
adp_state_q <= ADP_RESET;
end else begin
unique case (adp_state_q)
ADP_RESET : adp_state_q <= ADP_WAIT_PHY;
ADP_WAIT_PHY : if (phy_link_up) adp_state_q <= ADP_READY;
ADP_READY : if (link_fault) adp_state_q <= ADP_ERROR;
ADP_ERROR : if (recovered) adp_state_q <= ADP_WAIT_PHY;
default : adp_state_q <= ADP_ERROR;
endcase
end
end
// The single abstracted fact the Protocol Layer is allowed to consume.
assign link_operational = (adp_state_q == ADP_READY);Architecture. The Adapter converts "the PHY says the physical link is up" plus its own negotiation status into one boolean the layer above can act on. That conversion is the abstraction.
State. One enum register. Everything the Protocol Layer needs to know about a multi-step bring-up is compressed into whether it equals ADP_READY.
Cycle behaviour. Reset forces ADP_RESET; the machine advances only when the PHY reports the link usable; a fault moves to ADP_ERROR and blocks traffic until recovery returns it to waiting.
Contract. The Protocol Layer's readiness must be gated on this, not on reset:
assign up_ready = link_operational && adapter_has_space;Failure/DV. Without the gate, protocol traffic is accepted during bring-up and lost exactly as in Chapter 4.1 §5. This property catches it:
// No protocol traffic may be accepted before the adapter is ready.
property p_no_accept_before_adapter_ready;
@(posedge clk) disable iff (!rst_n)
(adp_state_q != ADP_READY) |-> !(up_valid && up_ready);
endproperty
assert property (p_no_accept_before_adapter_ready)
else $error("traffic accepted before the adapter reached ADP_READY");11. Abstracting Link Status — and the Anti-Pattern
Here is the architectural mistake this chapter most wants you to recognise, because it looks like a sensible optimisation.
// BAD ARCHITECTURE: protocol behaviour derived from raw PHY lane state.
always_ff @(posedge clk) begin
if (!rst_n) protocol_mode_q <= MODE_NORMAL;
else if (!phy_lane3_ok) protocol_mode_q <= MODE_DEGRADED;
endIt is not wrong in the sense of producing an immediate bug — it may work in the design where it was written. It is wrong architecturally, in four compounding ways:
- The Protocol Layer now knows the PHY's implementation. It knows there is a lane 3. A PHY with a different lane count, different numbering, or a repair mechanism that transparently remaps lanes breaks it.
- Physical changes force protocol redesign. The independence that justified the layering is gone: re-implementing the PHY for a different package now requires touching protocol logic.
- The verification boundary collapses. The Protocol Layer can no longer be verified against an abstract model of the layer below, because it depends on details only a real PHY exposes.
- Reuse suffers. This Protocol Layer is now bound to this PHY family rather than to a specification-defined boundary.
The correct structure converts physical conditions into a defined abstract capability or status at the layer that owns them:
// PHY/Adapter reduce physical conditions to something the layer above
// can act on without knowing why.
assign link_degraded = (usable_lane_count < NOMINAL_LANES);
assign link_operational = (adp_state_q == ADP_READY);The Protocol Layer consumes link_operational and, if the architecture defines one, link_degraded. It never learns which lane failed, and it should not — that is the PHY's uncertainty to own, and reducing it to an abstract status is exactly what a layer is for.
12. A Cycle Walkthrough With Stalls
Tie the pieces together. Ownership sequence, not a latency claim.
- Cycle 0. Reset has deasserted;
adp_state_q == ADP_WAIT_PHY, solink_operationalis low andup_readyis low. The Protocol Layer assertsup_validwith a payload and holds it — this is the contract working: it cannot be accepted, so it is not lost. - Cycle 1.
phy_link_upasserts. On this edgeadp_state_qbecomesADP_READY, solink_operationalrises. The buffer is empty, soup_readyrises. - Cycle 2.
up_valid && up_ready— the transfer fires.out_valid_qsets andout_data_qcaptures the payload. The Adapter now owns it. - Cycle 3. The PHY applies backpressure:
dn_readylow.out_valid_qholds,out_data_qis stable. The Protocol Layer offers another payload; the skid stage absorbs it, soup_readyneed not fall this cycle. - Cycle 4. Both stages are occupied and
dn_readyis still low, soup_readyfalls — correctly refusing a third beat. This is the momentp_no_accept_when_fullprotects. - Cycle 5.
dn_readyrises. The output stage drains, reloads from the skid stage, andup_readyrises again.
Every state change is on a clock edge, in a named register, owned by a named layer. That is what "the architecture is real" means in practice.
13. Verification Architecture and a Debug Checklist
The layering pays off twice: once in design, once in debug.
Environments. A protocol environment drives real protocol traffic and terminates it with an abstract Adapter model. An adapter environment sources traffic from above and terminates below with a PHY model, and is where stall, error-injection, and buffer-corner testing belongs. A PHY environment covers training, transport, and status reporting. An end-to-end environment connects two full endpoints. Each uses models in place of the layers it is not testing, which is what keeps them tractable.
A durable debug checklist. When a payload sent by one die's Protocol Layer never arrives at the other's, walk the stack rather than guessing:
- Did the local Protocol Layer actually present it —
up_validasserted with the expected payload? - Did the Adapter accept it — did
up_valid && up_readyfire, and did occupancy set? - Did the Adapter present it downstream —
dn_validwith the expected transport unit? - Did the PHY accept and transmit it?
- Did the remote PHY receive it and pass it upward?
- Did the remote Adapter accept, check, and reconstruct it?
- Did the remote Protocol Layer accept it?
The first step that answers no localises the defect to one layer or one boundary. Each check is observable at a boundary the architecture already defines — which is the practical reason specified inter-layer interfaces are worth having even inside a single vendor's endpoint.
14. Layer-Boundary Invariants Worth Holding
Collecting the contract in one place:
Protocol ↔ Adapter. Payload held stable while stalled. No acceptance unless the Adapter can retain it — storage, not optimism. No acceptance before the link is operational. Payload semantics preserved unchanged: lower layers may reframe, protect, and retransmit, never reinterpret.
Adapter ↔ PHY. No transmission before the physical link is usable. No overflow — never accept without storage. No underflow — never assert valid without a beat. Preserve the ordering the contract requires. Propagate errors and status upward through the defined abstraction rather than by exposing raw physical state.
These are architectural invariants for a layered transport, expressed at the level this chapter teaches. The specification's own normative requirements at FDI and RDI are richer and belong to the modules that cover those interfaces in detail.
15. Common Misconceptions
16. Understanding Check
17. Summary and What Comes Next
Each layer owns one kind of uncertainty — Protocol owns semantics, the Adapter owns reliable transport across the link, the PHY owns the physical channel — and that is the test for where a new function belongs. State follows ownership: transaction context in Protocol, boundary buffers and negotiated parameters and higher-level link state in the Adapter, lane training and health and serialisation in the PHY. Reliability bookkeeping is mode-dependent — the Adapter's when it provides CRC and retry, the Protocol Layer's in raw mode — so the layers are fixed while some responsibility assignments are not.
Two corrections matter for implementers. A PHY is a functional layer, not analog circuitry — it holds training state machines, lane mapping and repair, serialisation, clocking control, and sideband logic, all of it regular RTL. And the Adapter is not glue: it is the most state-rich layer in the stack.
At the boundaries, the contract is payload down, backpressure up, status abstracted upward. Threading readiness combinationally through every layer creates an endpoint-spanning timing path and, combined carelessly with valid derived from ready, a combinational loop — so storage at the Adapter both makes readiness honest and decouples the boundaries. Guard it with p_no_accept_when_full and p_no_send_when_empty, and remember that local invariants prove the boundary honest without proving ordering.
Finally, reset deasserted is not link operational. PHY training, adapter bring-up, and partner negotiation sit between them, so readiness must be gated on an abstracted status — and the Protocol Layer should consume link_operational, never phy_lane3_ok, because reducing physical conditions to abstract status is the entire job of a layer.
The boundaries are now sharp. Next the curriculum walks the stack once more as a complete protocol path, layer by layer, with the interfaces between them treated properly rather than abstracted:
- 4.3 — The UCIe Protocol Stack — each layer's responsibility as one continuous stack, and the inter-layer interfaces in their own right.
Browse the full path on the UCIe tutorials index.