Skip to content

UCIe · Module 12

Request Flow

Following one request from the cycle it is accepted at FDI to the cycle the remote endpoint owns it — ownership transfer at each boundary, the skid buffer that breaks the ready chain, the admission gate's independent vetoes, overlapping PHY and replay ownership, simultaneous push and pop, why a timeout does not mean the request was never delivered, and the conservation equation that proves nothing was lost.

Module 11 established what state must exist. It said comparatively little about when, because a chapter holding three state planes in view cannot also be cycle-accurate.

This chapter is cycle-accurate about one thing: a single request, from the cycle a Protocol Layer hands it down to the cycle a remote Protocol Layer owns it. Everything in between is buffers, resource checks, framing, transmission, and reconstruction — and the entire difficulty is that at every step, exactly one party is responsible for the request not being lost, and the identity of that party changes.

Get that wrong and a request disappears. Not corrupted — disappeared, with every FIFO count legal, every CRC passing, and no error anywhere.

1. The One-Sentence Model

A request is passed through ownership boundaries, not copied through layers.

At every boundary the producer owns the request before the handshake and the consumer owns it after, and the producer may forget it only when its contract says it may. Most of this chapter is working out, boundary by boundary, when that is — and the answer is almost never "as soon as the handshake completed".

2. Sourcing, and What Is Symbolic

3. The Two Named Boundaries

Everything in this chapter happens at, or between, two interfaces that the UCIe architecture names:

BoundaryBetweenWhat crosses it
FDI — Flit-Aware Die-to-Die InterfaceProtocol Layer ↔ D2D Adapterflit-aware traffic; the Adapter is flit-aware on this side
RDI — Raw Die-to-Die InterfaceD2D Adapter ↔ Physical Layerraw traffic; Raw Mode bypasses the Adapter to RDI directly

Why the naming matters for this chapter. These are the two places where ownership of a request changes hands between blocks that may come from different vendors. The UCIe material's stated motivation for standardising them is exactly that — plug-and-play IPs across an FDI/RDI boundary. So the contracts at these two boundaries are not internal design conventions you can bend; they are the integration surface.

And a request crosses each of them twice — once outbound as a request, once inbound as its response, which is Chapter 12.2's subject. This chapter follows the outbound direction only.

4. The Ownership Rule, Stated Precisely

Three clauses, and the third is the one that gets dropped.

Before the handshake, the producer owns the request. It must hold it, stable and unchanged, for as long as the consumer stalls. Chapter 5.1 §4 built this at the Protocol Layer and it holds at every boundary below.

After the handshake, the consumer owns the request. It has taken responsibility for the request not being lost, which means it must have had somewhere to put it — §12's argument.

The producer may forget the request only when its own contract allows. Not when the handshake completes. Handing a request to the Adapter does not let the Protocol Layer discard its transaction state, because the Protocol Layer's contract is with the far side and is not discharged until a response returns. Handing a flit to the PHY does not let the Adapter discard the replay copy, because the Adapter's contract is reliable delivery. The handshake transfers responsibility for forward progress; it does not transfer responsibility for recovery.

There are therefore intervals in which two layers are simultaneously responsible for the same request, for different reasons. §20 is that observation made concrete, and it is the single most misunderstood thing about this path.

5. The Pipeline, and Who Owns Each Stage

A protocol source hands a request across FDI into a boundary skid buffer, then into the adapter which frames it and allocates replay capacity, then across RDI into a physical layer transmit stage and onto the lanes. On the far die the physical layer receives, the remote adapter validates and reconstructs, and the remote protocol consumer accepts the request.Protocol sourceowns it until FDIhandshakeFDI boundarybufferAdapter owns forwardprogressAdapter framingheader and CRC addedhereReplay storeAdapter owns recoveryPHY transmitacross RDI; owns thewireLanesdata, valid, track, fwdclockRemote PHYreceives, deserialisesRemote Adaptervalidates andreconstructsRemote consumerowns it once,semanticallyFDIRDIonce12
Figure 1 — the outbound request path, drawn as a chain of owners rather than a chain of blocks. Each box is a place a request can be while nothing is moving, and the label beneath names what makes that stage necessary. The Adapter appears twice on purpose: once as the stage that frames and admits the request, and once as the replay store that keeps a copy after the PHY has taken it.

The ownership table, which is the chapter's spine:

StageOwnerRetained becauseReleased when
Protocol sourcethe sourcenot yet accepted at FDIthe FDI handshake completes
FDI boundary bufferthe Adapterupstream was told it was acceptedframing consumes it
Adapter framingthe Adapterheader and CRC must be applied atomicallyhanded toward RDI
Replay storethe Adapterthe transmission may need repeatingtransport confirms delivery
PHY transmitthe PHYa physical transfer is in progressthe bits are on the wire
Remote PHY / Adapterthe remote sidereconstruction and validation incompletea whole object exists
Remote consumerthe remote Protocol Layerthe semantic transaction is openthat protocol's own rules retire it

Two rows overlap in time and that is not an error. The replay store and the PHY transmit stage are simultaneously responsible for the same request — the PHY for getting the bits out, the Adapter for being able to send them again. §20.

And one row is the whole of Chapter 5.1. The remote consumer's ownership outlasts everything to its right in the figure, which is why the Protocol Layer needs real storage rather than a pass-through path.

6. The Request Object

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE internal request representation. NOT a UCIe format: no FDI
// signal name, width, or field position is asserted. Symbolic widths.
localparam int ADDR_W = 52;      // symbolic
localparam int DATA_W = 512;     // symbolic
localparam int BE_W   = DATA_W/8;
 
typedef struct packed {
  logic [ADDR_W-1:0] addr;
  logic [DATA_W-1:0] data;        // meaningful for writes
  logic [BE_W-1:0]   byte_valid;  // which bytes a partial write touches
  logic              write;
  logic [ID_W-1:0]   id;          // the PROTOCOL's transaction identity — §14
  logic [MON_ID_W-1:0] mon_id;    // VERIFICATION ONLY — §14
} request_t;

Architecture. Address, data, extent, direction, identity. Five fields that are consumed together by the far side and must therefore travel together — §16's rule, and the reason this is a struct rather than five buses.

State. None yet; this is the representation.

Contract. Below FDI, addr, data and id are carried, not interpreted. The Adapter's job list — link state, negotiation, arbitration, CRC/retry — contains nothing that requires reading them, and Chapter 10.1 §7 is the argument for keeping it that way.

Note byte_valid explicitly, because partial writes are not an edge case in a memory path and the CXL link layer's own treatment shows why the field needs care: Chapter 11.4 §8 recorded that CXL does not transmit byte-enable bits when all bytes are enabled, clearing a header field instead and requiring the receiver to regenerate the all-ones value. A mapping layer that carries a cleared field through unchanged hands the far side all-zeros — a write that touches nothing, from a request that was perfectly well formed.

Note mon_id explicitly. It is a verification-only tag, present in simulation so a scoreboard can follow one request end to end. It is not a protocol field, not a transport sequence number, and not present in silicon. §14 is why all three must exist separately.

7. Wrong RTL — the Combinational Ready Chain

