UCIe · Module 3
The Interoperability Problem
What two independently designed dies must actually agree on before they can communicate — handshake semantics, payload invariants, reset and link state, capability intersection, error semantics, and ordering — with illustrative RTL and assertions.
Chapter 3.1 ended on a claim: two dies can perform equivalent jobs and still have no basis for working together. That is the kind of statement a reader nods at and cannot use. This chapter makes it concrete, and it does so at the level where interoperability actually succeeds or fails — signals, state, and invariants. By the end you should be able to look at two interface implementations that each pass their own verification and say precisely why connecting them will deadlock, lose data, or corrupt ordering.
This is the last chapter before the curriculum turns to standards and then to UCIe itself, and it is deliberately the most implementation-grounded of Module 3. The reason is simple: "interoperability" is meaningless as a slogan and extremely precise as an engineering property. The precision is the lesson.
1. Two Links That Look Compatible and Are Not
Take the smallest possible case. Two dies from different vendors, each with a die-to-die interface:
- Die A produces requests over its interface. Transmit lanes, receive lanes, a clock, data.
- Die B consumes requests over its interface. Transmit lanes, receive lanes, a clock, data.
Suppose the physical side genuinely matches — same lane count, same signalling rate, same direction assignment, compatible electrical behaviour. Wire them together and bits cross successfully in both directions. Measured on an oscilloscope, this link works.
It nevertheless fails immediately, and the reasons are all above the physical layer:
- Die A drives a control field in the first cycle of a transfer that Die B interprets as belonging to a different field.
- Die A treats a transfer as complete the moment it observes acceptance; Die B expects the request to stay asserted until it signals completion.
- Die A begins issuing traffic a few cycles after reset; Die B is not usable until it has completed initialisation and exchanged capabilities.
- Die A expects a failed transfer to be retried; Die B logs an error and continues.
Every one of those is a disagreement about meaning, not about voltage.
A wire carrying bits is not an interface. It becomes an interface only when both sides attach the same meaning to those bits.
2. Compatibility Is a Stack of Contracts
Group the required agreements by layer, because it clarifies both what can go wrong and why fixing one layer is insufficient.
Electrical and physical. Signalling scheme, electrical behaviour, lane organisation and ordering, clocking arrangement, and the channel and reach the design assumes — which, per Chapter 2.6, includes assumptions about the package the link will run over. Get this wrong and bits do not arrive.
Link behaviour. Initialisation and bring-up, framing (where a unit of data begins and ends), flow control and backpressure, ordering guarantees the link provides, error detection and whatever recovery follows, and coordinated power-state behaviour. Get this wrong and bits arrive but transfers do not.
Transaction meaning. What a request is, what a response is, how addresses are interpreted, which transaction types exist, and what ordering the consumer may assume between them. Get this wrong and transfers complete but the system computes incorrectly.
Management. Discovery (how each side learns what the other is), configuration, capability exchange, status reporting, and error reporting. Get this wrong and the link may never reach a usable state, or may reach one silently degraded.
Two consequences follow. First, a mismatch at any single layer is sufficient to break the interface — there is no partial credit. Second, this is why Chapter 3.1's point about AIB mattered: an open, genuinely useful specification for the first layer leaves the other three unaddressed.
3. The Same Wires, Two Different Contracts
Now the concrete case, because this is where the abstraction becomes checkable. Both endpoints below are internally correct. Neither has a bug. Connected, they fail.
Illustrative RTL model — not UCIe specification signal naming. The point is the class of mismatch, not any real interface.
Endpoint A implements a one-cycle transfer: a beat moves on any cycle where both sides are asserting.
// Endpoint A: a transfer completes on ANY cycle where valid and ready
// are both high. After that cycle the sender is free to move on.
logic fire;
logic [BEATS_W-1:0] beats_q;
assign fire = valid_i && ready_o;
always_ff @(posedge clk) begin
if (!rst_n) beats_q <= '0;
else if (fire) beats_q <= beats_q + 1'b1;
endEndpoint B implements a two-phase exchange: the request must remain asserted until B signals it has been consumed, and B expects the request to then drop before another can begin.
// Endpoint B: expects req to be held until ack, where ack means the
// request has been CONSUMED (not merely observed), and then expects
// req to drop before the next request begins.
typedef enum logic [1:0] { IDLE, BUSY, ACKED } b_state_t;
b_state_t state_q;
always_ff @(posedge clk) begin
if (!rst_n) state_q <= IDLE;
else begin
unique case (state_q)
IDLE : if (req_i) state_q <= BUSY;
BUSY : if (work_done) state_q <= ACKED; // ack asserted here
ACKED : if (!req_i) state_q <= IDLE; // waits for req to fall
endcase
end
end
assign ack_o = (state_q == ACKED);Trace the connection, mapping A's valid onto B's req and B's ack onto A's ready — which is exactly the mapping an integrator would reach for, since the signals appear to correspond:
- A asserts
valid. B entersBUSY. - B finishes and asserts
ack. A observesreadyhigh, counts the beat, and deassertsvalidon the next cycle — its contract says the transfer is done. - B is in
ACKEDwaiting forreqto fall. It does fall, so B returns toIDLE. So far, accidentally fine. - Now A presents a second beat and, because B happened to be ready, A treats it as accepted in a single cycle. But B was in
IDLEand needs a fullBUSYphase. The beat A counted was never consumed.
The failure is silent data loss, and depending on relative timing the same mismatch can instead produce deadlock — if A holds valid for exactly one cycle and B misses the edge, B waits forever in IDLE while A waits for a ready that will not come.
Name what actually differs, because these are the four questions any transport contract has to answer:
- Who owns forward progress? In A, either side may stall and the transfer completes when both agree. In B, the receiver drives the phases and the sender must follow.
- When is a transfer complete? A: the cycle both are asserted. B: when
ackindicates consumption, followed by request withdrawal. - When may the sender change the payload? A: immediately after the accepting cycle. B: not until the full handshake has closed.
- What does the acceptance signal mean? A treats it as "you may proceed". B means "I have consumed this".
Both endpoints are self-consistent. Self-consistency is not compatibility, and no amount of endpoint-level verification detects this, because each endpoint passes against its own assumptions.
4. The Invariant Behind ready/valid
Take just one of those four questions — when the sender may change the payload — and make it precise, because it is the clearest example of an invariant that must be shared rather than merely implemented.
Here is a sender that is wrong in a way that is easy to write and hard to see:
// INCORRECT — advances the payload whenever a new beat is available,
// without checking whether the receiver accepted the previous one.
always_ff @(posedge clk) begin
if (!rst_n) begin
valid_q <= 1'b0;
data_q <= '0;
end else if (have_new_beat) begin
valid_q <= 1'b1;
data_q <= next_data; // BUG: overwrites a beat that was never accepted
end
endIf the receiver asserted backpressure — ready low — the beat sitting in data_q has not been taken. Overwriting it destroys it. The receiver eventually accepts whatever is present, so the transfer count is right and the contents are wrong. This is among the nastiest classes of interface bug: no protocol violation is visible on the signals, and the corruption surfaces far downstream.
The corrected sender holds both the payload and the qualifier while stalled:
// CORRECTED — a presented beat is held until it is accepted.
always_ff @(posedge clk) begin
if (!rst_n) begin
valid_q <= 1'b0;
data_q <= '0;
end else if (valid_q && !ready_i) begin
valid_q <= 1'b1; // stalled: hold the beat unchanged
data_q <= data_q;
end else if (have_new_beat) begin
valid_q <= 1'b1;
data_q <= next_data;
end else begin
valid_q <= 1'b0;
end
endAnd here is the same rule expressed so that a tool can check it:
// Illustrative transport invariant — not a UCIe normative property.
property p_payload_stable_under_backpressure;
@(posedge clk) disable iff (!rst_n)
(valid_q && !ready_i) |=> (valid_q && $stable(data_q));
endproperty
assert property (p_payload_stable_under_backpressure)
else $error("payload or valid changed while the receiver was stalling");Read the assertion as English: if I am presenting a beat and you are not accepting it, then on the next cycle I am still presenting it and the payload has not changed. That single line is the contract the receiver depends on. A receiver written against it may legitimately register the payload later, or take several cycles to decide, precisely because it has been promised the data will still be there.
This is the mechanism by which specifications become useful:
An interoperability requirement is an invariant both endpoints can be independently checked against. The assertion is the contract in executable form.
Note what makes this an interoperability property rather than merely a good design rule. The sender could implement any internal buffering it likes; the receiver could be a register, a FIFO, or a state machine. Neither cares about the other's implementation. What they must share is this invariant — and if the specification does not state it, one implementer will assume it and another will not.
5. Reset Is Part of the Protocol
Reset feels like an implementation detail. Across a die boundary it is not.
Consider the mismatch:
- Side A treats its interface as usable shortly after reset deasserts. Its designers reasoned that reset puts the logic in a known state, so it is ready.
- Side B requires a sequence: reset deassertion, internal initialisation, an exchange establishing what each side supports, and only then a usable link.
Connect them and A issues traffic into a partner that is not listening. The traffic is lost or, worse, is partially interpreted by B's initialisation logic. Nothing on either side is defective; the two have different models of when the interface exists.
Which means bring-up ordering has to be stated: which side may begin, how each learns the other is ready, what happens if one side resets while the other keeps running, and whether the link can recover without resetting everything. A useful way to hold it:
Reset and bring-up semantics are part of the interface contract, not preparation for it.
6. Link State Must Mean the Same Thing on Both Sides
Reset mismatch is a special case of a more general problem: both sides carry a notion of link state, and those notions have to correspond.
// Illustrative link state model — NOT UCIe's state machine or naming.
typedef enum logic [1:0] {
ST_RESET, // held in reset, interface unusable
ST_INIT, // initialising and exchanging capabilities
ST_READY, // usable for traffic
ST_ERROR // fault detected, traffic not permitted
} link_state_t;
link_state_t state_q;Four states, and already several ways for two implementations to disagree. Does ST_READY mean "my side is ready" or "both sides are ready"? Is entering ST_READY triggered by a fixed delay, or by an explicit indication from the partner? On a fault, does a side enter ST_ERROR and stop, or continue with degraded operation? Can it leave ST_ERROR without a full reset?
If one implementation reaches ST_READY after a timeout while the other waits for an explicit partner indication, the first sends into a partner that has not finished initialising. Both are reasonable designs; together they do not work.
Two properties worth stating for any such model:
// Illustrative properties — generic link-state invariants, not UCIe rules.
// No traffic may be accepted before the link is ready.
property p_no_traffic_before_ready;
@(posedge clk) disable iff (!rst_n)
(valid_q && ready_i) |-> (state_q == ST_READY);
endproperty
assert property (p_no_traffic_before_ready);
// Initialisation cannot be skipped: RESET must not jump straight to READY.
property p_no_skip_init;
@(posedge clk) disable iff (!rst_n)
(state_q == ST_RESET) |=> (state_q != ST_READY);
endproperty
assert property (p_no_skip_init);The first is a safety property protecting the partner from premature traffic. The second forbids a shortcut an optimising implementation might otherwise take — and it is exactly the kind of rule that must be written down, because an implementer who skips initialisation when they believe it is unnecessary produces silicon that works with their own test partner and fails with someone else's.
7. Capability Intersection: Finding a Mutually Supported Mode
Two endpoints can be individually valid and still have no way to operate together, because compatibility is not about either endpoint — it is about the overlap between them.
Suppose one chiplet supports certain widths and modes, and the other supports a different set. If nothing is common, there is no operating point, and the correct behaviour is to refuse to bring the link up rather than to run in a configuration neither implements.
// Illustrative capability negotiation — conceptual, not UCIe's mechanism.
localparam int unsigned CAP_W = 8;
logic [CAP_W-1:0] local_caps; // what this die supports
logic [CAP_W-1:0] remote_caps_q; // captured from the partner during INIT
logic [CAP_W-1:0] common_caps;
logic compatible;
assign common_caps = local_caps & remote_caps_q;
assign compatible = (common_caps != '0);The bitwise AND is the idea, stripped of everything else: a mode is usable only if both sides support it. Real negotiation adds structure — grouping capabilities, selecting a preferred mode from the common set, ordering preferences, and handling asymmetry — but the core operation is intersection, and the failure case is an empty intersection.
That failure case deserves an assertion, because "bring the link up anyway" is a plausible and catastrophic implementation choice:
// Illustrative property: never enable the link without a mutually
// supported mode.
property p_no_enable_without_common_mode;
@(posedge clk) disable iff (!rst_n)
link_enable |-> compatible;
endproperty
assert property (p_no_enable_without_common_mode);Which yields a definition worth keeping:
Interoperability means finding a mutually supported operating point — not merely connecting two individually valid endpoints.
8. Error Semantics Cannot Be an Implementation Detail
When something goes wrong on the link, the two sides must agree on what happens next. If they do not, the recovery becomes the failure.
The questions that must be answered identically on both sides:
- What counts as an error, and which errors are correctable versus fatal?
- Who retries — and does the sender keep a copy until it knows the beat was received, or is loss permitted?
- Who reports, to whom, and with what attribution back to the offending transaction?
- Does traffic continue during recovery, or must it stop?
- Is reset required, and does it reset the link only or the whole system?
Take just the retry question. If the sender assumes the receiver retries and the receiver assumes the sender does, an error means the transaction is dropped and both sides believe the other handled it. If both retry, it may be duplicated. Loss and duplication are opposite failures produced by the same missing agreement — which is why "at most once" versus "at least once" versus "exactly once" is interface semantics rather than an implementation nicety.
9. Ordering Is a Contract, Not an Optimisation
The subtlest mismatch, because both sides can be individually correct and fast, and the damage appears in software.
Suppose Side A is permitted to reorder two independent requests — a reasonable optimisation, since it knows they are independent. Side B assumes that the order requests arrive implies the order in which they complete, and builds a dependency on that.
Independently, each is defensible. Together, software-visible correctness breaks: a program that writes a data structure and then sets a flag can have the flag observed before the data.
Note the two things that make this dangerous. It is intermittent — reordering may only occur under specific load, so the system passes testing and fails in the field. And it is invisible at the interface — no protocol rule is violated, because the interface never stated one.
Ordering is part of the contract. What may be reordered, and what must not be, has to be stated rather than inferred from an implementation's observed behaviour.
The corollary matters for implementers: if the specification permits reordering, a consumer must not depend on arrival order, however reliably its current partner happens to preserve it. Depending on unspecified behaviour is how an implementation becomes accidentally coupled to one partner.
10. Version Compatibility
Everything above assumed two dies targeting the same interface definition. Revisions break that assumption in a way that is easy to underestimate.
Between revisions, a field may be added, reset behaviour clarified or changed, a capability introduced, error semantics altered, or an optional feature defined that one side implements and the other has never heard of. A die built to revision N and a die built to revision N+1 may be electrically identical and behaviourally incompatible.
So a durable interface needs three things beyond its functional definition: version identification so each side knows what the other implements, capability negotiation so a common operating point can be found across revisions (§7), and defined backward-compatibility rules stating what a newer implementation must still support and how it must behave toward an older partner.
This is also where Chapter 3.1's conclusion returns with more force. Stability is what makes an interface reusable — and versioning is the machinery that lets an interface change without losing stability. An interface with no version identification cannot evolve safely, because no implementation can tell what it is talking to.
11. Why Interoperability Verification Is a Different Activity
This is the shift in mindset that Module 3 exists to produce, and it is worth stating sharply because it changes what a verification plan contains.
Endpoint verification asks: does this implementation obey its specification? The testbench is built from the same understanding as the design, usually by the same organisation. It is necessary and it is not sufficient — every failure in this chapter involves two endpoints that each pass their own verification.
Interoperability verification asks: does this implementation work with an independently developed implementation of the other end? That is a different question requiring different work:
- Conformance testing against the specification's stated behaviour rather than against a chosen partner — because the partner may not exist yet, may be unavailable, or may be built by someone with no obligation to share it.
- Independently developed models or BFMs, ideally written from the specification by someone who did not write the design. A BFM derived from the design encodes the design's assumptions and cannot detect a shared misreading.
- Capability matrix coverage — every permitted combination of supported modes, not just the one the design prefers.
- Unsupported-option behaviour — what happens when the partner requests something this implementation does not support. Graceful refusal is a requirement, not a courtesy.
- Version permutations, including older and newer partners where the specification requires compatibility.
- Reset and bring-up timing permutations, since §5 showed that relative timing is where these bugs live.
- Error injection, to confirm both sides converge on the same recovery rather than diverging.
- Negative compatibility tests — confirming the link refuses to operate when there is no common mode (§7), rather than proceeding in an undefined state.
The deepest point here is about ambiguity. Two implementations can each be genuinely compliant with a specification and still fail to interoperate, if the specification permits two readings and each implementer chose a different one. That is not an implementation bug — it is a specification defect, discoverable only by independent implementation. It is the reason interoperability programmes exist at all, and the reason a standard's precision matters more than its feature list.
12. Common Misconceptions
13. Understanding Check
14. Summary and What Comes Next
A wire carrying bits is not an interface. It becomes one when both sides attach the same meaning to those bits — and that meaning is a stack of contracts: electrical behaviour so bits arrive, link behaviour so transfers complete, transaction meaning so results are correct, and management so the link reaches and reports a usable state. Agreement is conjunctive, so a mismatch at any single layer is enough.
The failures are specific and checkable. A handshake encodes who owns progress, when a transfer is complete, when the payload may change, and what acceptance means — and two self-consistent endpoints mapping onto each other's signals can silently lose data or deadlock. The payload-stability invariant (valid && !ready |=> valid && $stable(data)) is the clearest case of a rule that must be shared rather than merely implemented, and it shows how a specification becomes executable. Reset and bring-up determine when the interface exists. Link state must mean the same thing on both sides, including whether readiness is reached by timeout or by partner indication. Capability intersection decides whether an operating point exists at all, and an empty intersection must prevent the link from enabling. Error semantics must agree or recovery becomes the failure, producing loss or duplication. Ordering is software-visible correctness, intermittent, and invisible at the signal level. And versioning is what lets any of this evolve without losing stability.
Above all: self-consistency is not compatibility. Endpoint verification cannot detect a shared misunderstanding, which is why interoperability requires conformance against a specification, independently written models, capability-matrix coverage, and negative tests — and why an ambiguous specification produces two compliant implementations that do not work together.
Which is the argument for what follows. If interoperability demands agreement across the entire externally visible contract, then bilateral private agreements do not scale — every new pairing is a fresh negotiation across every layer. The industry needs one shared definition instead:
- 3.3 — Industry Standardisation — why an ecosystem needs a single common die-to-die contract rather than many individually good interfaces, and what such a contract should and should not define.
Browse the full path on the UCIe tutorials index.