Skip to content

UCIe · Module 9

Streaming Reliability

What a streaming transmitter must retain so corruption is recoverable without loss, duplication, or reordering — the retirement point, replay-buffer lifecycle and occupancy, CRC alignment, retry without duplicate allocation, duplicate suppression, retry storms, and exactly-once verification.

Chapter 9.2 preserved message boundaries. Chapter 9.3 constrained the order in which packets may be observed. Both assumed the packet arrives.

This chapter removes that assumption, and the consequence is larger than it first appears. Once a packet can be corrupted in flight, a transmitter that has handed a packet to the link cannot forget it — because it may have to send it again. That single requirement generates a data structure, a pointer discipline, an occupancy invariant, a retry state machine, an interaction with ordering, and an entire class of bugs whose symptom is data that is silently lost, silently duplicated, or silently reordered.

It is also the chapter where the two mechanisms this module has built so far stop being independent.

1. The One-Sentence Model

Reliability is memory. A transmitter cannot retry what it has already forgotten, so every reliability architecture has a retirement point — the moment it becomes safe to stop remembering.

The retirement point is later than instinct suggests, and getting it wrong is the chapter's central bug. A packet can be:

  • generated by the source,
  • accepted into the transmit path,
  • transmitted onto the link,
  • physically received at the far die,

and still not be safe to discard — because none of those events proves it arrived intact, and the transmitter is the only entity that can still fix it if it did not.

2. What UCIe Provides

3. CRC Detects; It Does Not Repair

The distinction that structures everything downstream.

A CRC computed over a transport object and checked at the far end answers exactly one question: did these bytes arrive as sent? It does not identify which bit was wrong, does not repair anything, and does not tell the transmitter anything — the detection happens at the receiver, while the only party that can fix it is the transmitter.

So detection is only the first step of a three-part mechanism:

StepWhereWhat it produces
Detectreceivera corrupted object, which must not be delivered upward
Signalreceiver → transmitterthe fact that recovery is needed
Recovertransmitterthe retained copy, sent again

The middle step is what makes retained state necessary. Between transmitting and learning the outcome there is a round trip, and everything sent during that window is unresolved. The replay buffer's depth is, fundamentally, the amount of traffic that can be in that window.

A CRC with no retained copy behind it is an error detector, not a reliability mechanism. It tells you data was lost; it does not stop the loss.

4. The Replay Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative streaming RTL — not UCIe normative naming or encoding.
// A ring of retained packets awaiting confirmation.
localparam int REPLAY_DEPTH = 16;
localparam int RP_PTR_W     = $clog2(REPLAY_DEPTH);     // power-of-two depth
localparam int RP_CNT_W     = $clog2(REPLAY_DEPTH + 1);
localparam int SEQ_W        = 8;                         // internal identity
 
typedef struct packed {
  logic              valid;      // this slot holds an unretired packet
  logic [SEQ_W-1:0]  seq;        // implementation-internal identity
  stream_packet_t    packet;     // the retained copy itself
} replay_entry_t;
 
replay_entry_t          replay_mem [REPLAY_DEPTH];
logic [RP_PTR_W-1:0]    rp_alloc_q;    // next slot to allocate into
logic [RP_PTR_W-1:0]    rp_send_q;     // next slot to transmit from
logic [RP_PTR_W-1:0]    rp_retire_q;   // oldest unretired slot
logic [RP_CNT_W-1:0]    rp_count_q;    // occupancy: allocated, not retired

Architecture. A packet must be retained from the moment the transmitter takes responsibility for it until the moment it is safe to forget. A ring buffer with three pointers expresses exactly that lifecycle.

State. A memory of retained packets and three pointers — and the third is the one people omit. rp_alloc_q and rp_retire_q bound the retained set. rp_send_q is separate because transmission position and retention position are different things: after a retry, the send pointer moves backwards to re-transmit while the retire pointer does not move at all.

Cycle behaviour. Allocation advances rp_alloc_q and increments the count. Transmission advances rp_send_q and changes nothing else. Retirement advances rp_retire_q and decrements the count. A retry rewinds rp_send_q to rp_retire_q and touches neither the count nor the other pointers.

Contract. The producer may only be accepted when a slot is free (§11). The far end's confirmation is what authorises retirement.

Failure. With two pointers instead of three, a retry cannot express "go back and send again without un-allocating" — and designs that try usually end up decrementing the count, which is §8's bug.