Start with the design that cannot work, because the reason it cannot is the reason every buffer in §5 exists.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — acceptance at the top derived from readiness everywhere below.
assign src_ready = adapter_ready && phy_ready && remote_ready;

Architecture. It expresses the intuition "only accept if the whole path can take it", which sounds conservative and is unimplementable.

Four separate problems, and they are worth separating because they fail differently.

The timing path is the length of the stack. src_ready now depends combinationally on logic in the Adapter and the PHY. In a design where those are separately-delivered IP blocks across an FDI/RDI boundary, this is a combinational path across a vendor boundary — which is precisely what standardising those interfaces exists to avoid.

It couples layers that must be independently replaceable. Swap the PHY and the Protocol Layer's timing closure changes.

remote_ready does not exist. There is no combinational signal from the far die. Whatever stands in for it is a stale view of the far side, delayed by at least a link round trip — which is what credits are for (Chapter 9.5), and credits are registered state, not a wire.

And it makes the whole path move in lockstep. One stall anywhere stops acceptance everywhere, so the pipeline can never hold more than one request in flight and the link runs at a fraction of its bandwidth. Chapter 9.5 §11's bandwidth-delay product says how bad: a path that cannot hold enough requests in flight to cover the round trip is idle most of the time.

The fix is the shape of §5. Every boundary gets storage, ready at each boundary depends only on that boundary's own state, and the far side's capacity arrives as registered credits rather than as a wire. The buffers are not an optimisation; they are what makes the layering physically possible.

8. The FDI-Side Boundary Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE two-entry skid buffer at the FDI boundary. Signal names are
// this chapter's own; no FDI signalling is asserted.
//
// A two-entry skid is the smallest structure that lets `in_ready` be registered
// (no combinational path from in_valid to in_ready) while still accepting a
// request in the same cycle one is consumed downstream.
module fdi_skid #(
  parameter type T = request_t
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic in_valid,
  output logic in_ready,
  input  T     in_data,
 
  output logic out_valid,
  input  logic out_ready,
  output T     out_data
);
 
  T     buf_q   [2];
  logic occ_q   [2];        // per-slot occupancy
  logic wptr_q, rptr_q;     // one bit each — two slots
 
  wire push = in_valid  && in_ready;
  wire pop  = out_valid && out_ready;
 
  // Registered ready, and it depends on OCCUPANCY only. There is no path from
  // in_valid to in_ready, which is the property §7's chain destroys.
  assign in_ready  = !(occ_q[0] && occ_q[1]);
  assign out_valid = occ_q[rptr_q];
  assign out_data  = buf_q[rptr_q];
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      occ_q[0] <= 1'b0;
      occ_q[1] <= 1'b0;
      wptr_q   <= 1'b0;
      rptr_q   <= 1'b0;
      buf_q[0] <= '0;
      buf_q[1] <= '0;
    end else begin
      // Push and pop are INDEPENDENT and may occur in the same cycle. Because
      // they touch different slots unless the buffer is empty, each slot's
      // occupancy has exactly one writer per cycle — which is what makes this
      // safe without a combined case statement (§21 handles the counter form).
      if (push) begin
        buf_q[wptr_q] <= in_data;
        occ_q[wptr_q] <= 1'b1;
        wptr_q        <= ~wptr_q;
      end
      if (pop) begin
        occ_q[rptr_q] <= 1'b0;
        rptr_q        <= ~rptr_q;
      end
    end
  end
 
endmodule

Classification: synthesizable, illustrative.

Architecture. Two slots, one bit of pointer each way. It exists to make in_ready a function of registered state and nothing else, and to let a request arrive in the same cycle another leaves — which is what keeps the boundary at full throughput under a downstream stall of one cycle.

State. Two request slots and two occupancy bits, per-request lifetime. An occupied slot is the Adapter's ownership of an accepted request made physical: §4's second clause is this array.

Cycle behaviour. push and pop are independent. The same-cycle case is the interesting one and it is safe here for a specific reason: when the buffer is non-empty, wptr_q and rptr_q differ, so push and pop touch different slots and each occ_q bit has exactly one writer. When the buffer is empty, out_valid is low so pop cannot fire — the case cannot arise. That is an argument, not a coincidence, and it is worth writing down because the counter form in §21 does not get the same free pass.

Contract. Upstream relies on in_ready meaning the request will be retained. Downstream relies on out_data being stable while it stalls.

Failure. Make in_ready depend on out_ready and the combinational path of §7 reappears at one boundary. Forget the same-cycle case and throughput halves under any sustained backpressure — a performance bug that looks like a link problem.

DV. Fill both slots; drain both; push and pop in the same cycle at every occupancy; and hold out_ready low for many cycles with in_valid high, verifying in_ready falls after exactly two accepts and not before.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — an accepted request is retained until it is handed on.
// Uses the verification-only monitor tag: the property is about a specific
// request object, not about occupancy.
property p_accepted_request_retained;
  @(posedge clk) disable iff (!rst_n)
    (in_valid && in_ready) |=> (request_present_somewhere[in_data.mon_id]
                                until_with handed_downstream[in_data.mon_id]);
endproperty
a_accepted_request_retained: assert property (p_accepted_request_retained);
 
// Illustrative — no combinational dependency from valid to ready. Written as a
// structural check rather than a temporal one: in_ready must be explainable
// from registered occupancy alone.
property p_ready_independent_of_valid;
  @(posedge clk) disable iff (!rst_n)
    in_ready == !(occ_q[0] && occ_q[1]);
endproperty
a_ready_independent_of_valid: assert property (p_ready_independent_of_valid);
 
// Illustrative — payload stability under downstream stall.
property p_out_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
    (out_valid && !out_ready) |=> ($stable(out_data) && out_valid);
endproperty

On p_ready_independent_of_valid. It looks tautological against the code above, and that is the point: it is a structural guard that fires if someone later optimises in_ready to peek at out_ready for one extra cycle of throughput. The property costs nothing and it protects an interface contract that is not otherwise visible in a diff.

9. What the Adapter Adds

Before the admission gate, it is worth being precise about what the Adapter actually does to a request — because the UCIe material states it, and it changes the resource question.

The Adapter is described as responsible for packetization, adding a 2-byte Flit Header and a 2-byte CRC. The header is described as carrying a 3-bit Protocol ID, a 1-bit Credit field, Flit Ack/Nak management as a 2-bit command plus an 8-bit sequence number, and 2 reserved bits. The CRC covers a 128-byte payload, with smaller payloads zero-extended, giving a triple-bit-flip detection guarantee in 16 bits, and replay if CRC fails.

Four consequences for this chapter, and each is a design constraint rather than trivia.

Framing is atomic with respect to the request. The header and CRC describe this payload. Chapter 9.4 §7 made the general argument — CRC must be aligned with what it describes — and the concrete form here is that the framing stage must not be able to advance the payload without the header, which is §16.

The sequence number is 8 bits and it is not the protocol's identity. An 8-bit Ack/Nak sequence number is a transport identity with a transport lifetime. §14 is the section that keeps the three identity spaces apart, and this is the middle one.

The Credit field is one bit in the header, which means credit return is piggybacked on ordinary traffic. That has a design consequence people miss: if no flits are being sent in one direction, credits are not flowing back in that direction either — which is exactly why the next point matters.

A flit is transmitted whether or not the Protocol Layer supplied one. The UCIe material's flit-format description includes a field carrying payload "or all 0s if no Flit from Protocol Layer". So the transport does not idle waiting for the Protocol Layer; it frames and sends regardless. §12 develops what that means for the request path, and it is the opposite of the intuition most people bring from a valid/ready-only mental model.

10. The Admission Gate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE admission decision at the Adapter. Written as one expression
// for legibility; a real implementation almost certainly pipelines these terms
// and precomputes most of them, because this is a timing-critical decision.
assign adapter_accept =
    in_valid                 // §8 — a request is presented
 && boundary_space           // somewhere to put it
 && replay_space             // §11 — it can be RETAINED for retry
 && credit_available         // Ch 9.5 — the far side has a slot
 && link_operational;        // the Adapter's own link state permits traffic

Architecture. Four independent vetoes plus the presented request. None is the others' proxy, and the practical value of writing them separately is that each failure lands in a different place with a different symptom.

State. None of its own — a conjunction over four registered facts owned by four mechanisms.

Cycle behaviour. Evaluated per request. Note that credit_available is registered state reflecting a view of the far side that is at least a round trip old, which is Chapter 9.5's entire subject and the reason it appears here as a count rather than a wire.

Contract. The Protocol Layer relies on acceptance meaning safe handling all the way through. That is a strong claim and each term is part of what makes it true.

Failure, per omitted term:

OmittedWhat happens
boundary_spacethe request is overwritten locally — lost before it was framed
replay_space§11 — it becomes unrecoverable the moment the link errors
credit_availablethe remote receive buffer overflows; the damage is on the other die
link_operationalit is accepted into a path that cannot move it, and Chapter 10.1 §10's rule is violated

DV. Each term low with the others high — four directed cases — and each term transitioning low with a request presented, which is the harder set because it tests whether the decision is sampled coherently.

11. Wrong RTL — Accept With Only Queue Space

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the most plausible single-term gate.
assign adapter_accept = in_valid && boundary_space;

Architecture. Queue space is the term a designer can see locally, and it is the one that feels like the resource. Replay capacity is invisible from the boundary and is therefore the one that gets forgotten.

Cycle behaviour. The request is accepted. It is framed. It is transmitted. Everything works.

Failure, and it is conditional on an error occurring — which is what makes it survive testing. With the replay store full, the Adapter has accepted responsibility for reliable delivery of a request it cannot retain a copy of. As long as nothing corrupts, nobody notices. The first CRC failure on that flit is unrecoverable: the Adapter is asked to replay something it does not have.

What happens next depends on the design and none of the options are good. It may replay a different entry — delivering a stale request a second time, which is a duplicate. It may replay nothing and declare success, which loses the request silently. Or it may escalate the link to an error state, which is the best outcome and still converts a single-bit flip into a link-level failure.

Why regressions miss it. It requires the replay store to be full and an error to be injected on the affected flit. A suite that injects errors under light load, or fills buffers without injecting errors, exercises neither leg of the conjunction. The cross of "replay full" with "error injected" is the bin that finds it, and §30 lists it for that reason.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — reliability capacity is part of acceptance, not an
// afterthought. Chapter 9.4 §11's rule, encoded at this boundary.
property p_accept_implies_replay_capacity;
  @(posedge clk) disable iff (!rst_n)
    adapter_accept |-> replay_space;
endproperty
a_accept_implies_replay_capacity: assert property (p_accept_implies_replay_capacity);
 
// Illustrative — every accepted request has a retained copy until confirmed.
property p_accepted_has_replay_entry;
  @(posedge clk) disable iff (!rst_n)
    (adapter_accept && !transport_confirm_fire)
      |=> replay_holds[in_data.mon_id];
endproperty

12. The Transport Does Not Wait for the Protocol Layer

A consequence of §9's last point, and it inverts an assumption most readers arrive with.

In a valid/ready-only mental model, nothing happens until a producer has something to send. That is not how this transport behaves. The UCIe material's flit-format description carries a field holding payload "or all 0s if no Flit from Protocol Layer" — the Adapter frames and transmits a flit regardless of whether the Protocol Layer had one to give.

Three design consequences.

Request latency is quantised. A request that misses its framing opportunity waits for the next one. So the observed latency of an otherwise-identical request varies by up to a framing interval, and a design that assumes a fixed FDI-to-wire latency will mis-size downstream buffers.

"No traffic" is not the same as "link idle". Flits continue to cross, which is what keeps the Ack/Nak sequence and the piggybacked credit machinery alive. A design that gates the whole transmit path on having payload would starve its own credit return — the sender stops sending, so credits stop coming back, so the sender stays stopped. A self-sustaining stall built out of two individually reasonable optimisations.

Acceptance and transmission are decoupled in both directions. §10's gate answers "may this request be admitted". Whether a flit leaves this cycle is a different question with a different answer, and conflating them is how designs end up with a request accepted into a path that will not carry it for an unbounded time.

The framing engine's cadence is not the Protocol Layer's cadence, and the request path's buffering exists to absorb the difference.

13. Three Identity Spaces

A conflation that produces spectacular bugs, and there are now three spaces rather than the two Chapter 10.2 §10 compared.

Protocol transaction IDTransport sequence numberMonitor ID
LayerProtocolAdapterthe testbench
Purposematch a response to its requestlocate a retained flit for replayfollow one object end to end
Ownerthe Protocol Layer (Ch 5.1 §5)the transmitting Adapterthe verification environment
Lifetimeissue → response receivedallocation → confirmed deliverythe whole test
Reusedwhen the response retires itwhen the entry retiresnever
Visible toboth Protocol Layers, and softwareneither Protocol Layernothing in silicon
Size hereprotocol-defineddescribed as 8 bits for Ack/Nakas wide as the test needs

Why the first two get conflated. Both are small integers identifying something in flight, both wrap, both have an outstanding window. It is a natural mistake and it has two failure modes.

Using the protocol ID as the replay identity bounds the replay window by the protocol's tag space, and — worse — makes two transactions with the same tag at different times indistinguishable to the transport, so a replay can resurrect the wrong one.

Using the transport sequence number to match responses is equally broken: it changes across retries and it means nothing to the far Protocol Layer.

Why the third must exist separately. Both real identities are reused. So neither can answer the verification question "is this the same object I saw before, or a different object that inherited its identity?" — and that question is exactly what §28 and Chapter 12.2 §27 are about. A monitor ID is not a convenience; it is the only identity in the system that is unique over the whole test.

14. The Adapter-to-PHY Handoff

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE bundled transport object crossing toward RDI. NOT a UCIe format:
// the header is symbolic and no field position is asserted. What is modelled is
// that the header, the payload, and the extent travel as ONE object.
typedef struct packed {
  logic [HDR_W-1:0]    hdr;         // framing metadata — symbolic width
  logic [PAY_W-1:0]    payload;
  logic [PAY_B_W-1:0]  byte_valid;
  logic [SEQ_W-1:0]    seq;         // §13's middle column
  logic [MON_ID_W-1:0] mon_id;      // VERIFICATION ONLY
} txobj_t;
 