Why the sequence field is SEQ_W wide and labelled internal. It identifies a retained slot for the retry logic. Do not assume such a value appears on the wire; the transport carries whatever the specification defines, and this one is the implementation's own bookkeeping unless the contract says otherwise.

A replay ring buffer with three pointers. The retire pointer marks the oldest unretired packet, the send pointer marks how far transmission has progressed, and the alloc pointer marks the next free slot. Packets between retire and send are transmitted but unconfirmed; packets between send and alloc are allocated but not yet transmitted.retire pointeroldest unretiredsend pointertransmission frontalloc pointernext free slotsent, unconfirmeda retry re-sends thesenot yet sentallocated, waitingrewind on retry12
Figure 1 — the three pointers and what each one means. Everything between retire and alloc is retained and cannot be forgotten. The send pointer sits inside that window and marks how far transmission has got: packets between retire and send have been transmitted but not confirmed, and are exactly the population a retry re-sends. A retry rewinds send back to retire without touching alloc or the occupancy count — which is why the buffer needs three pointers and why a retry must never look like a new allocation.

5. The Entry Lifecycle

Five states, and naming them is what prevents the retirement bug:

StageTriggerOccupancyPointers moved
Allocateproducer's packet accepted+1alloc
Transmitpacket handed to the linkunchangedsend
Waitunchangednone
Retryrecovery signalledunchangedsend rewinds
Retireconfirmed safe−1retire

The two rows that carry the chapter: transmit does not change occupancy, and retry does not change occupancy. Only allocate and retire do. That single invariant is §8's assertion and rules out most replay-buffer bugs by construction.

6. The Retirement Bug

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the retained copy is discarded when the packet is transmitted.
always_ff @(posedge clk) begin
  if (tx_fire) begin
    replay_mem[rp_send_q].valid <= 1'b0;
    rp_count_q <= rp_count_q - 1'b1;
  end
end

Physical transmission is not delivery. The packet is on the wire, its outcome is unknown, and the only copy that could recover it has just been erased. When corruption is signalled a few cycles later, there is nothing to replay — and the failure mode is total: the packet is gone, the receiver never got a valid copy, and no layer can reconstruct it.

The symptom is silent, permanent loss under a condition that only occurs when the link has errors. A clean-link bring-up looks perfect. The first marginal channel in the field loses data.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — retention until retirement, with three distinct movements.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    rp_alloc_q  <= '0;
    rp_send_q   <= '0;
    rp_retire_q <= '0;
    rp_count_q  <= '0;
    for (int i = 0; i < REPLAY_DEPTH; i++) replay_mem[i].valid <= 1'b0;
  end else begin
    // 1. Allocate — the ONLY place occupancy increases.
    if (alloc_fire) begin
      replay_mem[rp_alloc_q].valid  <= 1'b1;
      replay_mem[rp_alloc_q].seq    <= next_seq_q;
      replay_mem[rp_alloc_q].packet <= alloc_packet;
      rp_alloc_q <= rp_alloc_q + 1'b1;
      rp_count_q <= rp_count_q + 1'b1;
    end
 
    // 2. Transmit — moves ONLY the send pointer. Nothing is freed.
    if (tx_fire) rp_send_q <= rp_send_q + 1'b1;
 
    // 3. Retry — rewind transmission; retention and occupancy untouched.
    if (retry_trigger) rp_send_q <= rp_retire_q;
 
    // 4. Retire — the ONLY place occupancy decreases.
    if (retire_fire) begin
      replay_mem[rp_retire_q].valid <= 1'b0;
      rp_retire_q <= rp_retire_q + 1'b1;
      rp_count_q  <= rp_count_q - 1'b1;
    end
  end
end

Architecture. Each pointer has exactly one mover, and occupancy has exactly two. That structure makes the invariants checkable and the bugs hard to write.

State. As §4.

Cycle behaviour. Allocation and retirement can occur in the same cycle, in which case the count is unchanged — worth writing explicitly if both can fire together, exactly as the FIFO cases in earlier chapters.

Contract. The retry logic requires that everything from rp_retire_q forward is still valid and intact.