txobj_t txobj_q, txobj_q2;
 
// One object, ONE enable. Misalignment is not prevented by review; it is
// made unrepresentable.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    txobj_q  <= '0;
    txobj_q2 <= '0;
  end else if (tx_pipe_en) begin
    txobj_q  <= txobj_in;
    txobj_q2 <= txobj_q;
  end
end

Architecture. The framing metadata and the payload it describes are consumed together by the far side, so they travel together. One enable means there is no way to advance one and not the other.

State. Two pipeline stages of one object, per-object lifetime.

Cycle behaviour. A stall stalls the whole object.

Contract. The receiving side relies on the header describing the payload it arrives with. The CRC makes that dependency load-bearing: §9 established that the CRC covers the payload, so a header paired with the wrong payload is a validly CRC-protected wrong object.

Failure. §15.

15. Wrong RTL — Metadata and Payload Pipelined Apart

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two paths, different depths. Each always_ff is individually correct.
always_ff @(posedge clk) hdr_q     <= txobj_in.hdr;         // 1 stage
always_ff @(posedge clk) pay_q     <= txobj_in.payload;
always_ff @(posedge clk) pay_q2    <= pay_q;                // 2 stages
// The output pairs hdr_q with pay_q2 — header N with payload N-1.

Architecture. Someone needed an extra cycle on the wide payload path for timing and added it there, because that is where the timing problem was. The metadata path did not need it and did not get it.

Cycle behaviour. Every cycle, the emitted object consists of this request's header and the previous request's payload.

Failure, and it is the most severe class in this chapter. The far side receives a well-formed request with the wrong contents:

  • The address, the direction, and the identity come from request N. The data comes from request N−1.
  • The CRC is correct, because it was computed over the object as assembled.
  • Every field is individually legal, so nothing rejects it.
  • For a write, it is executed — a legitimate write, to a legitimate address, with the previous request's data.

And note the compounding. Because the identity comes from request N, the response will return for N and match N's outstanding entry correctly. The bookkeeping is flawless. Chapter 12.2 will not catch this, because there is nothing wrong with the response path.

Why it survives review. Each always_ff is a single correct assignment. The bug is in the relationship between three statements, and relationships do not appear in a diff.

Why it survives simulation. It produces no symptom whenever consecutive requests carry similar payloads — which in a directed test writing incrementing patterns to sequential addresses is often enough to look plausible. It shows up when consecutive requests differ, which is a stimulus property, not a design property.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the whole bundle holds still, and it holds still TOGETHER.
property p_txobj_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
    (txobj_valid && !txobj_ready) |=> ($stable(txobj_q) && txobj_valid);
endproperty
a_txobj_stable_under_stall: assert property (p_txobj_stable_under_stall);
 
// Illustrative — the header and the payload emitted together came from the
// same source object. Checkable only with the verification-only tag, because
// no protocol field distinguishes "this payload" from "the previous payload".
property p_header_matches_payload_source;
  @(posedge clk) disable iff (!rst_n)
    txobj_emit |-> (emitted_hdr_mon_id == emitted_pay_mon_id);
endproperty
a_header_matches_payload_source: assert property (p_header_matches_payload_source);

p_header_matches_payload_source is the only property in this chapter that catches §15, and it is worth noticing why nothing else can. Stability assertions pass — both paths are stable under stall. CRC passes. Occupancy is correct. The misalignment is invisible to every check that does not carry an independent notion of which request this is.

16. The RDI Side, and Why the PHY Buffers Too

A request that has crossed RDI is not yet on the wire, and it is worth being clear about why a further stage exists without inventing UCIe internals.

What the UCIe material establishes about the Physical Layer. Its unit is a Module — unidirectional, with 1, 2, or 4 modules forming a Link — carrying Data (16 or 64 lanes), Valid, and Track, plus one differential forwarded-clock pair, with lane reversal on the transmit side, spare lanes in advanced packaging and width degradation in standard, and data rates from 4 to 32 GHz. Sideband is always on at 800 MHz.

Four reasons that structure implies buffering, each derived from a verified property rather than invented:

Width adaptation. The Adapter's framing granularity and the module width (16 or 64 lanes) are different quantities. Something must convert between them, and conversion between mismatched widths needs storage.

Multi-module scheduling. With 1, 2, or 4 modules forming a Link, an object must be distributed across modules — a scheduling decision with its own buffering.

Degradation and repair. Width degradation in standard packaging and spare lanes in advanced packaging mean the usable width can change, so the mapping from object to lanes is not fixed at design time.

Clock domain. The Adapter's clock and the PHY's transmit clock are not required to be the same, and where they are not, the boundary needs a proper asynchronous structure.

On CDC, and deliberately not re-teaching it. Chapter 5.3 §7 built the safe crossing and §8 built the multi-bit CDC trap — the bug where each bit of a bus is synchronised independently and the receiver samples a word that never existed on the transmit side. That is the correct treatment and this chapter does not repeat it. The only thing worth adding is the request-path consequence: a request word assembled from bits captured across two transmit-side values is not a corrupted request, it is a fabricated one, with a plausible address that nobody issued. Use a proper asynchronous FIFO or the canonical handshake of Chapter 5.3 §7, and never a per-bit synchroniser on the request bus.

17. Overlapping Ownership — the PHY Transmits While the Adapter Retains

The subtlety §4 promised, and it is the point where the ownership model stops being a simple relay.

Once the physical transfer begins, the PHY owns the transmission. It is putting bits on lanes; nothing above it can change what is going out.

And the Adapter still owns recovery. Its replay copy exists precisely because the transmission may need repeating. It cannot be released when the PHY takes the object; it is released when transport confirms delivery, which is a message from the far side.

PHY ownership of transmission and Adapter ownership of reliability overlap in time, and neither implies the other has ended.

Why this is worth a section. Because "the PHY has it, so we're done" is the single most natural way to reason about a layered stack, and it is wrong in a way that only shows up under error injection. Three concrete consequences:

Replay occupancy does not fall when transmission completes. It falls on confirmation. A design that frees on transmission has Chapter 9.4 §6's retirement bug.

The replay copy must be immutable while retained. A replay must re-send what was sent. If the retained copy shares storage with a queue slot that has since been reused, the replay sends something else — a request that was never issued in the first place.

And the object may be simultaneously on the wire and in the replay store for the second time. During a go-back-N replay, an object is being transmitted again while its retained copy is still retained. Occupancy is unchanged; only the send pointer moved (Chapter 9.4 §8).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a retained object is immutable while retained.
property p_replay_entry_immutable;
  @(posedge clk) disable iff (!rst_n)
    (replay_valid_q[e] && !replay_retire[e]) |=> $stable(replay_obj_q[e]);
endproperty
a_replay_entry_immutable: assert property (p_replay_entry_immutable);
 
// Illustrative — transmission completing does not release reliability state.
property p_transmit_does_not_retire_replay;
  @(posedge clk) disable iff (!rst_n)
    (phy_transmit_complete[e] && !transport_confirm[e]) |=> replay_valid_q[e];
endproperty
a_transmit_does_not_retire_replay: assert property (p_transmit_does_not_retire_replay);
 
// Illustrative — a replay does not allocate. Occupancy is unchanged.
property p_replay_does_not_allocate;
  @(posedge clk) disable iff (!rst_n)
    replay_start |=> (replay_occupancy == $past(replay_occupancy));
endproperty

18. Simultaneous Push and Pop, Done Properly

§8's skid buffer got a free pass on the same-cycle case because per-slot occupancy has one writer. A counter does not, and this is where the classic bug lives.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE occupancy counter for a deeper request queue. The explicit
// case over the two events is the whole point of the example.
logic [OCC_W-1:0] occ_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    occ_q <= '0;
  end else begin
    unique case ({push, pop})
      2'b10: occ_q <= occ_q + 1'b1;
      2'b01: occ_q <= occ_q - 1'b1;
      2'b11: occ_q <= occ_q;          // one in, one out — UNCHANGED
      2'b00: occ_q <= occ_q;
    endcase
  end
end
 
// The bound is asserted, never enforced by saturation (Ch 9.5 §8): a counter
// that clamps has already discarded the information explaining the failure.
assign queue_space = (occ_q != DEPTH);

Architecture. One writer, one explicit case, four enumerated outcomes. unique case makes an unintended overlap a simulation error rather than a silent precedence.

State. One counter, aggregate per-request lifetime — each increment matched by exactly one decrement.

Cycle behaviour. The 2'b11 row is the one that matters and it is the one that is easy to get wrong. A request arriving in the same cycle one departs leaves occupancy unchanged, and the counter must express that as a single assignment rather than as two.

Contract. queue_space gates §10's admission. A drifting counter therefore corrupts admission, not just reporting.

Failure. §19.

DV. Every occupancy from empty to full; 2'b11 at empty, mid-range and full; and a long run of alternating push/pop verifying the counter returns to its starting value. The last one is the cheapest drift detector there is and it catches §19 in a handful of cycles.

19. Wrong RTL — Two Independent Count Updates

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two if-statements, each individually correct.
always_ff @(posedge clk) begin
  if (push) occ_q <= occ_q + 1'b1;
  if (pop)  occ_q <= occ_q - 1'b1;
end

Architecture. Two assignments to one register with no statement of what happens when both fire.

Cycle behaviour. When both fire, the last assignment executed wins. The count decrements — losing the increment entirely.

Failure, and the shape of the drift is what makes it hard. Every simultaneous push/pop loses one count. So occ_q drifts downward relative to reality: the queue believes it is emptier than it is.

Trace the consequence through §10: queue_space is asserted when there is none, the admission gate accepts a request the queue cannot hold, and the request is overwritten. A request has been lost, and:

  • the FIFO count is legal — it is a small number, well inside its range;
  • every CRC passes, because the flits that were sent were fine;
  • no assertion on the queue fires, because the count never went out of bounds;
  • and the symptom appears as a missing transaction, arbitrarily long after the drift began.

Why the direction of drift matters diagnostically. Downward drift causes loss. If the two statements were ordered the other way the drift would be upward, which causes the queue to refuse requests it could hold — a throughput bug that looks like a link problem and destroys nothing. Same bug, opposite ordering, completely different severity, and neither is visible in a count-range check.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the count must agree with reality. The strongest check here
// is not a bound; it is a comparison against an independently maintained
// count in the verification environment.
property p_occupancy_matches_model;
  @(posedge clk) disable iff (!rst_n)
    occ_q == model_occupancy;
endproperty
a_occupancy_matches_model: assert property (p_occupancy_matches_model);
 
// Illustrative — and the cheap version if no model exists: conservation over
// the whole run. Drift of one is invisible per cycle and obvious in aggregate.
property p_occupancy_conserved;
  @(posedge clk) disable iff (!rst_n)
    occ_q == (total_pushes - total_pops);
endproperty

20. The Flagship Trace — One Request, With a PHY Stall

Fifteen illustrative cycles. Latencies are illustrative; the point is which state exists in which row, and where responsibility sits.

Request A, a write, issued by the Protocol Layer.

CycProtocol sourceFDI skidAdapter frameReplayPHY txRemoteNotes
1A validsource owns A
2A valid, ready highAFDI handshake — ownership moves
3A retained in txn tableA§4 clause 3 — source keeps semantic state
4A retainedA framingheader + CRC applied atomically
5A retainedA framingPHY not ready — A holds, stable
6A retainedA framingstill stalled; nothing is lost
7A retainedAAreplay allocated and RDI handoff
8A retainedAAon the lanes
10A retainedAA receivedPHY ownership ends; replay retained
11A retainedAA validatingCRC checked remotely
12A retainedAA reconstructeda whole object exists
13A retainedAA acceptedremote consumer owns it — once
15A retainedAconfirmation still in flight
17A retainedconfirmed → replay retires
20A retainedstill outstanding — awaiting a response
A protocol source hands a request across FDI to the adapter, which frames it and allocates a replay entry, then hands it across RDI to the physical layer after a stall. The physical layer transmits, the remote side reconstructs and accepts the request once, and a confirmation returns to the adapter which then retires the replay entry.One request, one stall, one acceptance — conceptualProtocol sourceD2D AdapterPhysical LayerRemote siderequest across FDIframe, allocatereplaynot ready yetobject across RDItransmit on lanesreconstruct, acceptoncedelivery confirmedretire replay entry
Figure 2 — the same trace as an exchange, drawn to make the handoffs visible. Each solid arrow is a point where responsibility for forward progress moves; each dashed arrow is information returning. Note that the Protocol Layer never appears again after its handoff and yet still holds transaction state throughout, and that the replay copy is released only by the confirmation at the end. Message labels are conceptual, not UCIe signalling.

Seven things to read off it, and together they are the chapter.

Cycle 2 is the only ownership transfer that looks like one. Everything after it is more subtle.

Cycle 3 is the clause people drop. The FDI handshake completed at cycle 2 and the Protocol Layer has not forgotten A. It cannot: its contract is with the far side and no response has returned. Two layers now hold state for A, for different reasons.

Cycles 5–6 are a stall in which nothing bad happens. A is valid, stable, and owned by the Adapter. The correct behaviour is to do nothing, and §8's stability property is what guarantees it.

Cycle 7 does two things at once, and they are frequently conflated: the replay entry is allocated and the object crosses RDI. Allocation is not transmission.

Cycle 10 is the overlap. The PHY's job is done — the bits are gone. The replay entry is still there, and will be for seven more cycles. §17.

Cycle 13 is the semantic acceptance, and it happens exactly once regardless of how many times the transport delivered the object. That is Chapter 11.4 §16's delivery fence doing its work on the request path.

Cycle 20 is the row that connects this chapter to the next. Transport is entirely finished. The replay entry retired three cycles ago. And the transaction is still open, because a response has not returned — which is Chapter 12.2's entire subject.

21. A Stall Is Not a Problem; Losing State During One Is

Worth isolating, because "the request is stuck" is a common report and usually describes correct behaviour.

At cycles 5–6 above, the request is stalled. Three things must be true and one must not.

Must: valid stays asserted. A producer that drops valid while waiting has withdrawn a request the consumer may have already begun to act on.