Failure. The version above; also note that retry_trigger is placed after tx_fire in the block, so a retry arriving in the same cycle as a transmission wins. That ordering is a real decision, not an accident — retrying is the safe direction.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a transmitted, unretired packet is still retained.
property p_transmitted_packet_retained;
  @(posedge clk) disable iff (!rst_n)
    tx_fire |=> replay_mem[$past(rp_send_q)].valid;
endproperty

This is the most important assertion in the chapter. It catches clear-on-transmit directly, and it catches subtler variants — an entry invalidated by an unrelated cleanup path, or a reset that clears the memory without clearing the pointers.

7. CRC Must Be Aligned With What It Describes

A pipeline bug with the same shape as Chapter 9.2 §12, and worse consequences.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — CRC computed over a pipelined datapath, metadata not delayed to match.
always_ff @(posedge clk) crc_q     <= crc_of(pkt_in);      // 3-stage internally
always_ff @(posedge clk) pkt_id_q  <= pkt_in.id;           // 1 stage
// The CRC of packet N is attached to packet N-2's identity.

Every packet then carries a CRC computed over different data. The receiver checks packet N against a CRC belonging to N−2, and the check fails on every packet — which is at least loud. The nastier variant is a one-stage misalignment on a stream of identical packets, where the CRC happens to match and the error only appears when the payload changes.

The fix is the same as before: carry the data and its metadata as one object through identical staging.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the packet and its computed CRC travel together.
typedef struct packed {
  stream_packet_t    packet;
  logic [CRC_W-1:0]  crc;
  logic              crc_valid;
} crc_result_t;
 
crc_result_t crc_pipe_q [CRC_LATENCY];    // one shift register, one object

Architecture. A CRC has no meaning apart from the bytes it was computed over, so the two must be inseparable in the pipeline.

State. A shift register of bundled objects, CRC_LATENCY deep.

Cycle behaviour. Both fields advance under one enable, so misalignment is structurally impossible.

Contract. Whatever transmits the packet uses the CRC from the same struct entry.

Failure. The separated version; and note the CRC generator itself is typically a separate block — this chapter deliberately does not implement a polynomial, because the specification's polynomial and width belong to the specification, and an invented one would be worse than none.

DV — the technique worth reusing. Attach a test-only identifier to each packet at the input, carry it alongside in the testbench, and assert it matches at the CRC output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative, verification-only — a tag that exists in simulation only.
property p_crc_aligned_with_packet;
  @(posedge clk) disable iff (!rst_n)
    crc_out_valid |-> (crc_out_tag == expected_tag_at_output);
endproperty

This catches misalignment regardless of whether the payload happens to be identical, which functional comparison does not.

8. Retry Is Not Allocation

The second structural bug, and the one that corrupts the buffer rather than losing a packet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a replay treated as a new packet.
if (retry_trigger) begin
  replay_mem[rp_alloc_q] <= replay_mem[rp_retire_q];   // a second copy
  rp_alloc_q <= rp_alloc_q + 1'b1;
  rp_count_q <= rp_count_q + 1'b1;                     // occupancy grows
end

The same packet now occupies two slots. Occupancy is overstated, so the buffer fills with duplicates of itself; a second retry makes three copies; and eventually either the buffer overflows or the duplicate copies are transmitted, delivering the same packet twice.

Retry re-transmits from retained state. It does not create retained state. That is the whole of §5's occupancy column.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — occupancy invariants that make the buffer trustworthy.
property p_occupancy_never_overflows;
  @(posedge clk) disable iff (!rst_n)
    rp_count_q <= RP_CNT_W'(REPLAY_DEPTH);
endproperty
 
property p_retry_preserves_occupancy;
  @(posedge clk) disable iff (!rst_n)
    (retry_trigger && !alloc_fire && !retire_fire) |=> $stable(rp_count_q);
endproperty
 