Must: the payload stays stable. §8's property. Advancing it mid-stall means the consumer captures a different request than the one presented — Chapter 5.1 §4's failure, and the corrupted result is semantically valid, so every layer below delivers it faithfully.

Must: the responsible party stays responsible. Whoever owned the request before the stall still owns it.

Must not: upstream progress stop unnecessarily. If the boundary has a free slot, the Protocol Layer should still be able to hand down another request. A stall at stage N is not a reason to stall stage N−2, and a design in which it is has rebuilt §7's lockstep by accident.

22. Multiple Requests In Flight

The single-request trace hides an accounting problem that appears the moment there are two.

With requests A and B in flight simultaneously, each stage holds independent per-request state — and the number of entries differs per stage. A may be in the replay store while B is still in the FDI skid. So:

Each stage's occupancy is independent, and Chapter 11.5 §12's warning applies: those numbers are not required to relate, and a check comparing them produces false failures.

Ordering is a protocol obligation, not a transport one. Chapter 5.1 §6 established that the Adapter must not invent ordering policy — it does not know which requests are related. And Chapter 11.3 §24 recorded the sharper CXL case: the host does not preserve the order of CXL.cache requests as delivered by the device, and the device must maintain ordering where it matters. So a request path that preserves acceptance order is being conservative, and one that reorders needs a justification it can point at.

Retry reorders nothing under go-back-N, which is a structural property rather than a policy (Chapter 9.4 §9). Selective retry buys bandwidth and requires reordering support at the receiver.

This chapter deliberately stops there. Ordering has its own chapterChapter 9.3 for the streaming case — and the request path's job is to not introduce reorderings, not to define what they mean.

23. Reset and Recovery After Upstream Acceptance

The awkward case: a reset or link recovery lands after the FDI handshake but before the remote consumer accepted.

The request cannot be silently dropped. The Protocol Layer was told it was accepted. Something above is waiting. Discarding it converts a link event into a hang with no error — Chapter 11.2 §18 and Chapter 11.5 §17, arriving through the request path.

Four dispositions exist and the architecture must pick one per stage:

DispositionWhen it is right
Retain and retrythe link recovers and the request can still be delivered — the good case, worth designing for
Retain and waitrecovery is expected shortly; the request stays owned and stalled
Escalaterecovery failed; the request is reported upward as failed through a defined path
Flush with explicit cancellationpermitted only if an upper-layer contract defines an abort, and only with the cancellation signalled

The one that is never acceptable is a fifth: discard silently. And note that the fourth requires the abort to be visible: a flush that the Protocol Layer does not learn about is a silent discard with extra steps.

Which disposition applies is not this chapter's to invent. Chapter 11.5 §17 argued the same point for coherence, and the reasoning is identical: the correct behaviour depends on the carried protocol's error model, and approximating it would be worse than naming the requirement.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — an accepted request reaches a defined disposition. Written as
// a coverage-and-reconciliation obligation rather than a bounded property,
// because the bound belongs to the protocol, not to this layer.
property p_no_silent_discard;
  @(posedge clk) disable iff (!rst_n)
    request_leaves_path[m] |-> (delivered[m] || abort_signalled[m]);
endproperty
a_no_silent_discard: assert property (p_no_silent_discard);

24. A Transport Retry Must Not Create a Second Remote Acceptance

Chapter 11.4 §17 established this for CXL semantics. On the request path it needs restating because the mechanism is where the request arrives, not where it leaves.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — remote semantic acceptance keyed on transport arrival.
assign remote_accept = flit_received && flit_crc_ok;

A retry occurs on corruption and also when a confirmation is lost (Chapter 9.4 §12) — in which case the original arrived intact and the receiver sees the same object twice, both times with a passing CRC. Keyed on arrival, the remote Protocol Layer accepts the same request twice.

For a write that is a write executed twice. For a read it is two responses for one request identity, and the requester receives a response it never asked for. Neither produces an error: the transport did exactly what it was designed to do.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — acceptance gated on RESOLUTION plus duplicate suppression.
assign remote_accept = obj_reconstructed
                    && !obj_is_duplicate      // this transport object is new
                    && delivery_order_ok;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — exactly-once remote acceptance, using the verification-only
// monitor tag because the protocol and transport identities are both reused.
property p_remote_accept_at_most_once;
  @(posedge clk) disable iff (!rst_n)
    remote_accept |-> !accepted_mon[remote_mon_id];
endproperty
a_remote_accept_at_most_once: assert property (p_remote_accept_at_most_once);

On "at most once" versus "exactly once". The property above is safety and is checkable in any run. The other half — that every accepted request is eventually accepted remotely — requires liveness assumptions and belongs in §33's end-of-test reconciliation, not in a temporal property with an invented bound.

25. A Timeout Does Not Mean the Request Was Not Delivered

The most important distributed-systems insight in this chapter, and the one that produces the worst bugs when it is missed.

A request has been outstanding too long. A watchdog fires. What has been learned?

Only this: no response has arrived. That is all. It is not the same as "the request was not delivered", and the difference is the difference between a safe recovery and a corrupted system.

Enumerate what may actually have happened:

RealityProbability of being the benign case
The request never left the local diebenign — reissue is safe
The request was delivered and executed; the response was lostreissue duplicates the effect
The request was delivered and is still executingreissue duplicates the effect
The response is queued locally behind a stalled consumerthe response is coming; reissue duplicates
The link is recovering and both will completereissue duplicates

One of five is benign. And the local side cannot distinguish them, because every distinguishing fact is on the other die or in a buffer the watchdog cannot see.

A timeout is an absence of information, not the presence of information. Treating it as evidence of non-delivery is the single most expensive inference in this chapter.

26. Wrong Recovery — Blind Reissue After a Timeout

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — and dangerously plausible, because it is what "retry" means
// everywhere else in engineering.
always_ff @(posedge clk) begin
  if (request_timeout[t]) begin
    reissue_request[t] <= 1'b1;      // "just send it again"
  end
end

Architecture. It treats a semantic request the way the transport treats a flit. But the transport's replay is safe precisely because the receiver suppresses duplicates below the semantic boundary (§24). A reissue from above enters as a new semantic request, and nothing suppresses it.

Failure, by request type:

A write is executed twice. If it is not idempotent — an increment, a doorbell, a queue push, a byte-enabled partial write over a value that has since changed — the system state is now wrong, permanently, with no error anywhere.

A read generates two responses for one identity. The first retires the outstanding entry; the second matches nothing, or matches a later transaction that reused the identity, delivering one requester's data to another. Chapter 12.2 §27 is that failure in full.

A coherence request may attempt an ownership transition twice, and Chapter 11.3 §16 showed what two ownership transactions on one line produce.

What a defensible timeout handler does instead. It stops treating the transaction as in-progress, marks it as failed with an unknown outcome — which is a different and more honest state than "not delivered" — reports it through the architecture's defined error path, and does not reissue unless the carried protocol's contract says a reissue is safe for that operation. For an idempotent read it often is. For a write it usually is not, and the correct answer is escalation rather than cleverness.

And the identity must not be recycled immediately. That is the other half, and it is Chapter 12.2 §26's subject: a timed-out identity reused for a new request will match a late response from the old one.