property p_retire_requires_outstanding;
  @(posedge clk) disable iff (!rst_n)
    retire_fire |-> (rp_count_q != '0);
endproperty
 
property p_no_alloc_when_full;
  @(posedge clk) disable iff (!rst_n)
    alloc_fire |-> (rp_count_q != RP_CNT_W'(REPLAY_DEPTH));
endproperty

Four small properties covering overflow, underflow, retry neutrality, and allocation legality. Together they make the buffer's state machine essentially self-proving — and p_retry_preserves_occupancy is the one that catches the bug above the first time it fires.

9. Retry Must Preserve Ordering

The interaction Chapter 9.3 §10 flagged and deferred. This is where it is resolved.

Packets A0 and A1 have both been transmitted. A0 is corrupt; A1 arrived intact. If the receiver delivers A1 and A0 arrives afterwards by replay, the consumer has observed the stream out of order — a violation produced by the recovery mechanism.

The go-back-N structure in §6 solves it on the transmit side, and it is worth seeing why:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (retry_trigger) rp_send_q <= rp_retire_q;

Rewinding the send pointer to the oldest unretired packet means everything from the failure point onward is retransmitted in its original order. The receiver may discard what it already holds from that window, because it will receive it again correctly ordered. No reorder buffer, no sequence-based sorting, no receive-side holding.

The costs are real and worth naming:

Go-back-N (rewind to oldest)Selective retry (resend only the bad one)
Retransmitted on one erroreverything unconfirmedone packet
Receiver complexityminimal — discard and re-receivereorder buffer, holding logic
Ordering preservedstructurallyonly if the receiver reorders
Bandwidth on errorhigherlower
Best whenerrors rare, window smallerrors frequent, window large

Which the UCIe streaming reliability contract uses is a specification question, and this chapter does not assert one. The transferable point is the constraint: the ordering domain determines what the retry mechanism must do, and the two cannot be designed apart. A selective-retry transmitter paired with a receiver that does not reorder produces exactly the violation above — and each half looks correct in isolation.

10. A Worked Retry

Two outstanding packets, oldest fails. Illustrative timing; the round-trip latency is architecture-specific.

CycleEventallocsendretirecountRetained
1A0 accepted1001A0
2A0 transmitted1101A0
3A1 accepted2102A0, A1
4A1 transmitted2202A0, A1
5–7in flight; outcome unknown2202A0, A1
8A0 corrupt — retry signalled2002A0, A1
9A0 re-transmitted2102A0, A1
10A1 re-transmitted2202A0, A1
13A0 confirmed2211A1
14A1 confirmed2220

Four readings:

Cycle 8 rewinds send and leaves count at 2. That is the whole of §8 in one row — no allocation, no occupancy change.

Cycle 10 re-transmits A1, which was never corrupt. That is go-back-N's cost, and it is the price of the receiver needing no reorder buffer.

A0 is retained from cycle 1 to cycle 13 — twelve cycles, spanning two transmissions. Retention is bounded by confirmation, not by transmission.

Occupancy returns to zero only at cycle 14. Any check on the buffer draining is a check on confirmations arriving, not on packets being sent.

A source hands two packets to a transmit replay buffer, which transmits both. The receiver detects a CRC error on the first, discards it, and signals a retry. The transmitter replays both packets in order, the receiver validates and delivers them to the sink, and the transmitter then retires the retained copies.Corruption, replay, and retirement — conceptual exchangeSourceTX replayLinkRX AdapterSinkA0 then A1send A0send A1A0 corruptnothing deliveredretry neededreplay A0replay A1A0 then A1confirmedretire A0, A1
Figure 2 — the same retry as a message exchange. The two points worth tracing are that the transmitter still holds A0 at the moment corruption is detected — which is only true because transmission did not retire it — and that retirement happens at the end, after confirmation, not when the packet left. The receiver discards the corrupt copy rather than delivering it upward, so the consumer never observes a bad packet and never observes A1 before A0. Message names are conceptual, not UCIe encodings.

11. Readiness Must Include Reliability Capacity

A cross-layer bug that is easy to write and produces overflow rather than backpressure.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — readiness derived from the physical transmitter alone.
assign stream_ready = phy_ready;

The PHY can be perfectly ready to accept another packet while the replay buffer is full. Accepting one then either overwrites a retained packet — destroying the ability to recover it — or advances the allocation pointer past the retire pointer, corrupting the ring.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — accept only when the packet can be BOTH sent and retained.
assign replay_space  = (rp_count_q != RP_CNT_W'(REPLAY_DEPTH));
assign stream_ready  = phy_ready && replay_space;

Architecture. Accepting a packet incurs two obligations: transmit it, and retain it until confirmed. Readiness must reflect both.

State. The occupancy counter.

Cycle behaviour. Combinational over registered occupancy.

Contract. The producer relies on stream_ready genuinely meaning the packet will be handled.

Failure. Ring corruption or silent overwrite of a retained packet — and the second is particularly cruel, because it destroys recoverability at exactly the moment the link is busy enough for errors to matter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — nothing is accepted without a slot to retain it in.
property p_accept_requires_replay_capacity;
  @(posedge clk) disable iff (!rst_n)
    alloc_fire |-> replay_space;
endproperty

This is where reliability meets flow control, and it is worth being precise about the boundary. Reliability asks does this packet need to be replayed? Flow control asks is the receiver able to accept another packet? They are different questions with different state, and they interact because retained state is finite — a full replay buffer backpressures the producer just as surely as a full receiver does. The credit mechanism that manages the receiver's side is Chapter 9.5's subject; this chapter needs only the local resource constraint.

12. Duplicates Must Not Reach the Consumer

A subtle case with a real cause: the packet arrived intact, but the confirmation was lost. The transmitter, having no evidence of success, replays. The receiver now sees the same packet twice.

Loss and duplication are equally serious, and a mechanism that prevents one by causing the other has not helped. So the receiver needs some way to recognise a repeat.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — receive-side duplicate suppression. NOT a UCIe-defined field;
// this works only if the contract provides an identity the receiver can use.
logic [SEQ_W-1:0] last_delivered_seq_q;
logic             seq_initialised_q;
logic             is_duplicate;
 
assign is_duplicate = seq_initialised_q &&
                      seq_lte(rx_seq, last_delivered_seq_q);   // §13
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    last_delivered_seq_q <= '0;
    seq_initialised_q    <= 1'b0;
  end else if (deliver_fire) begin
    last_delivered_seq_q <= rx_seq;
    seq_initialised_q    <= 1'b1;
  end
end

Architecture. "Send again" and "deliver twice" must be distinguishable, and only the receiver can distinguish them.

State. The last delivered identity plus an initialised flag — per-link lifetime, re-established after a reset or a recovery that resets the identity space.

Cycle behaviour. Updated on delivery; compared on arrival.

Contract. Requires an identity the receiver can compare, which the reliability contract must provide. If it does not, duplicate suppression must come from somewhere else — from the protocol above, or from a mechanism that makes duplicates impossible rather than detectable.

Failure. Without it, a lost confirmation delivers a packet twice, and for a protocol where messages are not idempotent that is a correctness failure indistinguishable from a source bug.

Note seq_initialised_q. Without it, the very first packet compares against a reset value of zero and may be misjudged a duplicate — a classic first-packet-after-reset bug that only appears once per link bring-up and is therefore easy to dismiss as noise.

13. Comparing Wrapping Identities

If the identity field is finite it wraps, and naive comparison breaks exactly at the wrap.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — plain magnitude comparison across a wrapping space.
assign is_older = (rx_seq < last_delivered_seq_q);
// With SEQ_W = 8: after 255 comes 0, and 0 < 255 says the new packet is older.

Every packet immediately after a wrap is classified as a duplicate and discarded. The link works perfectly for 256 packets and then loses one, repeatedly and periodically — a signature that looks like a marginal link and is arithmetic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — window-limited comparison over a modular sequence space.
// Valid only while the outstanding window is strictly less than half the space.
function automatic logic seq_lte(logic [SEQ_W-1:0] a, logic [SEQ_W-1:0] b);
  // "a is at or before b" if the forward distance from a to b is small.
  return ((b - a) < (SEQ_W'(1) << (SEQ_W-1)));
endfunction

Architecture. Modular arithmetic has no total order, so "older" only means anything within a bounded window.

State. None — a pure function.

Contract. The window must be smaller than half the sequence space, which is a real constraint linking SEQ_W to REPLAY_DEPTH. With an 8-bit identity the outstanding window must stay below 128; a 16-entry replay buffer satisfies that comfortably, and it is worth an elaboration check rather than an assumption.

Failure. Using magnitude comparison, or sizing the sequence space too close to the window, produces the periodic-loss signature above.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the identity space must be large enough for the window.
generate
  if (REPLAY_DEPTH >= (1 << (SEQ_W-1)))
    $fatal(1, "REPLAY_DEPTH=%0d too large for SEQ_W=%0d — modular comparison unsafe.",
           REPLAY_DEPTH, SEQ_W);
endgenerate

Catching it at elaboration is free; catching it in the field takes a periodic-loss investigation.

14. The Corrupted Packet Must Not Be Delivered

The receiver's core obligation, and one worth asserting despite seeming obvious.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a packet failing its integrity check is never delivered upward.
property p_corrupt_packet_not_delivered;
  @(posedge clk) disable iff (!rst_n)
    (rx_packet_valid && rx_crc_error) |-> !deliver_fire;
endproperty

Why it needs stating. The check and the delivery are usually in different pipeline stages, and the natural bug is a delivery path that qualifies on rx_packet_valid without waiting for the check result — so a corrupt packet is delivered one cycle before the error is known. The assertion above is only correct if both signals refer to the same pipeline stage, which is itself worth verifying: writing it forces you to check that the error and the packet are aligned, which is §7's problem in a different place.

The end-to-end version is stronger and belongs in the testbench:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative, verification-only — an injected corruption never reaches the sink.
property p_injected_error_never_delivered;
  @(posedge clk) disable iff (!rst_n)
    (deliver_fire && (delivered_tag inside corrupted_tags)) |-> 1'b0;
endproperty

Using a test-only tag rather than data comparison catches the case where a corrupted packet happens to be delivered and subsequently replayed correctly — where a data check would pass on the second copy and never notice the first was delivered.

15. Retry Storms and Escalation

A persistent physical fault produces send → fail → retry → fail, indefinitely. Forward progress collapses while every mechanism behaves correctly — the same shape as Chapter 7.6's recalibration storm.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — bounded retry with escalation and preserved diagnosis.
localparam int MAX_RETRY = 8;                    // illustrative, not normative
 
logic [$clog2(MAX_RETRY+1)-1:0] pkt_retry_q;     // per-packet, cleared on retire
logic [15:0]                    total_retry_q;   // per-link, saturating
logic                           escalate;
 
typedef enum logic [1:0] {
  REL_FAIL_NONE = 2'd0,
  REL_FAIL_CRC  = 2'd1,   // a CRC error started this
  REL_FAIL_TMO  = 2'd2,   // no outcome arrived in time
  REL_FAIL_MAX  = 2'd3    // retries exhausted
} rel_fail_t;
 
rel_fail_t first_fail_q;                          // per-link, sticky
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    pkt_retry_q   <= '0;
    total_retry_q <= '0;
    first_fail_q  <= REL_FAIL_NONE;
  end else begin
    if (retire_fire)              pkt_retry_q <= '0;          // per-packet
    else if (retry_trigger)       pkt_retry_q <= pkt_retry_q + 1'b1;
 
    if (retry_trigger && !(&total_retry_q))
      total_retry_q <= total_retry_q + 1'b1;                  // per-link, saturating
 
    if ((first_fail_q == REL_FAIL_NONE) && fail_event)
      first_fail_q <= fail_cause;                             // FIRST, not last
  end
end
 
assign escalate = (pkt_retry_q == MAX_RETRY);   // hand off to link recovery

Architecture. Retrying is correct for transient errors and futile for persistent ones, and only a bound distinguishes them.

State. Three counters with three different lifetimes — per-packet retries cleared on retirement, a per-link saturating total for telemetry, and a sticky first-failure cause. That classification is the point of the block.

Cycle behaviour. The per-packet counter clears on retirement rather than on success-of-transmission, so it counts retries of this packet.

Contract. escalate hands off to the link-level recovery of Chapter 8.6 — reliability does not attempt to fix a broken channel.

Failure. Without the bound, a persistent fault consumes the link forever with no error reported. Without the per-link total, a link that retries constantly but always succeeds looks perfectly healthy — Chapter 7.6's point that marginality is a rate. Without first_fail_q capturing the first cause, a CRC failure followed by a retry timeout records TMO, and the investigation starts at the timeout instead of the channel (Chapter 8.5 §11).

16. Verifying Exactly-Once

The methodological core, and where assertions and scoreboard divide cleanly.

Assertions prove local invariants: occupancy bounded, retention until retirement, retry occupancy-neutral, no allocation when full, corrupt packet not delivered, CRC aligned. Each is cheap, runs everywhere, and catches a specific structural bug.

A scoreboard proves the end-to-end property that no assertion can express: exactly once, in order, unmodified.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
source_queue[stream]     — every message the producer offered, in order
transport_model          — allocated / transmitted / unconfirmed / retired
sink_queue[stream]       — every message the consumer received

The invariant checked at the end of the test, and continuously: sink_queue equals source_queue, per stream, under any number of injected recoverable errors. Not "the data was correct" — the sequence of messages is identical, which simultaneously covers loss (sink short), duplication (sink long or a repeat), reordering (same set, wrong sequence), and corruption reaching the consumer.

The transport model is worth maintaining separately because it lets the scoreboard say why: when the sink diverges, knowing that the diverging message was in the unconfirmed window at the time of the error localises the bug immediately.

Error injection, and what each case is for:

InjectionTargets
Corrupt one packet, nothing else outstandingthe basic detect-and-replay path
Corrupt the oldest of several outstandinggo-back-N rewind; §9's ordering interaction
Corrupt the newest of several outstandingthat older confirmed packets are not needlessly resent
Corrupt the same packet repeatedlythe retry counter and escalation (§15)
Corrupt a replayed packetthat a retry of a retry does not double-allocate
Lose a confirmationduplicate suppression (§12)
Fill the replay buffer completelythat readiness includes replay capacity (§11)
Error at sequence wrap§13's modular comparison
Recovery during an outstanding windowChapter 8.6 §14 — retained work must survive or be reported
Reset during an outstanding windowthat both ends re-baseline together

The last two are the ones regressions usually lack, and they are where reliability meets the link-state machine.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative streaming-reliability coverage — not UCIe-defined.
covergroup cg_reliability @(posedge clk iff (retry_trigger || retire_fire));
 
  cp_retries : coverpoint pkt_retry_q {
    bins none = {0}; bins one = {1}; bins few = {[2:MAX_RETRY-1]};
    bins exhausted = {MAX_RETRY};
  }
  cp_occupancy : coverpoint rp_count_q {
    bins empty = {0}; bins low = {[1:REPLAY_DEPTH/2]};
    bins high  = {[REPLAY_DEPTH/2+1 : REPLAY_DEPTH-1]};
    bins full  = {REPLAY_DEPTH};
  }
  cp_position : coverpoint retry_target_position;   // oldest / middle / newest
  cp_wrap     : coverpoint (rp_retire_q == '0);
  cp_cause    : coverpoint first_fail_q;
 
  // Did a retry ever occur with the buffer nearly or completely full?
  x_retry_by_occ  : cross cp_retries, cp_occupancy;
  // Was every retry position exercised — oldest is the ordering-critical one?
  x_pos_by_retry  : cross cp_position, cp_retries;
 
endgroup

Why x_retry_by_occ. A retry with one packet outstanding exercises almost none of the mechanism. A retry with a full buffer exercises rewind across the whole window, the interaction with readiness, and the wrap — and it is the configuration a busy link actually runs in.

17. Diagnostic Signatures

SymptomLikely causeFirst move
CRC errors rising, retries succeedmarginal physical linkChapter 7.6 — margin, PVT sweeps
CRC clean, semantic data wrongframing or ordering, not reliabilityChapters 9.2, 9.3
Replay buffer persistently fullconfirmations not arriving, or window undersizedcheck retirement path, then §11
Same message delivered twiceduplicate suppression or premature retirement§12, then the retire trigger
Message disappears after an errorretired before it was safe§6 — the clear-on-transmit bug
Younger message delivered before olderretry/ordering interaction§9 — is the receiver reordering?
Periodic loss every 2^SEQ_W packetsmodular comparison§13
Retries constant but link "healthy"persistent marginality masked by success§15 — read the per-link total
Occupancy grows and never drainsretry allocating duplicates§8 — the occupancy assertion

Two rows deserve emphasis. "Message disappears after an error" is the signature of the retirement bug, and it is the most damaging because the data is unrecoverable. And "retries constant but the link is healthy" is the one that ships — everything works, throughput is a little low, and only the per-link retry total reveals the link is spending its bandwidth on resends.

18. Debug Checklist

  1. Was the source packet accepted? If not, stream_ready is low — check whether it is the PHY or the replay buffer (§11).
  2. Was a replay slot allocated? Occupancy should have incremented exactly once.
  3. Was the CRC computed over this packet? §7 — check alignment with a tag, not with data.
  4. Was the packet transmitted? Send pointer advanced; occupancy unchanged.
  5. Did the receiver validate it? Distinguish "arrived corrupt" from "never arrived".
  6. If corrupt, was it blocked from delivery? §14 — check the stage alignment of the error and the packet.
  7. Was a retry triggered, and by what? CRC, timeout, or link event — the causes escalate differently.
  8. Did the transmitter still hold the packet? §6 — the single most likely defect.
  9. Did the retry allocate a new entry? §8 — check occupancy across the retry.
  10. Did the send pointer rewind to the retire pointer? §9 — a rewind to the wrong place breaks ordering.
  11. Was ordering preserved on replay? Compare delivery order against source order for that stream.
  12. Was the packet retired only after confirmation? Not after transmission.
  13. Did occupancy return to the expected value? A drift of one per error is a retire/alloc imbalance.
  14. Was anything delivered twice? §12 — and check whether a confirmation was lost.
  15. Did retries escalate appropriately? §15 — and what does the per-link total say?
  16. What was the first recorded failure cause? Not the last (§15).

Steps 1 to 4 are readable from state and localise most failures. Step 8 is the one to reach for when data has vanished entirely.

19. Common Misconceptions

"CRC makes the link reliable." CRC detects; it does not repair, and the detection happens at the end that cannot fix it. Without retained state behind it, CRC is an error detector that tells you data was lost (§3).

"Once transmitted, a packet can be discarded." Transmission proves nothing about arrival. The retirement point is confirmation, and clearing on transmit destroys recoverability entirely (§1, §6).

"Retry is just sending the packet twice." Retry re-transmits from retained state without creating any. A retry that allocates a second entry corrupts the buffer and eventually delivers duplicates (§8).

"Retry does not interact with ordering." A replayed packet arriving after a younger one violates the ordering the retry was protecting. The two mechanisms constrain each other (§9).

"A clean CRC proves the packet is framed correctly." It proves the bytes arrived as sent. Framing is Chapter 9.2's problem and fails with CRC passing.

"The receiver can deliver later packets while an older same-stream retry is unresolved." Not under per-stream ordering — that is precisely the violation (§9).

"Replay buffers only affect performance." A full replay buffer must backpressure the producer; if readiness ignores it, retained packets are overwritten and become unrecoverable (§11).

"Flow control and retry are the same mechanism." Reliability asks whether a packet needs replaying; flow control asks whether the receiver can accept one. Different state, different questions, and they interact only through finite resources (§11).

"A timeout after a CRC failure should overwrite the cause." Then the recorded cause is the symptom and the investigation starts in the wrong place. Preserve the first (§15).

"SVA alone proves exactly-once delivery." Assertions prove local invariants. Exactly-once, in order, unmodified is an end-to-end property that needs a scoreboard (§16).

20. Understanding Check

21. Summary and What Comes Next

Reliability is memory. A transmitter cannot retry what it has forgotten, so the architecture is organised around a retirement point — and that point is confirmation, not transmission. Clearing a retained copy when the packet is handed to the link is the chapter's most damaging bug, producing silent unrecoverable loss that appears only on links with errors.

CRC detects; it does not repair, and it detects at the end that cannot fix anything — so detection, signalling, and recovery are three steps, and the round trip between them is what makes retained state necessary and sets the window size.

The structure: a ring with three pointers, because transmission position and retention position differ. Each pointer has one mover; occupancy has exactly two, allocate and retire — so transmit does not change occupancy and neither does retry. That single invariant, asserted, rules out most replay bugs. CRC must be bundled with the data it describes, or it describes a different packet. Readiness must include replay capacity, or retained packets are overwritten exactly when the link is busy enough for errors to matter.

Retry and ordering constrain each other. Go-back-N rewinds to the oldest unretired packet and preserves order structurally, at the cost of resending packets that were fine; selective retry is cheaper and requires the receiver to reorder. Pairing the wrong halves produces a violation in which each half looks correct.

Two traps at the edges: a lost confirmation causes duplication, so a receiver needs an identity to recognise repeats — compared with window-limited modular arithmetic, since magnitude comparison loses exactly one packet per sequence wrap. And retries must be bounded, with per-packet, per-link, and first-cause state at three different lifetimes.

Finally the verification split: assertions prove local invariants; only a scoreboard proves exactly-once, in order, unmodified — and the injection cases that matter are the ones regressions omit.

Reliability explains why a transmitted packet stays stored long after it leaves the wire. That retained state is finite, and this chapter has already shown a producer being backpressured because the replay buffer is full. Accounting for the resources on both sides of the link, before a sender creates work the link cannot hold, is a mechanism of its own:

Browse the full path on the UCIe tutorials index.