27. Diagnostic State: Age and First Failure

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE diagnostic state. Not a UCIe mechanism and NOT a UCIe timeout:
// no timeout value or escalation threshold is asserted anywhere. The width and
// the policy are engineering choices.
logic [AGE_W-1:0] request_age_q  [MAX_OUTSTANDING];   // ticks since accepted
logic [2:0]       first_stall_q  [MAX_OUTSTANDING];   // §32's taxonomy, encoded
logic [3:0]       retry_count_q  [MAX_OUTSTANDING];
logic [1:0]       last_event_q   [MAX_OUTSTANDING];   // stall / retry / recovery
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    for (int i = 0; i < MAX_OUTSTANDING; i++) begin
      request_age_q[i] <= '0;
      first_stall_q[i] <= STALL_NONE;
      retry_count_q[i] <= '0;
    end
  end else begin
    for (int i = 0; i < MAX_OUTSTANDING; i++) begin
      if (req_alloc[i]) begin
        request_age_q[i] <= '0;
        first_stall_q[i] <= STALL_NONE;     // fresh per request
        retry_count_q[i] <= '0;
      end else if (req_open_q[i]) begin
        // Saturate rather than wrap. A wrapped age reads as a young request,
        // which is the one reading that would hide the problem.
        if (request_age_q[i] != {AGE_W{1'b1}})
          request_age_q[i] <= request_age_q[i] + 1'b1;
 
        // FIRST stall source only — later stalls are consequences.
        if ((first_stall_q[i] == STALL_NONE) && stall_now)
          first_stall_q[i] <= stall_source;
 
        if (retry_now[i] && (retry_count_q[i] != 4'hF))
          retry_count_q[i] <= retry_count_q[i] + 1'b1;
      end
    end
  end
end

Classification: synthesizable, illustrative — and a design may reasonably implement only a subset, or only in a debug build.

Architecture. Four small fields per outstanding request, existing to answer §34's checklist in one read instead of by instrumenting the design after the failure.

State. Per-request. Note that first_stall_q is deliberately write-once per request: the first stall source is diagnostic gold and every subsequent stall is a consequence of it. A field that records the latest stall tells you where the request is now, which you already know.

Cycle behaviour. Age increments while open and saturates. The saturation direction is chosen deliberately: a wrapped counter reads as a young request, which is exactly the reading that would make the oldest request in the system look healthy.

Contract. A timeout policy, if the architecture has one, reads request_age_q. Debug reads all four.

Failure. Two of them, both about the counter rather than the request. Wrapping instead of saturating hides the oldest request. Recording the latest stall instead of the first destroys the attribution.

DV. Verify age saturates rather than wraps; verify first_stall_q does not change on a second stall; and verify all four fields clear on allocation rather than on retirement — allocation, because a stale value from a previous request in the same slot is worse than no value.

28. Backpressure Taxonomy

Five places a request can stall, and knowing which one is most of the diagnosis.

Stalled atCauseSymptomWhere to look
Protocol sourceno request to issuelink idle, nothing wrongabove this stack entirely
FDI boundaryboth skid slots occupiedsource sees ready low§8's occupancy
Adapter admissionreplay full, no credit, or link not operationalrequest presented and not accepted§10 — which term
RDI / PHYPHY internal queue full, or PHY not operationalAdapter holds a framed object§16's reasons
Remote sidefar-side buffer full, so credits stop returningcredits at zero locallyCh 9.5 — and note the cause is on the other die

The last row is the one that costs teams the most time, because the local symptom is "we have no credits" and the local instinct is to look at the local credit logic. The credit count is a message about the far side. A local investigation of a credit stall is looking at the messenger.

And the third row is the one where "which term" matters more than "stalled". Replay-full and credit-zero are both "the Adapter did not accept", and they mean completely different things: one is a local reliability resource, the other is remote buffer occupancy.

29. The Request Conservation Scoreboard

The invariant that makes "a request disappeared" a detectable event rather than a mystery.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOIN KEY — the verification-only monitor ID (§13). Required because both the
protocol identity and the transport sequence number are reused.
 
per request (by mon_id):
  accepted_at_fdi       : cycle
  stage_history[]       : (stage, entered_cycle) for every stage it occupied
  replay_allocations    : count — must be exactly 1
  transmissions         : count — may be > 1 under retry
  remote_accepts        : count — must be exactly 1
  disposition           : delivered | aborted_signalled | in_path
 
aggregate:
  accepted, remote_accepted, in_path, aborted

The conservation equation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
accepted  =  remote_accepted  +  in_path  +  aborted

Checked every cycle, and at end of test with in_path required to be zero.

Why this equation is the right one. It has no free variables. Every request the source handed down is in exactly one of three states, and there is no fourth. A request that is in none of them has been lost, and that is precisely the failure §19 produces and no count-range check detects.

The four checks it enables, and the bug each catches:

accepted = remote_accepted + in_path + aborted, continuously. Catches §19 — a lost request makes the left side exceed the right, immediately, in the cycle it is overwritten.

replay_allocations == 1 per request. Catches a replay that re-allocates on retry (Chapter 9.4 §8), and catches §11 from the other direction — a request with zero allocations was accepted without reliability capacity.

remote_accepts == 1 per request, with transmissions >= 1. Catches §24. The interesting rows in the log are those with transmissions > 1 and remote_accepts == 1, which is duplicate suppression working — and if that combination never appears in a regression, the mechanism has never been tested.

stage_history has no gaps. Every request occupied a contiguous chain of stages. A request that appears in the replay store with no framing entry has skipped a stage, which means two stages share state they should not.

On what the model must not do. It must not derive in_path by reading the design's occupancy counters — that is §19's suspect. It must count stage entries and exits from interface observations, so that a drifting design counter shows up as a disagreement rather than being adopted as truth.

30. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative request-flow coverage. Not UCIe-defined. Every bin exists to
// reach a specific failure named in this chapter.
covergroup cg_request_flow @(posedge clk iff req_event);
 
  cp_stall     : coverpoint first_stall_source {   // §28 — each of five
    bins none = {STALL_NONE};  bins fdi = {STALL_FDI};
    bins adapter = {STALL_ADAPTER};  bins phy = {STALL_PHY};
    bins remote = {STALL_REMOTE};
  }
  cp_veto      : coverpoint which_admission_veto;   // §10 — each term, alone
  cp_skid_occ  : coverpoint skid_occupancy { bins e = {0}; bins one = {1}; bins full = {2}; }
  cp_simul     : coverpoint push_pop_same_cycle;    // §18's 2'b11 row
  cp_simul_occ : coverpoint occupancy_at_simul;
  cp_replay    : coverpoint replay_occupancy { bins e = {0}; bins mid = {[1:$-1]}; bins full = {REPLAY_DEPTH}; }
  cp_retry     : coverpoint retry_count { bins none = {0}; bins one = {1}; bins many = {[2:$]}; }
  cp_dup       : coverpoint remote_saw_duplicate;   // §24
  cp_outstand  : coverpoint requests_in_flight { bins one = {1}; bins few = {[2:3]}; bins many = {[4:$]}; }
  cp_recovery  : coverpoint recovery_with_request_in_path;   // §23
  cp_timeout   : coverpoint request_timed_out;      // §25
  cp_write     : coverpoint request_is_write;
  cp_be        : coverpoint byte_enable_partial;    // §6 — partial writes
  cp_cdc       : coverpoint crossed_cdc_boundary;
 
  // THE cross §11 needs: replay full AND an error injected. Either alone
  // exercises nothing; together they reach the unrecoverable case.
  x_replay_retry  : cross cp_replay, cp_retry;
  // Simultaneous push/pop at every occupancy — §18/§19.
  x_simul_occ     : cross cp_simul, cp_simul_occ;
  // A duplicate arriving while several requests are in flight — §24 under load.
  x_dup_load      : cross cp_dup, cp_outstand;
  // A timeout on a WRITE, which is the case where reissue is unsafe (§26).
  x_timeout_write : cross cp_timeout, cp_write;
  // Recovery with requests at each stage — §23's four dispositions.
  x_recovery_stall: cross cp_recovery, cp_stall;
 
endgroup

Why x_replay_retry is the highest-value cross here. §11's bug requires two independent conditions that testbenches naturally separate: buffer-pressure tests run without error injection, and error-injection tests run under light load. The bug lives exactly in the intersection, and it is unreachable unless the intersection is a coverage goal.

And x_timeout_write is the bin that finds §26. A timeout on a read is recoverable by reissue and looks like the mechanism works. A timeout on a non-idempotent write is where blind reissue corrupts state, and it is a different bin.

31. Debug Checklist — a Request Disappeared

  1. Did the source handshake at FDI? If not, this is a backpressure question (§28), not a loss question.
  2. Which stage first owned it? §5's table — and stage_history in §29 answers it directly.
  3. Is a skid slot still occupied with it? §8's occupancy.
  4. Did the admission gate accept it, and under which terms? §10 — and if it was accepted, was replay_space actually high (§11)?
  5. Was a replay entry allocated, exactly once? §29's second check.
  6. Did the header and payload emitted together come from the same request? §15 — and this is checkable only with the monitor tag.
  7. Did the object cross RDI? And did the PHY accept it?
  8. Did a physical transfer occur? Below this point the question becomes Module 7's.
  9. Did the remote side reconstruct a complete object? Extent match, not a >=.
  10. Did the remote consumer accept it, exactly once? §24.
  11. Did a retry occur, and did it re-allocate anything? §17's third property.
  12. Did a reset or recovery occur while it was in the path? §23 — and which disposition was applied.
  13. Did any stage clear valid or free state early? §19 for counters; §17 for the replay store.
  14. Does the conservation equation balance? §29. If it does not, the answer is a missing request and the cycle it went missing.

Step 14 is the one to run first, not last. It is a single arithmetic check, it fires in the cycle the loss occurs, and it converts every other step from an investigation into a lookup.

32. Common Misconceptions

"A request moves through all layers in one handshake." It crosses at least four ownership boundaries, and at three of them the previous owner keeps state. The FDI handshake transfers responsibility for forward progress; it transfers nothing about recovery or semantics (§4).

"Ready can be chained through the entire stack." It cannot: the timing path spans the stack and crosses vendor boundaries, it couples independently-replaceable layers, and remote_ready does not exist as a wire — the far side's capacity arrives as registered credits at least a round trip old (§7).

"Once the PHY accepts, the Adapter can forget everything." The PHY owns transmission and the Adapter owns recovery, and those overlap in time. The replay copy is released on confirmation, not on transmission (§17).

"Queue space alone is enough to accept." Four independent vetoes, and the one that gets omitted — replay capacity — produces a failure that only appears when an error is injected while the replay store is full (§10, §11).

"Metadata can be pipelined separately from payload." Then the far side receives a well-formed request with the previous request's data, correctly CRC-protected, and executes it. The response even returns and matches correctly, so the bookkeeping is flawless (§15).

"Replay means the remote side should accept again." No. A lost confirmation makes the transport deliver the same object twice; the receiver must accept it once. Otherwise a write executes twice with no error anywhere (§24).

"Timeout means the request was not delivered." A timeout means no response arrived. Four of the five things that may actually have happened involve the request having been delivered, so blind reissue duplicates the effect (§25, §26).

"Reset may clear accepted requests freely." Four dispositions are legitimate — retain and retry, retain and wait, escalate, or flush with an explicitly signalled cancellation. Silent discard is not one of them, and an unsignalled flush is a silent discard with extra steps (§23).

"If every FIFO count is legal, no request can be lost." Two independent count updates lose one count per simultaneous push and pop, drifting the count downward until the queue accepts a request it cannot hold. Every count stays a small legal number throughout (§19).

"The transport waits for the Protocol Layer to have something to send." It does not — a flit is framed and sent regardless, with a payload field carrying zeros if the Protocol Layer supplied nothing. Gating the transmit path on having payload starves the piggybacked credit return and builds a self-sustaining stall (§9, §12).

"The protocol tag and the transport sequence number are interchangeable." Three identity spaces, three owners, three lifetimes — and the third exists only in the testbench because the first two are reused and therefore cannot answer "is this the same object?" (§13).

33. Understanding Check

34. Summary and What Comes Next

A request is passed through ownership boundaries, not copied through layers.

The architecture: two named interfaces — FDI between Protocol Layer and Adapter, RDI between Adapter and Physical Layer — across which ownership changes hands between blocks that may come from different vendors. The Adapter packetizes, adding a 2-byte header and a 2-byte CRC, with flit size decided at negotiation and an 8-bit Ack/Nak sequence number that is a transport identity and not the protocol's. The PHY's unit is a Module, 1, 2 or 4 forming a Link, with Data, Valid and Track lanes and an always-on sideband. And the transport frames and sends whether or not the Protocol Layer supplied a flit, which decouples the framing cadence from the request cadence and keeps piggybacked credit return alive.

The mechanisms: a registered-ready skid buffer at the boundary, because a chained ready is a combinational path across the whole stack and a lockstep pipeline that cannot cover the round trip. Four independent admission vetoes, of which replay capacity is the one that gets omitted and the one whose omission is only visible under error injection. One bundled object with one enable, because a header paired with the previous payload is a validly CRC-protected wrong request whose response even matches correctly. An explicit case over push and pop, because two independent count updates drift the occupancy downward until a request is overwritten. And overlapping ownership — the PHY owns transmission while the Adapter owns recovery, and the replay copy is released on confirmation.

The insight worth carrying furthest: a timeout is an absence of information, not the presence of information. Four of the five things that may have happened involve the request having been delivered, so blind reissue duplicates the effect — and for a non-idempotent write that is permanent corruption with no error anywhere.

And the check to write first: accepted = remote_accepted + in_path + aborted. One arithmetic statement that fires in the cycle a request is lost, and turns every other step of the debug checklist from an investigation into a lookup.

The remote side now owns the request. The transaction is not complete. A response may return much later, through a different set of queues, with independent backpressure, in an order that may not match issue order, and it must find the one outstanding entry that created the obligation — and retire it exactly once:

Browse the full path on the UCIe tutorials index.