UCIe · Module 19
Adapter Design
What the UCIe Adapter must retain so a transport object can be flow-controlled, protected, retried and delivered exactly once — reservation before ownership transfer, staging and replay as separate lifetimes, a ring whose allocation pointer must never pass retirement, CRC that must stay aligned with the object it covers, duplicate suppression that needs a window rather than a last-sequence register, and retirement that waits for resolution rather than for the send.
Chapter 19.2 converted semantic obligations into stable transport objects and handed them across FDI. This chapter owns those objects from the moment they arrive until the far end no longer needs a copy.
1. The One-Sentence Model
The Adapter owns transport reliability, not higher-level semantics. Its whole obligation is that an accepted object is eventually delivered exactly once or explicitly failed — while the number of physical attempts underneath that promise may be zero, one, or many.
2. What This Chapter Owns, and What It Does Not
| Layer | Owns | Chapter |
|---|---|---|
| Protocol engine | semantic operations, identity, generation, per-protocol completion | 19.2 |
| Adapter | admission, staging, integrity, replay history, duplicate suppression, retirement | this chapter |
| Buffering | the storage structures themselves, sizing, watermarks, ping-pong | 19.4 |
| Flow control | the credit machine in RTL | 19.5 (planned) |
| Link top level | the block partition and the state-ownership table | 19.1 |
The boundaries are deliberate and this chapter respects them. Credits appear here only as an admission term and a resource-indexing question (§34–§36); the arithmetic, the return path and the CDC hazards are 19.5's. Buffers appear here only as lifetimes and ownership; depth, watermarks and structure are 19.4's.
Six things exist only here:
Reservation before ownership transfer (§9–§12). An object may be accepted only if every resource needed to retain it is already secured — and §10 is the design that accepts first and discovers the shortage afterwards.
Staging and replay are different buffers with different lifetimes (§13–§16), even when a design merges them into one memory.
The ring's three pointers and the invariant between them (§21–§24). Allocation passing retirement overwrites history a retry still needs.
CRC alignment (§26–§31). A result computed over one object and attached to another fails at the far end on data that is perfectly correct.
Duplicate suppression needs a window (§32–§35). A last-sequence register accepts a duplicate that is two behind.
And retirement waits for resolution (§41–§43) — not for the send, not for the PHY, and not for reaching the far end.
3. Sourcing
4. The Obligation, Stated Precisely
ACCEPTED transport object
→ delivered semantically exactly once at the far end
OR → explicitly failed and reported
while physical attempts may be: 0, 1, 2, ... N| Quantity | Bound |
|---|---|
| semantic deliveries per accepted object | exactly ≤ 1, and eventually 1 or an explicit failure |
| physical attempts per object | ≥ 0 — an object accepted and then failed may never be sent |
| replay entries per object | exactly 1 while it is unresolved |
| staging entries per object | 1, until replay owns it (§13) |
Everything in this chapter is a mechanism for keeping the first row true while the second varies. Reservation makes the promise affordable; the ring makes retry possible; the window makes exactly-once achievable; and retirement decides when the promise is discharged.
5. The Adapter
Read the replay block's position. It is beside the transmit path, not in series with it — because the object must reach the PHY and be retained, and a design that puts replay in series has one structure doing two jobs with two lifetimes (§13).
6. Adapter State Categories
The chapter's backbone table. Every section below is a row of it.
| State | Allocated when | Released when | Lifetime | Owner |
|---|---|---|---|---|
| Staging entry | at admission | when replay owns the object (§14) | short — pipeline depth | adapter_tx |
| Replay entry | when integrity and the copy are committed | on resolution (§41) — never on send | a full round trip, plus retries | replay |
| Attempt count | with the replay entry | with the replay entry | as the entry | replay |
| Transmitted attempt | at launch | at feedback or timeout | one flight | phy_if |
| RX reconstruction state | at the first beat of an object | at completion or abandonment | one object | rx_ingress |
| Duplicate / history window | continuously | slides forward (§32) | a window, not an entry | rx_ingress |
| Credit | at advertisement | consumed on transfer; returned on release | link epoch | credit_mgr — 19.5 |
| Configuration epoch | at commit | at the next commit | configuration epoch | cfg |
| Recovery state | at trigger | at validated resume | one recovery | link_mgr |
| First fault | at the first error | deliberate clear only | survives everything but power-on | fault_mgr |
Three readings.
Rows 1 and 2 have very different lifetimes — one pipeline depth against one round trip plus retries. §13 is why they are separate concepts even when they share a memory.
Row 6 is a window, not a set of entries. Duplicate suppression is not "remember every object"; it is "remember a bounded range" (§32), and the bound is what makes it implementable.
And row 10's reset domain is power-on. 19.1 §35 established why; the Adapter is where the first fault usually originates.
7. The Transport Object
// ILLUSTRATIVE. NOT a UCIe format, and no field corresponds to any specified
// field (Section 3). Symbolic widths throughout.
typedef struct packed {
logic [OBJ_ID_W-1:0] object_id; // the ADAPTER's identity for this object
logic [SEM_ID_W-1:0] sem_id; // carried opaquely for 19.2's benefit
logic [GEN_W-1:0] sem_gen; // ditto — the Adapter never interprets it
logic [CLASS_W-1:0] tclass; // resource class for admission (Section 34)
logic [LEN_W-1:0] length;
logic [CFG_EPOCH_W-1:0] cfg_epoch; // Section 44
logic has_data;
logic [DATA_W-1:0] data;
} adapter_object_t;Architecture. The Adapter's own identity, plus the protocol engine's identity carried opaquely. That opacity is the layering guarantee: the Adapter routes, protects, retains and delivers the object without ever interpreting sem_id or sem_gen, which is what lets it carry any protocol (19.2 §9).
State. One register per pipeline stage, plus a staging entry and a replay entry.
Cycle behaviour. Formed at admission and held stable while offered (§29). cfg_epoch is captured once (§44).
Contract. The replay path needs enough to retransmit the object byte-identically; the receive side needs enough to reassemble and to decide novelty. Everything else is carried, not used.
Failure. Interpreting sem_id — for instance to order objects by it — which puts protocol knowledge in the Adapter and breaks the moment a protocol's identities are not ordered that way.
DV. Assert sem_id and sem_gen are byte-identical at the far end; assert cfg_epoch immutable while the object is live.
8. Three Identities, One Object
| Identity | Names | Recycled when | Owner |
|---|---|---|---|
| Semantic identity | the protocol operation | at semantic completion | 19.2 |
| Object identity | this transport object | when the object retires | the Adapter |
| Sequence / history identity | this object's place in the reliability stream | when the window slides past it | the replay and RX window |
| Physical attempt | one flight | immediately | the PHY |
A retransmission reuses the sequence identity and creates no new object and no new semantic operation. 19.1 §7 makes the general statement; here it is a wiring requirement, because the three identities are three different fields that a careless design will index by whichever is nearest.
9. Admission Reserves Before It Accepts
// ILLUSTRATIVE. Every resource needed to RETAIN the object, secured before the
// object is accepted. Section 10 is the design that accepts first.
assign adapter_accept =
in_valid
&& staging_space_available // somewhere to put it now
&& replay_slot_available // somewhere to KEEP it (Section 10)
&& credit_available[in_obj.tclass] // the far end can take it (19.5)
&& link_accepting // the link manager permits it
&& cfg_stable; // not mid-commit (Section 44)
assign in_ready = adapter_accept;Architecture. Five terms, and the second is the one designs omit. Staging says "I can hold it for a few cycles"; the replay slot says "I can hold a recoverable copy for a full round trip". They are different promises and both must be made before the object is taken.
State. None of its own — a conjunction over five facts owned by five mechanisms.
Cycle behaviour. Evaluated combinationally into in_ready, and all reservations are taken in the same cycle as the acceptance. Reserve-then-accept, never accept-then-hope.
Contract. Accepting an object promises §4's obligation. The Adapter cannot make that promise without the means to keep it, and the replay slot is the means.
Failure. §10.
DV. Force each term false alone and confirm no acceptance — five directed tests, which catch the term that is computed and not used.
10. Wrong RTL — Accept Before the Replay Slot Exists
// WRONG — the object is accepted on staging space alone, and the replay slot
// is sought later, when the object reaches the integrity stage.
assign adapter_accept = in_valid && staging_space_available && link_accepting;
// ... several stages later:
always_ff @(posedge clk)
if (integrity_done && !replay_slot_available)
stall_or_drop(obj); // ← neither option is acceptableBoth branches of that if are failures.
| The design chooses | Consequence |
|---|---|
| drop the object | an accepted object vanished (10.1 §10) — §4's obligation broken |
| stall it in place | it holds staging, blocks everything behind it, and still cannot be retained |
| send it without retaining a copy | the retry contract is broken — an error makes it unrecoverable |
Four properties.
The third option is the one that gets written, because it looks like it works: the object goes out, and most objects are not retried. The design fails only when that particular object needs a retransmission — which is rare, load-dependent and catastrophic.
The stall option deadlocks under sustained load. The stalled object waits for a replay slot; replay slots free on resolution; resolution requires objects to be sent; sending requires the staging the stalled object is occupying.
And the drop is silent. No overflow, no underflow, no CRC failure — an object simply is not there any more, and the protocol engine above waits forever with a live semantic entry.
The fix is one term in the admission conjunction (§9), and its cost is that the Adapter refuses work slightly earlier. That is the correct behaviour: refusing is a promise not made, and accepting is a promise broken.
11. SVA — an Accepted Object Is Always Owned
// MANDATORY. Every accepted object is held by staging, by replay, or by both.
// tb_object_live() is TESTBENCH knowledge — the design has no signal for it.
property p_object_always_owned(int unsigned rid);
@(posedge clk) disable iff (!rst_n)
tb_object_live(rid) |-> (tb_in_staging(rid) || tb_in_replay(rid));
endproperty
a_object_always_owned: assert property (p_object_always_owned(REF_UT));
// Acceptance implies a replay slot was reserved.
property p_accept_implies_replay_slot;
@(posedge clk) disable iff (!rst_n)
adapter_accept |-> replay_slot_available;
endproperty
a_accept_implies_replay_slot: assert property (p_accept_implies_replay_slot);
// An accepted object is never dropped.
property p_no_silent_drop;
@(posedge clk) disable iff (!rst_n)
tb_object_accepted(REF_UT) |-> ##[1:$] (tb_object_retired(REF_UT)
|| tb_object_failed(REF_UT));
endproperty
a_no_silent_drop: assert property (p_no_silent_drop);
// Reservation and acceptance happen together, never one without the other.
property p_reserve_with_accept;
@(posedge clk) disable iff (!rst_n)
adapter_accept |=> (staging_alloc_fired && replay_reserved_for(prev_obj_id));
endproperty
a_reserve_with_accept: assert property (p_reserve_with_accept);Architecture. Four properties: universal ownership, the reservation implication, no drop, and simultaneity.
Why the first must use a verification reference. The design has no signal saying "object 47 exists as an obligation". The testbench generated it and therefore knows — building that into the design would create a third tracker with its own bugs.
Why the third is written as an unbounded eventuality. It is a safety claim about no-drop rather than a liveness claim about promptness. Liveness bounds are §40's, with their assumptions written down; this one says only that the object does not vanish.
DV. The first needs an error injected in the acceptance window (§56).
12. Staging and Replay Are Different Lifetimes
| Staging | Replay / history | |
|---|---|---|
| Purpose | absorb pipeline and backpressure | retain a recoverable copy |
| Populated at | admission | when the copy is committed (§14) |
| Freed at | replay ownership | resolution (§41) |
| Typical residency | pipeline depth | a full round trip, plus retries |
| If it overflows | backpressure — a performance event | a retry contract violation |
| May be the same memory? | yes, and the lifetimes still differ | yes |
A design may implement both in one RAM. It must not implement both with one lifetime. §15 is the design that frees one memory's entry when the PHY consumes it, and thereby frees both.
13. The Ownership Handoff
// ILLUSTRATIVE. Staging releases only when replay CONFIRMS it holds a durable
// copy. Both own it in between, deliberately (19.1 Section 16).
logic replay_commit; // the entry is written and its metadata is valid
logic staging_release;
assign replay_commit = replay_alloc_fire && replay_entry_written;
assign staging_release = replay_commit; // NOT phy_tx_fire
assign staging_pop_fire = staging_release;| Cycle | Staging holds it | Replay holds it | Safe? |
|---|---|---|---|
| n | ✓ | — | ✓ — staging can re-offer |
| n+1 | ✓ | ✓ | ✓ — the deliberate window |
| n+2 | — | ✓ | ✓ — replay can retransmit |
| (the bug) | — | — | ✗ — nothing can recover it |
Architecture. The release is gated on replay's confirmation, not on the integrity stage completing and not on the PHY accepting.
Contract. Staging guarantees it will not release until replay confirms; replay guarantees it will not confirm until the entry is durable. Neither guarantee is visible at the other's interface, which is why §11's first property asserts the conjunction.
Failure. §15, and the one-cycle unowned window if the pop is gated on the wrong signal (19.1 §17).
DV. Cover the window occurring; inject an error inside it.
14. Wrong Architecture — One Slot Freed When the PHY Consumes
// WRONG — one memory, one lifetime, freed when the PHY takes the object.
always_ff @(posedge clk)
if (phy_tx_fire)
tx_mem_valid_q[tx_rd_ptr] <= 1'b0; // ← the ONLY copy, gone at send1. The object is written into the shared TX memory at admission.
2. The PHY consumes it. The entry is freed.
3. The far end reports an integrity failure — or reports nothing at all,
and a timeout fires.
4. A retransmission is required.
5. -> there is no copy. The object cannot be retransmitted.
6. The Adapter must fail an object it promised to deliver, for a fault
that was recoverable.Four properties.
It works for every object that is not retried, which under a healthy link is nearly all of them. The design passes every clean-link test.
The failure rate equals the retry rate, so it scales with exactly the condition the replay buffer exists for. A marginal link turns an occasional recoverable event into an occasional lost object.
And it converts a recoverable error into an unrecoverable one, which is the specific harm: the link had a mechanism for this and the RTL threw the mechanism's input away.
The fix is not necessarily two memories. One memory can serve both, provided the entry is freed on resolution rather than on send — the lifetime is the thing that must be separate, not the storage.
15. The Transmit Object State Machine
Read the loop between SENT WAIT ACK and REPLAY PENDING. That is the retry, and it does not pass through STAGED or FREE — a retransmission is the same object, so it never re-enters the states where a new object is created (19.2 §43).
And notice what is absent. There is no semantic state here at all. The protocol engine's operation is meanwhile sitting in its own machine (19.2 §24), untouched by any of these transitions.
16. The Transmit Next-State Function
// ILLUSTRATIVE. ONE next-state owner. Every simultaneous event resolved by an
// explicit arm order — Section 17 is the alternative.
typedef enum logic [2:0] {
TX_FREE = 3'd0,
TX_STAGED = 3'd1,
TX_HISTORY_OWNED = 3'd2,
TX_SENT_WAIT_ACK = 3'd3,
TX_REPLAY_PENDING= 3'd4,
TX_RETIRED = 3'd5,
TX_FAILED = 3'd6
} tx_state_e;
tx_state_e tx_state_q [MAX_TX_OBJ];
tx_state_e nxt;
always_comb begin
nxt = tx_state_q[i];
unique case (tx_state_q[i])
TX_FREE: if (admit_fire[i]) nxt = TX_STAGED;
TX_STAGED: if (fatal[i]) nxt = TX_FAILED;
else if (history_commit[i]) nxt = TX_HISTORY_OWNED;
TX_HISTORY_OWNED: if (fatal[i]) nxt = TX_FAILED;
else if (attempt_launch[i]) nxt = TX_SENT_WAIT_ACK;
// Priority here is the section's point: fatal, then retry, then resolve.
TX_SENT_WAIT_ACK: if (fatal[i]) nxt = TX_FAILED;
else if (retry_indication[i]) nxt = TX_REPLAY_PENDING;
else if (resolved[i]) nxt = TX_RETIRED;
TX_REPLAY_PENDING: if (fatal[i]) nxt = TX_FAILED;
else if (attempts_q[i] >= ATTEMPT_BUDGET)
nxt = TX_FAILED;
else if (attempt_launch[i]) nxt = TX_SENT_WAIT_ACK;
TX_RETIRED: if (slot_reclaim[i]) nxt = TX_FREE;
TX_FAILED: if (slot_reclaim[i]) nxt = TX_FREE;
default: nxt = TX_FAILED;
endcase
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) tx_state_q[i] <= TX_FREE;
else tx_state_q[i] <= nxt;Architecture. One owner, one unique case, and an explicit priority in TX_SENT_WAIT_ACK that is a real design decision: a retry indication beats a resolution in the same cycle.
Why that order and not the reverse. If both arrive together the safe direction is to retry an object that may already have been resolved — which produces a duplicate the far end's window suppresses (§32) — rather than to retire an object that needs retransmitting, which loses it. Choosing the recoverable failure over the unrecoverable one is the principle, and it is only reviewable because it is written in one place.
State. Three bits per object plus an attempt counter.
Contract. The scheduler reads TX_REPLAY_PENDING and TX_HISTORY_OWNED; the ring reads TX_RETIRED; the fault manager reads TX_FAILED. Three consumers, three states.
Failure. §17. Also omitting the attempt budget, which loops on a persistently failing object forever (14.3 §37).
DV. §18; cover simultaneous retry-and-resolve, and exhaust the budget.
17. Wrong TX State — Two Writers
// WRONG — the acknowledgement path and the retry path each write the state.
// In the ack handler:
always_ff @(posedge clk) if (resolved[i]) tx_state_q[i] <= TX_RETIRED;
// In the retry handler, a different module:
always_ff @(posedge clk) if (retry_indication[i]) tx_state_q[i] <= TX_REPLAY_PENDING;On a cycle where both fire, the tool's ordering decides — and both outcomes are wrong in different ways.
| Which write wins | Consequence |
|---|---|
TX_RETIRED wins | an object needing retransmission is retired — it is lost, and its slot is reused |
TX_REPLAY_PENDING wins | an already-resolved object is retransmitted — a duplicate, suppressed at the far end (§32) |
Four properties.
One outcome is recoverable and the other is not, which is exactly the choice §16 makes deliberately. Here the choice is made by the tool.
Simulation and synthesis may disagree, and with the writes in different modules and different gating conditions synthesis may not report a conflict.
The failure is rare. A retry indication and a resolution for the same object in the same cycle requires specific timing — so it appears in the field and not in regression.
And the retired-wrongly case is silent. The object is gone, the slot is reused, and the protocol engine above waits forever with a live semantic entry (19.2 §16).
18. SVA — Transmit State Discipline
// MANDATORY. Only legal transitions, and the state comes from one function.
property p_legal_tx_transitions;
@(posedge clk) disable iff (!rst_n)
$changed(tx_state_q[IDX])
|-> is_legal_tx_transition($past(tx_state_q[IDX]), tx_state_q[IDX]);
endproperty
a_legal_tx_transitions: assert property (p_legal_tx_transitions);
property p_tx_state_single_source;
@(posedge clk) disable iff (!rst_n)
$changed(tx_state_q[IDX]) |-> (tx_state_q[IDX] == $past(nxt_for(IDX)));
endproperty
a_tx_state_single_source: assert property (p_tx_state_single_source);
// A retry indication and a resolution together must NOT retire (Section 16).
property p_retry_beats_resolve;
@(posedge clk) disable iff (!rst_n)
((tx_state_q[IDX] == TX_SENT_WAIT_ACK) && retry_indication[IDX] && resolved[IDX])
|=> (tx_state_q[IDX] == TX_REPLAY_PENDING);
endproperty
a_retry_beats_resolve: assert property (p_retry_beats_resolve);
// An object in a working state always has a history entry.
property p_working_implies_history;
@(posedge clk) disable iff (!rst_n)
((tx_state_q[IDX] == TX_SENT_WAIT_ACK) || (tx_state_q[IDX] == TX_REPLAY_PENDING))
|-> replay_entry_valid_for(IDX);
endproperty
a_working_implies_history: assert property (p_working_implies_history);Architecture. Four properties: legality, single source, the priority decision, and the history invariant.
Why the third is worth asserting explicitly. It encodes a design decision rather than a structural fact. If a later change reverses the arm order, this property fails and someone has to justify the change — which is exactly what a design-decision assertion is for.
DV. Cover the simultaneous case; run the legal-transition function formally where possible.
19. The Replay Entry
// ILLUSTRATIVE. NOT a UCIe sequence format, and no width is claimed (Section 3).
typedef struct packed {
logic valid;
logic [OBJ_ID_W-1:0] object_id;
logic [SEQ_W-1:0] seq; // reliability identity (Section 8)
logic [LEN_W-1:0] length;
logic [CFG_EPOCH_W-1:0] cfg_epoch; // Section 44
logic [ATTEMPT_W-1:0] attempts; // evidence, and the budget (Section 16)
logic [DATA_W-1:0] data; // the retained copy
} replay_entry_t;
// Payload storage — inferred RAM, deliberately NOT reset (19.1 Section 12).
replay_entry_t replay_mem [REPLAY_DEPTH];Architecture. One entry per unresolved object. attempts is both a control input — it feeds the budget — and evidence, which is why it survives a recovery (§6).
State. REPLAY_DEPTH entries. The depth is a reliability window, not a throughput parameter — 19.4 derives it, and §22 states the invariant it must satisfy.
Cycle behaviour. Written once at history commit; data is never modified afterwards, because a retransmission must be byte-identical to the original.
Contract. The scheduler retransmits from this copy. If the copy can differ from what was sent, the far end's duplicate detection may not recognise it — and a "duplicate" that differs is a second object.
Failure. Modifying data in place — for instance to update an epoch field — which makes the retransmission a different object.
DV. Assert the retained copy is stable while the entry is valid; assert a retransmission is byte-identical to the original attempt.
20. The Ring and Its Three Pointers
// ILLUSTRATIVE circular replay buffer with three pointers. Wrap-safe compares
// via an extra bit — Section 23 is the version that uses equality alone.
localparam int PTR_W = $clog2(REPLAY_DEPTH);
logic [PTR_W:0] alloc_ptr_q; // next entry to allocate (extra MSB = wrap)
logic [PTR_W:0] send_ptr_q; // next entry to transmit
logic [PTR_W:0] retire_ptr_q; // oldest unresolved entry
// Index into the RAM with the low bits; compare with all bits.
wire [PTR_W-1:0] alloc_idx = alloc_ptr_q[PTR_W-1:0];
wire [PTR_W-1:0] send_idx = send_ptr_q[PTR_W-1:0];
wire [PTR_W-1:0] retire_idx = retire_ptr_q[PTR_W-1:0];
// Occupancy is a subtraction on the wide pointers — never an equality test.
wire [PTR_W:0] outstanding = alloc_ptr_q - retire_ptr_q;
wire replay_full = (outstanding == REPLAY_DEPTH[PTR_W:0]);
wire replay_empty = (outstanding == '0);
wire replay_slot_available = !replay_full;Architecture. Three pointers because there are three distinct positions: what has been allocated, what has been sent, and what is still unresolved. A two-pointer ring cannot express "sent but unresolved", which is exactly the state a retry operates on.
State. Three pointers, each one bit wider than the index. The extra bit is what distinguishes full from empty (§23).
Cycle behaviour. alloc_ptr_q advances at history commit; send_ptr_q at an attempt launch; retire_ptr_q at resolution. Each advances on an actual event, never on a decision that did not fire.
Contract. replay_slot_available gates admission (§9). The whole retention promise rests on outstanding being exactly right.
Failure. §21 and §23.
DV. §24; cover wrap on each pointer, and cover full and empty.
21. Wrong Pointer Architecture — Allocation Passes Retirement
// WRONG — allocation is gated on a check that does not account for wrap.
wire replay_full_wrong = (alloc_ptr_q[PTR_W-1:0] == retire_ptr_q[PTR_W-1:0]);
assign replay_slot_available = !replay_full_wrong; // ← also true when EMPTYWhen the buffer is empty the indices are also equal, so this reads "full" as "empty" and vice versa — and in the direction that matters, it permits allocation when the ring is full.
1. The ring is full: REPLAY_DEPTH unresolved entries.
2. alloc_idx == retire_idx, so replay_full_wrong is TRUE... but the design
may be using the opposite polarity, or checking only in one direction.
3. Allocation proceeds and overwrites the OLDEST UNRESOLVED entry.
4. That entry's object may still need retransmission.
5. A retry is later requested for it.
6. -> the ring returns the NEW object's data under the OLD sequence identity.Four properties, and this is the chapter's most catastrophic failure.
The far end receives a well-formed object under a sequence identity it was expecting. It passes integrity, it passes the duplicate check, and it is entirely the wrong data. No mechanism anywhere detects it.
Both objects are corrupted. The overwritten one is lost; the new one is delivered under the wrong identity and may itself be delivered again later. One overwrite, two failures.
It requires the ring to be genuinely full, which happens only under sustained load with a slow acknowledgement path — so it is a soak-test failure, not a regression failure.
And the correct form is one extra bit. Wide pointers with a subtraction give an unambiguous occupancy, and §24's assertion turns the remaining risk into a reported error.
22. The Ring Invariant
retire_ptr <= send_ptr <= alloc_ptr (in wrap-extended order)
alloc_ptr - retire_ptr <= REPLAY_DEPTH <- Section 21
send_ptr - retire_ptr <= alloc_ptr - retire_ptr <- cannot send unallocated| Relationship | Meaning | Violated by |
|---|---|---|
retire ≤ send | nothing is retired before it is sent | a resolution for an unsent object |
send ≤ alloc | nothing is sent before it is allocated | a scheduler reading past the allocation point |
alloc − retire ≤ DEPTH | allocation never passes retirement | §21 |
All three are checkable in one assertion each, and all three are cheap. The third is the one that prevents silent data substitution.
23. SVA — the Ring Is Safe
// MANDATORY. Occupancy never exceeds the depth (Section 21).
property p_replay_never_overfull;
@(posedge clk) disable iff (!rst_n)
((alloc_ptr_q - retire_ptr_q) <= REPLAY_DEPTH);
endproperty
a_replay_never_overfull: assert property (p_replay_never_overfull);
// Allocation requires a free slot.
property p_alloc_requires_slot;
@(posedge clk) disable iff (!rst_n)
replay_alloc_fire |-> !replay_full;
endproperty
a_alloc_requires_slot: assert property (p_alloc_requires_slot);
// A live entry is never overwritten.
property p_no_overwrite_live_entry;
@(posedge clk) disable iff (!rst_n)
replay_alloc_fire |-> !replay_mem[alloc_idx].valid;
endproperty
a_no_overwrite_live_entry: assert property (p_no_overwrite_live_entry);
// Pointer ordering holds (Section 22).
property p_pointer_ordering;
@(posedge clk) disable iff (!rst_n)
(((send_ptr_q - retire_ptr_q) <= (alloc_ptr_q - retire_ptr_q))
&& ((alloc_ptr_q - retire_ptr_q) <= REPLAY_DEPTH));
endproperty
a_pointer_ordering: assert property (p_pointer_ordering);
// The retained copy is stable while the entry is valid (Section 19).
property p_replay_data_stable;
@(posedge clk) disable iff (!rst_n)
replay_mem[IDX].valid |-> $stable(replay_mem[IDX].data);
endproperty
a_replay_data_stable: assert property (p_replay_data_stable);Architecture. Five properties covering occupancy, allocation, overwrite, ordering and content stability.
Why the third is not implied by the first. Occupancy can be correct while the allocation index points at a valid entry — through a pointer that advanced without its counterpart. The third checks the memory, not the arithmetic, and it is the one that fires on §21's overwrite at the exact cycle.
DV. All five always-on. Drive the ring to full under sustained load with delayed resolutions (§56) — the only condition that reaches §21.
24. Simultaneous Allocate and Retire
// ILLUSTRATIVE. At high load these coincide constantly. Pointers are
// independent, so each advances on its own event; occupancy is DERIVED and
// therefore never drifts.
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
alloc_ptr_q <= '0;
send_ptr_q <= '0;
retire_ptr_q <= '0;
end else begin
if (replay_alloc_fire) alloc_ptr_q <= alloc_ptr_q + 1'b1;
if (attempt_launch_any) send_ptr_q <= send_ptr_q + 1'b1;
if (replay_retire_fire) retire_ptr_q <= retire_ptr_q + 1'b1;
endArchitecture. Three independent pointers and no separate occupancy register at all. outstanding is a subtraction (§20), so a simultaneous allocate and retire needs no special case — both pointers advance and the difference is unchanged automatically.
This is why the wide-pointer form is preferable to a separate count. A count register requires the four-case
unique casethat every other counter in this curriculum needs; a derived difference cannot drift because there is nothing to drift.
Failure. Maintaining a redundant occupancy register alongside the pointers and updating it independently — which reintroduces the drift the derivation avoided, and now there are two answers to one question.
DV. Cover simultaneous allocate-and-retire; assert the derived occupancy matches a testbench count.
25. The Integrity Stage
// ILLUSTRATIVE integrity pipeline. The function is abstract on purpose.
logic [CRC_W-1:0] crc_partial_q;
logic crc_active_q;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
crc_partial_q <= CRC_INIT;
crc_active_q <= 1'b0;
end else if (crc_start) begin
crc_partial_q <= crc_next(CRC_INIT, chunk_in); // abstract (Section 25)
crc_active_q <= 1'b1;
end else if (crc_feed && crc_active_q) begin
crc_partial_q <= crc_next(crc_partial_q, chunk_in);
end else if (crc_final && crc_active_q) begin
crc_active_q <= 1'b0;
end
assign crc_result = crc_finalise(crc_partial_q);Architecture. A chunk-fed accumulator with an explicit active flag. crc_active_q is what prevents a chunk from a different object folding into a partial result — the flag is the object boundary, and without it a stall that interleaves two objects corrupts both results.
State. One partial register and one flag. Per object in flight, if the pipeline supports more than one.
Cycle behaviour. Fed on crc_feed, which must be qualified by the same enable that advances the data (§26). A chunk consumed by the CRC and not by the datapath — or vice versa — is a divergence.
Contract. The far end recomputes over what it received. The result must be over exactly the bytes that were sent, in exactly that order.
Failure. §27 and §29.
DV. §28's alignment property; assert crc_active_q bounds exactly one object.
26. The Integrity Input Bundle
// ILLUSTRATIVE. Everything the result covers travels as ONE object, so a stall
// cannot advance one part without the other.
typedef struct packed {
logic [META_W-1:0] meta; // whatever the architecture requires covered
logic [DATA_W-1:0] data;
logic last;
logic valid;
} crc_input_t;
crc_input_t crc_in_q;Architecture. One packed bundle. meta is inside it — not beside it — because §27 is the failure of metadata changing while data stalls, and a shared register makes that structurally impossible.
Contract. The result is a function of this bundle. If any covered field can change between the computation and the transmission, the result no longer describes what was sent (§27).
DV. §28's stability property.
27. Wrong CRC Pipeline — Metadata Changes While Data Stalls
// WRONG — metadata is taken from a live signal while the data path stalls.
assign crc_input = {live_meta, staged_data}; // ← two different lifetimes1. The CRC is computed over {meta = A, data = D}.
2. The data path stalls; the object waits.
3. live_meta changes to B — a new configuration, a new class, a re-derived field.
4. The object is finally transmitted as {meta = B, data = D}.
5. The far end recomputes over {B, D} and gets a different result.
6. -> integrity FAILS on data that is completely correct.Four properties.
The data is perfect. Every byte of D is exactly what the protocol engine handed over. The failure is entirely an accounting mismatch between what was covered and what was sent.
The far end does the right thing and rejects it, which triggers a retransmission — and the retransmission has the same problem, so the object can fail repeatedly and eventually exhaust the attempt budget.
The symptom is "intermittent CRC failures on a healthy link", which sends everyone to look at the physical layer. The physical layer is fine.
And it only occurs when the data path stalls, which is load-dependent. A design that never backpressures during test never sees it.
28. SVA — Integrity Alignment
// MANDATORY. The covered bundle does not change while the object waits.
property p_crc_input_stable_under_stall;
@(posedge clk) disable iff (!rst_n)
(crc_in_q.valid && !crc_consume) |=> $stable(crc_in_q);
endproperty
a_crc_input_stable_under_stall:
assert property (p_crc_input_stable_under_stall);
// The result accompanies the object it was computed over.
property p_crc_accompanies_its_object(int unsigned rid);
@(posedge clk) disable iff (!rst_n)
(phy_tx_fire && (tb_ref_id == rid))
|-> (tx_crc == tb_expected_crc(rid));
endproperty
a_crc_accompanies_its_object:
assert property (p_crc_accompanies_its_object(REF_UT));
// The accumulator is bounded to exactly one object.
property p_crc_active_one_object;
@(posedge clk) disable iff (!rst_n)
crc_start |-> !crc_active_q;
endproperty
a_crc_active_one_object: assert property (p_crc_active_one_object);
// The result's valid is aligned with the object's (Section 29).
property p_crc_valid_aligned;
@(posedge clk) disable iff (!rst_n)
tx_obj_valid |-> tx_crc_valid;
endproperty
a_crc_valid_aligned: assert property (p_crc_valid_aligned);Architecture. Four properties: stability, end-to-end correctness, accumulator scoping, and valid alignment.
Why the second uses a testbench reference. The expected result is a function of the bytes the testbench generated. Computing it in the design would use the same pipeline that produced the bug.
DV. §27 needs a stall injected between computation and transmission with a live metadata source; §29 needs two back-to-back objects.
29. Wrong CRC Valid Alignment
// WRONG — the result path is one stage longer than the object path.
always_ff @(posedge clk) begin
obj_q1 <= obj_in; obj_q2 <= obj_q1; // 2 stages
crc_q1 <= crc_in; crc_q2 <= crc_q1; crc_q3 <= crc_q2; // 3 stages
end
assign tx_obj = obj_q2;
assign tx_crc = crc_q3; // ← object N is sent with the result for N-1| Cycle | Object sent | Result attached | Far end |
|---|---|---|---|
| n | O0 | C(O0) | ✓ by luck — the first |
| n+1 | O1 | C(O0) | ✗ rejected |
| n+2 | O2 | C(O1) | ✗ rejected |
Four properties.
It fails only once the pipeline is full. With a single object in flight there is no previous result to attach, so the first object of any burst is correct — and a single-object smoke test passes completely.
Then every subsequent object fails, so the failure rate is near 100% under continuous traffic and 0% under isolated traffic. That discontinuity is the diagnostic signature.
The far end rejects correct data, triggering retransmission — and the retransmission is also misaligned, so objects fail repeatedly and the link appears to be failing catastrophically.
And it is a classic. The fix is to build the result into the object's bundle (§26) so the two cannot have different depths, which is the same structural argument as 19.1 §25 and 18.2 §41.
30. The Receive Delivery Gate
// ILLUSTRATIVE. Five terms, evaluated COMBINATIONALLY into the delivery.
// Section 31 is the version that delivers first.
assign rx_semantic_deliver =
rx_object_complete // every part of the object arrived (Section 39)
&& rx_integrity_ok // it passed (Section 25)
&& !rx_duplicate // the window says it is new (Section 32)
&& rx_in_window // and it is within the acceptable range
&& rx_downstream_space; // the protocol engine can take itArchitecture. Five named terms between reconstruction and the protocol engine. Naming them separately makes a non-delivery attributable to one of five causes rather than "the RX path dropped it".
Cycle behaviour. Combinational into the delivery. A registered gate delivers one object before deciding not to — and the protocol engine may already have allocated against it.
Contract. The protocol engine assumes exactly-once semantic delivery (19.2 §4). That assumption has no signal at the interface.
Failure. §31.
DV. Force each term false alone — five directed tests.
31. Wrong RX — Delivering Before the Verdict
// WRONG — the object is delivered as it is reconstructed, and checked after.
always_ff @(posedge clk)
if (rx_object_complete) begin
deliver_to_protocol(rx_obj); // ← delivered
start_integrity_check(rx_obj); // ← checked
update_duplicate_window(rx_obj); // ← and novelty decided
end| The object is | Correct outcome | With this design |
|---|---|---|
| valid and new | delivered once | delivered once ✓ |
| corrupt | discarded; retransmission requested | delivered, then flagged |
| a duplicate | suppressed; the reply still sent | delivered a second time |
| outside the window | rejected | delivered |
Three of four combinations are handled wrongly, and in every one the transport behaved exactly as specified.
Two consequences worth separating.
The corrupt case is a correctness failure at the protocol engine, which may allocate, complete or fail an operation based on garbage. "Undoing" it is not available — the engine has no mechanism to un-complete an operation.
And the duplicate case creates a second semantic delivery, which is the one thing §4's obligation forbids. The engine's own guards — generation checks, part bitmaps (19.2 §32) — may catch it, but relying on that is relying on the layer above to fix the layer below's bug.
32. Duplicate Suppression Needs a Window
A retried physical attempt may carry an object the far end already accepted. The receiver must distinguish three cases:
| The arriving object is | Response |
|---|---|
| new and in window | accept, deliver once, record it |
| a duplicate of something in the window | suppress the delivery, still acknowledge (14.3 §22) |
| outside the window entirely | reject and report — it is stale or corrupt |
// ILLUSTRATIVE receive window. A BITMAP over a bounded sequence range, not a
// single last-sequence register — Section 33 is that version.
logic [WINDOW-1:0] received_q; // one bit per sequence position
logic [SEQ_W:0] window_base_q; // wrap-extended, like the ring (Section 20)
wire [SEQ_W:0] offset = rx_seq - window_base_q;
wire in_window = (offset < WINDOW[SEQ_W:0]);
wire is_duplicate = in_window && received_q[offset[$clog2(WINDOW)-1:0]];
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
received_q <= '0;
window_base_q <= '0;
end else begin
// Record, guarded so a duplicate changes nothing.
if (rx_accept_fire && in_window && !is_duplicate)
received_q[offset[$clog2(WINDOW)-1:0]] <= 1'b1;
// Slide the base past a contiguous run of received positions.
if (received_q[0]) begin
received_q <= {1'b0, received_q[WINDOW-1:1]};
window_base_q <= window_base_q + 1'b1;
end
endArchitecture. A sliding window with one bit per position. The window is what makes duplicate suppression implementable — remembering every object ever received is unbounded; remembering a bounded range is a shift register.
State. WINDOW bits plus a wrap-extended base. The base is wide for the same reason the ring pointers are (§20): comparison must survive wrap.
Cycle behaviour. A position is recorded under a guard, so a duplicate finds its bit already set and changes nothing — idempotent by construction. The base slides past contiguous received positions, so out-of-order arrivals do not stall it forever.
Contract. The window's size must cover the maximum number of objects that can be outstanding and reorderable. A window smaller than the sender's replay depth cannot suppress every duplicate the sender can produce — which is a sizing relationship 19.4 makes precise.
Failure. §33. Also sliding the base on any received position rather than on a contiguous run, which discards positions that were never received and lets a genuinely missing object be forgotten.
DV. §34; cover duplicates at the base, in the middle and at the far edge of the window.
33. Wrong Duplicate Suppression — a Last-Sequence Register
// WRONG — only the most recent accepted sequence is remembered.
always_ff @(posedge clk)
if (rx_accept_fire) last_seq_q <= rx_seq;
assign is_duplicate = (rx_seq == last_seq_q); // ← only ONE backIllustrative, with three objects in flight and a retransmission of the oldest:
| Arrival | last_seq_q before | Detected as duplicate? | Correct? |
|---|---|---|---|
| seq 10 | 9 | no | ✓ new |
| seq 11 | 10 | no | ✓ new |
| seq 10 replayed | 11 | no | ✗ delivered twice |
Four properties.
It works for exactly one pattern: an immediate retransmission with nothing in between. Any other object arriving between the original and the duplicate defeats it entirely.
And that pattern is the least likely one. A retransmission follows a round trip plus a retry decision, during which the sender has almost certainly sent other objects. The register is correct in the case that essentially never happens.
The result is a second semantic delivery — §4's obligation broken, and the protocol engine above must then catch it with its own guards or complete an operation twice.
And the fix is a window whose size is a real design parameter, related to the sender's replay depth and the reordering the path permits. A single register is a window of size one, which is a sizing decision nobody made deliberately.
34. SVA — Exactly-Once Delivery
// MANDATORY. The property the entire receive architecture exists for.
int unsigned tb_deliveries [int]; // reference object id -> semantic deliveries
always @(posedge clk) if (rx_semantic_deliver)
tb_deliveries[tb_ref_id_of(rx_obj)]++;
property p_delivery_at_most_once(int unsigned rid);
@(posedge clk) disable iff (!rst_n)
(tb_deliveries[rid] <= 1);
endproperty
a_delivery_at_most_once: assert property (p_delivery_at_most_once(REF_UT));
// A duplicate delivers nothing.
property p_duplicate_delivers_nothing;
@(posedge clk) disable iff (!rst_n)
(rx_object_complete && rx_duplicate) |-> !rx_semantic_deliver;
endproperty
a_duplicate_delivers_nothing: assert property (p_duplicate_delivers_nothing);
// A duplicate is still acknowledged (14.3 Section 22).
property p_duplicate_still_acknowledged;
@(posedge clk) disable iff (!rst_n)
(rx_object_complete && rx_duplicate) |-> ##[1:ACK_BOUND] rx_ack_sent;
endproperty
a_duplicate_still_acknowledged:
assert property (p_duplicate_still_acknowledged);
// A failed integrity check delivers nothing.
property p_corrupt_delivers_nothing;
@(posedge clk) disable iff (!rst_n)
(rx_object_complete && !rx_integrity_ok) |-> !rx_semantic_deliver;
endproperty
a_corrupt_delivers_nothing: assert property (p_corrupt_delivers_nothing);
// The window record is idempotent.
property p_window_record_idempotent;
@(posedge clk) disable iff (!rst_n)
(rx_accept_fire && is_duplicate) |=> $stable(received_q);
endproperty
a_window_record_idempotent: assert property (p_window_record_idempotent);Architecture. Five properties: exactly-once, the two gate conditions, the acknowledgement obligation, and window idempotence.
Why the third is not optional. Suppressing the effect and the reply leaves the sender retransmitting forever — a livelock built from two individually correct behaviours (14.3 §22). A design that gets suppression right and acknowledgement wrong has replaced a duplicate with a hang.
DV. All five; the first needs a retry that actually occurs while traffic is live.
35. Reassembly
If an object spans several beats or fragments in a given architecture, the receiver must track completeness — and duplicates make a counter insufficient, for the seventh time in this curriculum.
// ILLUSTRATIVE. Position-indexed storage, and a BITMAP of what has arrived.
logic [MAX_FRAGS-1:0] frag_received_q [MAX_RX_INFLIGHT];
logic [DATA_W-1:0] reasm_mem [MAX_RX_INFLIGHT][MAX_FRAGS];
always_ff @(posedge clk)
if (frag_valid && rx_obj_q[frag.obj_slot].valid) begin
reasm_mem[frag.obj_slot][frag.frag_idx] <= frag.data; // by INDEX
if (!frag_received_q[frag.obj_slot][frag.frag_idx]) // GUARDED
frag_received_q[frag.obj_slot][frag.frag_idx] <= 1'b1;
end
assign rx_object_complete[s] =
(frag_received_q[s] & frag_mask(rx_obj_q[s].frag_count))
== frag_mask(rx_obj_q[s].frag_count);Architecture. Storage indexed by fragment index and a bitmap of arrivals. A duplicate writes the same data to the same slot and sets an already-set bit — harmless by construction.
Contract. Nothing downstream may consume the object until complete. Masking is against this object's fragment count, not against MAX_FRAGS — otherwise an object using fewer fragments is permanently incomplete (17.4 §52).
Failure. §36.
DV. Cover in-order, reversed, duplicated and missing fragments.
36. Wrong Fragment Counter
// WRONG — a count of received fragments.
always_ff @(posedge clk)
if (frag_valid) frag_count_q[slot] <= frag_count_q[slot] + 1'b1;
assign rx_object_complete[slot] = (frag_count_q[slot] == expected_frags[slot]);| Arrival | Counter | Reality |
|---|---|---|
| fragment 1 | 1 | 1 present |
| fragment 2 | 2 | 1, 2 present |
| fragment 2 replayed | 3 | 1, 2 present — 3 MISSING |
| — | complete | ✗ delivered with fragment 3 absent |
Three properties.
The delivered object contains stale or undefined data at fragment 3's position, and the protocol engine above has no way to know.
The duplicate is a correct transport retry, so no other component is wrong.
And fragment 3 arrives later at a slot that has been freed — producing a spurious out-of-window rejection, which is a symptom at the wrong time describing the wrong thing.
37. SVA — Reassembly Completeness
// MANDATORY. Completion requires the full unique set for THIS object.
property p_reassembly_complete_unique;
@(posedge clk) disable iff (!rst_n)
rx_deliver_fire |-> ((frag_received_q[dslot] & frag_mask(rx_obj_q[dslot].frag_count))
== frag_mask(rx_obj_q[dslot].frag_count));
endproperty
a_reassembly_complete_unique:
assert property (p_reassembly_complete_unique);
property p_duplicate_fragment_idempotent;
@(posedge clk) disable iff (!rst_n)
(frag_valid && frag_received_q[frag.obj_slot][frag.frag_idx])
|=> $stable(frag_received_q[frag.obj_slot]);
endproperty
a_duplicate_fragment_idempotent:
assert property (p_duplicate_fragment_idempotent);
property p_stale_fragment_ignored;
@(posedge clk) disable iff (!rst_n)
(frag_valid && !rx_obj_q[frag.obj_slot].valid) |=> $stable(frag_received_q);
endproperty
a_stale_fragment_ignored: assert property (p_stale_fragment_ignored);Architecture. Three properties: completeness against the object's own count, idempotence, and stale rejection.
DV. All three; the second and third must be injected.
38. Flow Control at the Adapter
19.5 owns the credit machine. What the Adapter needs from it here is one question, asked at admission:
May I allocate and transmit this object without exceeding what the far end has advertised for this object's resource class?
// ILLUSTRATIVE. Per class, because the classes have independent downstream
// resources. The arithmetic and the return path are 19.5's.
logic [CREDIT_W-1:0] credit_q [NUM_CLASSES];
assign credit_available[c] = (credit_q[c] != '0);The Adapter's obligation is to index correctly and to consume on the right event — and §39 is the design that does neither.
39. Wrong RTL — a Global Credit Scalar
// WRONG — one counter for every class.
logic [CREDIT_W-1:0] credit_q;
assign credit_available = (credit_q != '0);A scalar is wrong in both directions, and the two failures look nothing alike.
| Situation | With per-class credits | With a scalar |
|---|---|---|
| class A exhausted, class B has room | B proceeds | everything stalls — throughput lost |
| class A has room, class B exhausted | A proceeds | an object for B is admitted — the far end's B resource overflows |
Three properties.
The first row is a performance failure and the second is a correctness failure, from the same line.
The second is the dangerous one. The far end's class-B structure receives an object it has no room for — and flow control existed precisely to prevent that. What happens next depends on the far end: it drops, it overwrites, or it blocks. None of those is recoverable by the sender, which believed it had permission.
And the diagnostic signature of the first row is misleading. Aggregate credit is non-zero right up until it is zero, so the stall looks like a sudden cliff rather than one class's exhaustion.
40. The Transmit Scheduler
// ILLUSTRATIVE. Three contenders with a bounded policy — Sections 41 and 42
// are the two strict-priority designs.
logic new_pending, replay_pending, mgmt_pending;
logic [AGE_W-1:0] replay_age_q; // saturating
logic [AGE_W-1:0] new_age_q; // saturating
logic [BUD_W-1:0] replay_budget_q; // consecutive replays before yielding
wire replay_starved = (replay_age_q >= REPLAY_BOUND);
wire new_starved = (new_age_q >= NEW_BOUND);
wire replay_capped = (replay_budget_q == '0);
always_comb begin
tx_select = SEL_NONE;
if (mgmt_pending) tx_select = SEL_MGMT;
else if (replay_pending && replay_starved) tx_select = SEL_REPLAY;
else if (new_pending && new_starved) tx_select = SEL_NEW;
else if (replay_pending && !replay_capped) tx_select = SEL_REPLAY;
else if (new_pending) tx_select = SEL_NEW;
else if (replay_pending) tx_select = SEL_REPLAY;
endArchitecture. Management traffic first, then two bounded overrides, then a replay preference bounded by a budget. Replay is preferred because an unreplayed object holds a ring slot and blocks admission — but the budget stops a replay storm from starving new traffic forever (§42).
State. Two saturating ages and a budget counter. Saturating, because a wrapping age reports a fresh requester at the moment it has waited longest.
Cycle behaviour. Both ages advance only when their class wanted service and did not transfer — the eighth appearance of that rule in this curriculum.
Contract. Reliability relies on replay being scheduled within a bound; throughput relies on new traffic being scheduled within a bound. Neither is visible at the interface, which is why §43 asserts both.
Failure. §41 and §42.
DV. Saturate both sources; measure each worst-case wait against its bound.
41. Wrong Policy — New Traffic Always First
// WRONG — a pending replay waits behind every new object, forever.
assign tx_select = new_pending ? SEL_NEW : SEL_REPLAY;1. Under sustained load new_pending is permanently true.
2. A pending replay is never scheduled.
3. Its ring entry is never resolved, so retire_ptr never advances.
4. The ring fills.
5. Admission stops (Section 9), so new_pending eventually falls.
6. -> the replay finally goes — but only after the entire ring has filled,
and only after admission has stalled completely.Three properties.
It self-resolves, and only by stalling everything first. The design is not permanently deadlocked, which is why it survives review — but the resolution mechanism is a full ring and a total admission stall, which is a throughput collapse rather than a policy.
The reliability latency is unbounded in practice. An object needing retransmission waits for the ring to fill, which under light retry rates can be a very long time — and during that whole period the object is unresolved and the far end may be waiting for it.
And it interacts badly with the attempt budget (§16). If the budget is timer-based, an object can exhaust its attempts while never having been scheduled, and be failed for a fault that was never retried.
42. Wrong Policy — Replay Always First
// WRONG — new traffic waits behind every pending replay, forever.
assign tx_select = replay_pending ? SEL_REPLAY : SEL_NEW;Under a persistent error condition, replays regenerate replays.
1. A marginal link causes a steady retry rate.
2. Every replay that fails becomes another pending replay.
3. replay_pending is permanently true.
4. New traffic is never scheduled.
5. -> the link is fully utilised and delivers nothing new, indefinitely.Three properties.
It does not self-resolve. Unlike §41, nothing stops the replay stream — the condition that produces replays is the link's error rate, which the scheduler cannot influence.
And absolute replay priority is correct as a transient policy. Draining the retry backlog quickly is usually right. The error is making it unbounded, which converts a good short-term policy into a permanent one.
The budget is the fix, and it should be a number of consecutive replays rather than a rate — because what must be broken is a run, not an average.
43. SVA — Scheduler Liveness
// LIVENESS, bounded, both directions, with assumptions stated (15.2 §36).
//
// A1: the PHY eventually accepts a selected object
// A2: a pending class stays pending until served
// A3: recovery terminates (14.2 §30)
assume property (@(posedge clk) disable iff (!rst_n)
((tx_select != SEL_NONE) && !recovery_active) |-> ##[1:PHY_BOUND] phy_tx_fire);
assume property (@(posedge clk) disable iff (!rst_n)
(replay_pending && !phy_tx_fire) |=> replay_pending);
property p_replay_eventually_scheduled;
@(posedge clk) disable iff (!rst_n)
replay_pending |-> ##[1:REPLAY_BOUND] ((tx_select == SEL_REPLAY) && phy_tx_fire);
endproperty
a_replay_eventually_scheduled:
assert property (p_replay_eventually_scheduled);
property p_new_eventually_scheduled;
@(posedge clk) disable iff (!rst_n)
new_pending |-> ##[1:NEW_BOUND] ((tx_select == SEL_NEW) && phy_tx_fire);
endproperty
a_new_eventually_scheduled: assert property (p_new_eventually_scheduled);
// The replay budget is real — not a parameter set to infinity.
initial begin
assert (REPLAY_BUDGET > 0 && REPLAY_BUDGET < 32'hFFFF_FFFF)
else $fatal(1, "replay budget must be finite and non-zero (Section 42)");
endArchitecture. Two bounded liveness properties in opposite directions, plus an elaboration check.
Why both directions. §41 starves replay and §42 starves new traffic. A single fairness property with one bound cannot distinguish them, and a design corrected for one can fail the other.
Why the elaboration check. A budget set to its maximum removes the guarantee while leaving every line that implements it — the same silent-parameterisation hazard as 18.2 §34's zero reservation.
DV. Prove both; then set the budget to its maximum and confirm the second property fails.
44. Retirement Waits for Resolution
45. Wrong Retirement — Free on Send
// WRONG — the replay entry is freed when the PHY takes the object.
always_ff @(posedge clk)
if (phy_tx_fire) begin
replay_mem[send_idx].valid <= 1'b0;
retire_ptr_q <= retire_ptr_q + 1'b1; // ← retirement, at the send
endThree properties.
The retained copy is destroyed before anything confirms it was received. The object may be corrupted on the wire, may be lost entirely, or may arrive and be rejected — and in all three cases the retransmission the architecture provides for is now impossible.
And the ring's occupancy becomes a measure of the pipeline rather than of outstanding reliability, so replay_slot_available (§9) reports capacity the design does not have. Admission then over-commits, and the whole reservation discipline in §9 is defeated by one wrong retirement event.
This is the same failure as 14.3 §15's free-on-tx_fire and 19.1 §18's fourth property — and it recurs because phy_tx_fire is local, available this cycle, and feels like completion. Resolution is a message from the far end, several stages and a round trip away.
46. SVA — Retirement Discipline
// MANDATORY. An entry retires only on resolution.
property p_retire_requires_resolution;
@(posedge clk) disable iff (!rst_n)
replay_retire_fire |-> resolved_for(retire_idx);
endproperty
a_retire_requires_resolution: assert property (p_retire_requires_resolution);
// The entry is not freed at the send.
property p_not_freed_on_send;
@(posedge clk) disable iff (!rst_n)
(phy_tx_fire && !resolved_for(send_idx)) |=> $stable(retire_ptr_q);
endproperty
a_not_freed_on_send: assert property (p_not_freed_on_send);
// Retirement is in order — the oldest unresolved entry retires first.
property p_retire_in_order;
@(posedge clk) disable iff (!rst_n)
replay_retire_fire |-> (retire_idx == retire_ptr_q[PTR_W-1:0]);
endproperty
a_retire_in_order: assert property (p_retire_in_order);
// An object is never retired while a retransmission is pending for it.
property p_no_retire_with_replay_pending;
@(posedge clk) disable iff (!rst_n)
replay_retire_fire |-> (tx_state_q[retire_obj] != TX_REPLAY_PENDING);
endproperty
a_no_retire_with_replay_pending:
assert property (p_no_retire_with_replay_pending);Architecture. Four properties: the resolution requirement, the negative send case, in-order retirement, and consistency with the transmit state.
Why the fourth exists. The ring and the state machine are two views of one object. They can disagree — a resolution processed while a retry indication is in flight — and §16's priority decision is what keeps them consistent. The fourth property checks it did.
DV. All four always-on. The second catches §45 directly, at the cycle of the send.
47. Configuration Epoch
// ILLUSTRATIVE. Requested-versus-active, and the commit guard that a
// retransmission makes necessary.
link_cfg_t requested_cfg_q, active_cfg_q;
logic [CFG_EPOCH_W-1:0] active_cfg_epoch_q;
assign cfg_commit_allowed =
cfg_validated
&& (staging_occupancy == '0)
&& (replay_outstanding == '0); // ← the term a block-level design omits
always_ff @(posedge clk or negedge rst_n)
if (!rst_n)
active_cfg_epoch_q <= '0;
else if (cfg_commit_fire) begin
active_cfg_q <= requested_cfg_q;
active_cfg_epoch_q <= active_cfg_epoch_q + 1'b1;
endArchitecture. Two copies committed atomically, guarded by both a drained pipeline and an empty replay ring.
Why the replay term. A retained object was framed under the configuration in force when it was created. A retransmission after a commit would transmit it under a configuration the far end no longer expects — and the object's cfg_epoch (§7) is what makes the mismatch detectable rather than silent.
Contract. Everything in the datapath reads active_cfg_q. They see one complete configuration or the previous one, never a mixture (19.1 §37).
Failure. Committing with a non-empty ring, which produces exactly the mismatch above on the first retransmission.
DV. §48; cover a commit attempted with outstanding replay entries.
48. SVA — Configuration Lifetime
// MANDATORY. The active configuration changes only at a guarded commit.
property p_cfg_only_at_commit;
@(posedge clk) disable iff (!rst_n)
$changed(active_cfg_q) |-> $past(cfg_commit_fire);
endproperty
a_cfg_only_at_commit: assert property (p_cfg_only_at_commit);
property p_commit_requires_drain_and_empty_ring;
@(posedge clk) disable iff (!rst_n)
cfg_commit_fire |-> (($past(staging_occupancy) == '0)
&& ($past(replay_outstanding) == '0));
endproperty
a_commit_requires_drain_and_empty_ring:
assert property (p_commit_requires_drain_and_empty_ring);
// A live object's captured epoch never changes under it.
property p_object_epoch_stable;
@(posedge clk) disable iff (!rst_n)
replay_mem[IDX].valid |-> $stable(replay_mem[IDX].cfg_epoch);
endproperty
a_object_epoch_stable: assert property (p_object_epoch_stable);
// A retransmission uses the object's captured epoch, not the live one.
property p_retransmit_uses_captured_epoch;
@(posedge clk) disable iff (!rst_n)
(phy_tx_fire && is_retransmit) |-> (tx_cfg_epoch == replay_mem[send_idx].cfg_epoch);
endproperty
a_retransmit_uses_captured_epoch:
assert property (p_retransmit_uses_captured_epoch);Architecture. Four properties, and the fourth is the retransmission-specific one.
Why it matters. The first three could all hold while a retransmission reads the live epoch instead of the retained one. The fourth is the only property that checks the retry path specifically, and the retry path is exactly where a stale-configuration object lives.
DV. Force a commit with a non-empty ring and confirm the second fires.
49. Recovery
| State | On a recovery |
|---|---|
| Accepted transport obligations | preserved — §4's promise is not withdrawn by a link event |
| Object identities and captured epochs | preserved, and not recomputed |
| Replay history | preserved and re-baselined with the peer — not cleared (§50) |
| Attempt counts | preserved — they are evidence |
| RX partial reassembly | architecture-defined: completed, abandoned explicitly, or retried |
| The duplicate window | re-baselined with the peer — clearing it permits duplicates |
| Credits | re-advertised (19.5) |
| Training, lane map, physical state | rebuilt |
| First fault | preserved — this is the point (19.1 §35) |
Two rows deserve emphasis.
Clearing the duplicate window is as harmful as clearing the ring. After a recovery the peer may retransmit objects it never got confirmation for; a cleared window accepts every one of them as new, which is a second semantic delivery for each.
And the attempt counts are evidence, not just control. An object on its fourth attempt tells a post-silicon engineer something no other counter does.
50. Wrong Recovery — Clearing the Reliability State
// WRONG — a recovery is treated as a reason to start clean.
always_ff @(posedge clk)
if (recovery_entered) begin
for (int i = 0; i < REPLAY_DEPTH; i++) replay_mem[i].valid <= 1'b0;
alloc_ptr_q <= '0;
send_ptr_q <= '0;
retire_ptr_q <= '0;
received_q <= '0; // the duplicate window too
endFour independent failures from one block.
| Cleared | Consequence |
|---|---|
| replay entries | objects the peer never received can never be retransmitted — lost |
| the pointers | the ring's relationship to the peer's expectation is destroyed |
| the duplicate window | every retransmission the peer sends is accepted as new — duplicates |
| — | and the local side cannot know which of the two happened for any object |
Four properties.
The ambiguity is the core harm. Before the recovery, some objects had been received and some had not, and the state that distinguished them has been erased. A recoverable ambiguity has been converted into an unrecoverable one — the design now cannot even determine what to do.
The two failures are opposite and simultaneous. Some objects are lost because they cannot be replayed; others are duplicated because the window forgot them. A single test sees one or the other depending on timing.
And the link comes back and reports success. Training completes, the link reaches an admitting state, and every link-level metric says the recovery worked — which is 19.1 §9's observation at the Adapter.
The correct action is re-baselining, not clearing. Whatever the architecture's mechanism, both sides must agree on what has been received before traffic resumes — and preserving the local state is a precondition for that agreement, not an obstacle to it.
51. SVA — Reliability State Survives Recovery
// MANDATORY. Obligations, identities and evidence survive a recovery.
property p_replay_survives_recovery;
@(posedge clk) disable iff (!rst_n)
recovery_entered |=> ($stable(replay_mem[IDX].valid)
&& $stable(replay_mem[IDX].object_id)
&& $stable(replay_mem[IDX].cfg_epoch)
&& $stable(replay_mem[IDX].attempts));
endproperty
a_replay_survives_recovery: assert property (p_replay_survives_recovery);
// The duplicate window is not cleared (Section 50).
property p_window_survives_recovery;
@(posedge clk) disable iff (!rst_n)
recovery_entered |=> $stable(received_q);
endproperty
a_window_survives_recovery: assert property (p_window_survives_recovery);
// A recovery produces neither an acceptance nor a retirement.
property p_recovery_no_transport_event;
@(posedge clk) disable iff (!rst_n)
recovery_entered |=> ($stable(accept_count) && $stable(retire_count));
endproperty
a_recovery_no_transport_event:
assert property (p_recovery_no_transport_event);
// No new admission while the link manager forbids it.
property p_no_admission_during_recovery;
@(posedge clk) disable iff (!rst_n)
recovery_active |-> !adapter_accept;
endproperty
a_no_admission_during_recovery:
assert property (p_no_admission_during_recovery);
// The first fault survives (19.1 Section 35).
property p_first_fault_survives;
@(posedge clk) disable iff (!rst_n)
recovery_entered |=> $stable(first_fault_q);
endproperty
a_first_fault_survives: assert property (p_first_fault_survives);Architecture. Five properties: entries, window, events, admission, and evidence.
Why the third is the sharpest. It forbids both directions — a recovery must not accept new work and must not retire outstanding work. A design that "helpfully" fails outstanding objects on a link event violates it just as surely as one that clears them.
DV. Inject a recovery with a full ring, a partially-filled window, live reassembly and a captured fault (§56).
52. Error Counters and the Stall Classifier
// Diagnostic only, in the POR reset domain (19.1 Section 7).
logic [63:0] integrity_fail_q;
logic [63:0] retry_count_q;
logic [63:0] replay_attempts_q;
logic [63:0] duplicate_suppressed_q; // Section 32 — the guard working
logic [63:0] out_of_window_q; // Section 32 — stale or corrupt
logic [63:0] recovery_cycles_q;
logic [63:0] replay_full_stall_q; // Section 9's second term biting
logic [63:0] staging_full_stall_q;
logic [63:0] incomplete_object_q; // Section 35 — abandoned reassembly// ILLUSTRATIVE. 15.5 Section 10's causal-priority classifier, at admission.
typedef enum logic [3:0] {
ADP_ACCEPT = 4'd0,
ADP_NO_INPUT = 4'd1, // nothing offered <- must be early
ADP_RECOVERY = 4'd2,
ADP_CFG_COMMIT = 4'd3, // a deliberate quiesce
ADP_LINK_DOWN = 4'd4,
ADP_STAGING_FULL = 4'd5,
ADP_REPLAY_FULL = 4'd6, // Section 9's second term
ADP_NO_CREDIT = 4'd7,
ADP_PHY_STALL = 4'd8,
ADP_UNATTRIB = 4'd9 // must stay at zero
} adp_stall_e;
adp_stall_e adp_reason_d;
always_comb begin
unique case (1'b1)
adapter_accept : adp_reason_d = ADP_ACCEPT;
!in_valid : adp_reason_d = ADP_NO_INPUT;
recovery_active : adp_reason_d = ADP_RECOVERY;
!cfg_stable : adp_reason_d = ADP_CFG_COMMIT;
!link_accepting : adp_reason_d = ADP_LINK_DOWN;
!staging_space_available : adp_reason_d = ADP_STAGING_FULL;
!replay_slot_available : adp_reason_d = ADP_REPLAY_FULL;
!credit_available[in_class] : adp_reason_d = ADP_NO_CREDIT;
!phy_tx_ready : adp_reason_d = ADP_PHY_STALL;
default : adp_reason_d = ADP_UNATTRIB;
endcase
endArchitecture. Ten mutually exclusive reasons summing to elapsed cycles.
ADP_STAGING_FULL against ADP_REPLAY_FULL is the pair that matters most here. They look identical from outside — "the Adapter is not accepting" — and have completely different fixes: staging depth is a pipeline parameter, and replay depth is a reliability window (§19). A design that reports only "buffer full" cannot tell them apart.
And duplicate_suppressed_q is the counter that proves §32 is working. A zero value under a retrying link means the window is not doing its job.
53. The Adapter Scoreboard
// Verification-only. FOUR models. 19.2's semantic model stays separate.
class adapter_scoreboard;
// ---- Layer 1: OBJECT model — accepted, state, epoch, retired.
typedef struct {
bit accepted;
int state;
int cfg_epoch;
bit in_staging, in_replay; // TWO fields — Section 11's window
bit retired, failed;
bit crossed_recovery;
} object_model_t;
object_model_t objs [int];
// ---- Layer 2: REPLAY / HISTORY model — attempts and resolution.
typedef struct {
int seq;
int attempts; // >= 1 once sent
bit resolved;
bit overwritten; // Section 21 — must never be set
} history_model_t;
history_model_t hist [int];
// ---- Layer 3: RX-DELIVERY model — integrity, novelty, delivery count.
typedef struct {
bit integrity_ok;
bit was_duplicate;
bit in_window;
int semantic_deliveries; // MUST be <= 1
bit [63:0] frags_received;
int frag_count;
} rx_model_t;
rx_model_t rx [int];
// ---- Layer 4: RESOURCE model — occupancies and credits by class.
typedef struct {
int staging_occ, replay_occ;
int credits_available, credits_outstanding;
} resource_model_t;
resource_model_t res [int];
// ---- Catches Sections 10 and 14 — an object owned by nothing.
function void check_ownership(int id);
if (objs[id].accepted && !objs[id].retired && !objs[id].failed
&& !objs[id].in_staging && !objs[id].in_replay)
$error("OBJECT %0d owned by NOTHING (Sections 10, 14)", id);
endfunction
// ---- Catches Section 21 — the overwrite, before it corrupts a retry.
function void check_no_overwrite(int id);
if (hist[id].overwritten)
$error("HISTORY %0d overwritten while unresolved (Section 21)", id);
endfunction
// ---- Catches Sections 31, 33, 36 — delivery more than once.
function void check_delivery(int id);
if (rx[id].semantic_deliveries > 1)
$error("OBJECT %0d delivered %0d times (must be <= 1)", id, rx[id].semantic_deliveries);
if ((rx[id].semantic_deliveries > 0) && !rx[id].integrity_ok)
$error("OBJECT %0d delivered with failed integrity (Section 31)", id);
if ((rx[id].semantic_deliveries > 0)
&& ((rx[id].frags_received & frag_mask(rx[id].frag_count))
!= frag_mask(rx[id].frag_count)))
$error("OBJECT %0d delivered incomplete (Section 36)", id);
endfunction
// ---- Catches Section 45 — retired before resolution.
function void check_retirement(int id);
if (objs[id].retired && !hist[id].resolved)
$error("OBJECT %0d retired without resolution (Section 45)", id);
endfunction
// ---- Catches Section 50.
function void check_recovery(int id);
if (objs[id].crossed_recovery && !objs[id].retired && !objs[id].failed
&& !objs[id].in_replay)
$error("OBJECT %0d lost its history across a recovery (Section 50)", id);
endfunction
endclassArchitecture. Four models: the object, its history, its receipt, and the resources.
Layer 1 tracks in_staging and in_replay as two separate fields rather than one "somewhere in the Adapter" flag — because §10's and §14's failures are precisely the state in which both are false.
Layer 2's overwritten flag exists to be always false. It is a detector rather than a measurement, and a single set of it is §21.
And 19.2's semantic model is deliberately absent. The Adapter has no business modelling semantic operations, and a scoreboard that merges the two loses the ability to say which layer failed (19.2 §49).
54. Flagship Trace 1 — Transmit With a Retry
Illustrative. Cycle numbers illustrative.
| Cyc | Admission | Staging | TX state | Replay ring (a/s/r) | CRC | PHY | Attempts |
|---|---|---|---|---|---|---|---|
| 0 | ready | 0 | — | 4/4/4 | idle | idle | — |
| 1 | accepts | 1 | STAGED | 4/4/4 | — | idle | 0 |
| 2 | — | 1 | STAGED | 4/4/4 | starts | idle | 0 |
| 5 | — | 1 | STAGED | 4/4/4 | feeding | idle | 0 |
| 8 | — | 1 | STAGED | 5/4/4 | final | idle | 0 |
| 9 | — | 1 | HISTORY_OWNED | 5/4/4 | — | idle | 0 |
| 9 | — | both own it | — | — | — | — | §13's window |
| 10 | — | 0 | HISTORY_OWNED | 5/4/4 | — | idle | 0 |
| 12 | — | 0 | SENT_WAIT_ACK | 5/5/4 | — | attempt 1 | 1 |
| 30 | — | 0 | SENT_WAIT_ACK | 5/5/4 | — | in flight | 1 |
| 38 | — | 0 | REPLAY_PENDING | 5/5/4 | — | — | 1 |
| 38 | — | — | — | — | — | — | retry indication |
| 44 | — | 0 | SENT_WAIT_ACK | 5/5/4 | — | attempt 2 | 2 |
| 70 | — | 0 | RETIRED | 5/5/5 | — | — | 2 |
| 71 | — | 0 | FREE | 5/5/5 | — | — | — |
Seven readings.
Cycle 9 is the dual-ownership window — staging and replay both hold it. §14's design frees the only copy at cycle 12.
Cycle 10 releases staging, not at cycle 12 when the PHY took it. Staging's job ended when replay confirmed.
The ring's allocation pointer advances at cycle 8 and its retirement pointer at cycle 70 — sixty-two cycles apart. That gap is the reliability window, and it is what §19's depth must cover.
Cycle 38: a retry indication returns the object to REPLAY_PENDING without passing through STAGED (§15). The same object, the same sequence, the same retained copy.
Cycle 44 sends attempt 2 from replay_mem, byte-identical to attempt 1 (§19). A modified copy would not be recognised as a duplicate at the far end.
Cycle 70 retires on resolution. §45's design retires at cycle 12, and the retry at cycle 44 would then have had nothing to send.
And the attempt count reaches 2 and is retained as evidence (§6).
55. Flagship Trace 2 — Receive, Then the Duplicate
| Cyc | Beats | Integrity | Window | Reassembly | Delivery | Ack |
|---|---|---|---|---|---|---|
| 20 | frag 0 | — | — | {1,0,0} | — | — |
| 22 | frag 1 | — | — | {1,1,0} | — | — |
| 24 | frag 2 | — | — | {1,1,1} | — | — |
| 25 | — | checked: OK | — | complete | — | — |
| 26 | — | — | not a duplicate | — | — | — |
| 27 | — | — | recorded | — | delivered ×1 | — |
| 29 | — | — | — | — | — | ack sent |
| the duplicate, later | ||||||
| 60 | frag 0 | — | — | {1,0,0} | — | — |
| 64 | frag 2 | — | — | {1,1,1} | — | — |
| 65 | — | checked: OK | — | complete | — | — |
| 66 | — | — | DUPLICATE | — | none | — |
| 67 | — | — | bit already set | — | — | — |
| 68 | — | — | — | — | — | ack sent AGAIN |
Five readings.
Cycles 25 to 27: integrity, then novelty, then delivery. Three checks before the object reaches the protocol engine. §31's design delivers at cycle 24.
Cycle 66: the duplicate is recognised because the window remembers position 10 and not merely the last sequence. §33's register would have forgotten it by cycle 60.
Cycle 67: recording is idempotent. The bit is already set and nothing changes.
Cycle 68: the acknowledgement is sent again. §34's third property — suppressing the effect and the reply livelocks the sender.
And the semantic delivery count is exactly one across both arrivals — §4's obligation, observed.
56. Flagship Trace 3 — Recovery With Live Reliability State
| Cyc | Ring (a/s/r) | Window | TX state | Admission | Link | Must be true |
|---|---|---|---|---|---|---|
| 100 | 12/11/6 | 4 positions set | mixed | accepting | operational | 6 unresolved |
| 104 | 12/11/6 | 4 set | mixed | accepting | error | — |
| 105 | 12/11/6 | 4 set | unchanged | stops | recovery | nothing cleared |
| 106 | 12/11/6 | 4 set | unchanged | stopped | recovery | first fault captured |
| 110 | 12/11/6 | 4 set | unchanged | stopped | retraining | attempts preserved |
| 150 | 12/11/6 | 4 set | unchanged | stopped | re-baselining | both sides agree |
| 158 | 12/11/6 | 4 set | unchanged | stopped | operational | — |
| 160 | 12/11/6 | 4 set | replays begin | resumes | operational | — |
| 200 | 12/12/12 | 6 set | all RETIRED | accepting | operational | all six resolved |
Six readings.
Cycle 105: six unresolved entries, six preserved. The pointers, the retained copies, the attempt counts and the window are all untouched. §50's design zeroes all of it here.
Cycle 106: the fault is captured before the retrain (19.1 §33) — the ordering that keeps the evidence.
Cycle 150 is re-baselining, not clearing. Whatever the architecture's mechanism, both sides establish what has been received — which is only possible because the local state survived.
Cycle 160: admission resumes and replays begin. Admission stopped at 105 and nothing was retired in between — §51's third property.
Cycle 200: all six resolve. Some were retransmitted and some were already received; the window and the re-baselining decided which, and no object was delivered twice.
And the link may have returned degraded, which changes throughput and not one field of any entry.
57. Coverage
covergroup cg_adapter @(posedge clk);
option.per_instance = 1;
// --- Admission (Sections 9-11).
cp_admit_refusal : coverpoint admit_refusal_reason {
bins staging = {0}; bins replay = {1}; bins credit = {2};
bins link = {3}; bins cfg = {4}; // each term alone
}
cp_staging_occ : coverpoint staging_occupancy {
bins empty = {0}; bins mid = {[1:STAGING_DEPTH-1]}; bins full = {STAGING_DEPTH};
}
cp_replay_occ : coverpoint replay_outstanding {
bins empty = {0}; bins mid = {[1:REPLAY_DEPTH-1]}; bins full = {REPLAY_DEPTH};
}
cp_dual_window : coverpoint dual_ownership_window_active; // Section 13
cp_error_in_window : coverpoint error_during_ownership_handoff;
// --- TX state (Sections 15-18).
cp_tx_state : coverpoint tx_state_q_ut { bins each[] = {[0:6]}; }
cp_simul_retry_resolve : coverpoint retry_and_resolve_same_cycle; // Section 16
cp_attempts : coverpoint attempts_q_ut {
bins one = {1}; bins few = {[2:3]}; bins budget = {ATTEMPT_BUDGET};
}
// --- Ring (Sections 20-24).
cp_ring_wrap : coverpoint ring_pointer_wrapped {
bins none = {0}; bins alloc = {1}; bins send = {2}; bins retire = {3};
}
cp_simul_alloc_retire : coverpoint alloc_and_retire_same_cycle;
cp_ring_full_sustained : coverpoint ring_full_for_n_cycles {
bins none = {0}; bins brief = {[1:16]}; bins sustained = {[17:$]}; // Section 21
}
// --- Integrity (Sections 25-29).
cp_crc_outcome : coverpoint integrity_outcome {
bins pass = {0}; bins fail = {1};
}
cp_stall_during_crc : coverpoint stall_between_crc_and_send; // Section 27
cp_back_to_back : coverpoint consecutive_objects_differing; // Section 29
// --- Receive (Sections 30-37).
cp_rx_outcome : coverpoint rx_object_outcome {
bins delivered = {0}; bins corrupt = {1}; bins duplicate = {2};
bins out_of_window = {3}; bins no_space = {4}; // all five gate terms
}
cp_dup_position : coverpoint duplicate_window_position {
bins at_base = {0}; bins middle = {1}; bins far_edge = {2}; // Section 33
}
cp_frag_event : coverpoint fragment_event {
bins in_order = {0}; bins reordered = {1}; bins duplicate = {2}; bins missing = {3};
}
// --- Scheduler (Sections 40-43).
cp_sched_select : coverpoint tx_select_ut {
bins none = {0}; bins new_traffic = {1}; bins replay = {2}; bins mgmt = {3};
}
cp_replay_budget : coverpoint replay_budget_q_ut {
bins full = {REPLAY_BUDGET}; bins some = {[1:REPLAY_BUDGET-1]}; bins spent = {0};
}
cp_starvation : coverpoint starvation_override_fired {
bins none = {0}; bins replay_override = {1}; bins new_override = {2};
}
// --- Configuration and recovery (Sections 47-51).
cp_cfg_context : coverpoint cfg_commit_context {
bins idle = {0}; bins blocked_by_staging = {1};
bins blocked_by_replay = {2}; bins forced = {3};
}
cp_recovery_context : coverpoint recovery_with_state {
bins none = {0};
bins ring_nonempty = {1};
bins window_populated = {2};
bins reassembly_partial = {3};
bins all = {4}; // THE case — Section 56
}
// --- Attribution (Section 52).
cp_stall_reason : coverpoint adp_reason_d { bins each[] = {[0:9]}; }
// --- Crosses that carry the information.
x_ring_retry : cross cp_ring_full_sustained, cp_attempts; // Section 21
x_dup_position : cross cp_dup_position, cp_rx_outcome; // Section 33
x_recovery_ring : cross cp_recovery_context, cp_replay_occ; // Section 56
x_sched_budget : cross cp_sched_select, cp_replay_budget; // Section 42
endcovergroupNine bins worth calling out:
cp_ring_full_sustained.sustained crossed with cp_attempts. §21's precondition — the ring genuinely full for a long period with retries occurring. Only a soak test reaches it.
cp_error_in_window. §13's one-cycle handoff, with an error injected inside it.
cp_dup_position — all three. §33's argument: a duplicate at the base is caught by a last-sequence register and one at the far edge is not. Only the far-edge bin distinguishes the two designs.
cp_stall_during_crc. §27's precondition — a stall between the computation and the send, with a live metadata source.
cp_back_to_back. §29's off-by-one, undetectable with a single object.
cp_simul_retry_resolve. §16's priority decision, exercised.
cp_rx_outcome — all five. The five gate terms individually (§30).
cp_cfg_context.blocked_by_replay. §47's replay term, shown to block a commit.
And cp_recovery_context.all — a recovery with a non-empty ring, a populated window and partial reassembly simultaneously. Every property in §51 depends on it.
58. Debug Taxonomy
| Signature | Most likely cause | First instrument |
|---|---|---|
| Works until sustained load, then corrupts | §21 — the ring overwrote a live entry | cp_ring_full_sustained; the overwrite detector |
| A retry delivers the wrong data | §21 again — the retained copy was replaced | history model's overwritten flag |
| Integrity failures begin only once the pipeline fills | §29 — the result is one stage from its object | first-object-of-burst passes, rest fail? |
| Intermittent integrity failures on a healthy link | §27 — covered metadata changed during a stall | is the covered bundle one register? |
| A duplicate semantic object after a link error | §31 or §33 — delivery before the verdict, or too small a window | duplicate_suppressed_q; window size vs replay depth |
| Retries never stop on one object | §34's third property — the duplicate was not acknowledged | is the reply sent for a recognised duplicate? |
| An object is lost after a recovery | §50 — reliability state cleared | what changed at the recovery cycle |
| Duplicates appear after a recovery | §50's other half — the window cleared | was received_q preserved? |
| A replay storm blocks all new traffic | §42 — unbounded replay priority | cp_replay_budget.spent; is the budget finite? |
| A retry waits until the ring fills | §41 — unbounded new-traffic priority | replay age against REPLAY_BOUND |
| The Adapter stops accepting and nothing is wrong | §45 — retirement never happens, so the ring never drains | is retirement on resolution or on send? |
| The far end rejects after a configuration change | §47 — a retransmission under the new configuration | is the commit guarded by an empty ring? |
| "Buffer full" with no idea which | §52 — one stall bucket | split STAGING_FULL from REPLAY_FULL |
Row 1 is the worst. Works until sustained load, then corrupts is the ring overwrite, and it substitutes one object's data under another's identity with every check in the system passing.
59. Debug Checklist
- Which object — the Adapter's identity? (§7)
- Which sequence identity, and which semantic identity does it carry? (§8)
- What TX state, and for how long? (§15)
- Did the committed state equal the next-state function's output? (§18)
- Was the object owned by staging, replay, or both at every cycle? (§11)
- Was a replay slot reserved at admission? (§9)
- What are the three ring pointers, and what is the derived occupancy? (§20)
- Has allocation ever approached retirement? (§22, §23)
- Is the retained copy byte-identical to what was sent? (§19)
- How many attempts has this object had? (§16)
- Was the attempt budget exhausted? (§16)
- Which resource class, and were credits available for that class? (§38)
- Was the integrity result computed over exactly what was sent? (§28)
- Did the covered bundle change during a stall? (§27)
- Is the result's valid aligned with the object's? (§29)
- On the receive side: did all five gate terms hold? (§30)
- Was this object in the duplicate window, and at which position? (§32)
- How large is the window relative to the sender's replay depth? (§32)
- Was a recognised duplicate still acknowledged? (§34)
- Which fragments arrived, and were any duplicated or missing? (§35)
- What retired the entry — a resolution, or the send? (§44, §45)
- Which configuration epoch did the object carry, and did it change? (§47)
- Was a commit attempted with a non-empty ring? (§48)
- Did a recovery occur, and did the ring, window and attempts survive? (§51)
- Was admission stopped during the recovery, and nothing retired? (§51)
- What does the first-fault record say? (19.1 §35)
- What does the stall histogram say, and is
ADP_UNATTRIBnon-zero? (§52) - Which of the four scoreboard layers diverged first? (§53)
60. Common Misconceptions
"The Adapter is just CRC plus a FIFO." It owns admission with reservation, two buffers with different lifetimes, a three-pointer ring, an integrity pipeline whose alignment is a correctness property, a duplicate window, a scheduler with two liveness bounds, and a retirement rule that waits for the far end. The FIFO is one of ten rows in §6's table.
"The replay buffer is a normal TX queue." A queue's entry is freed when it is consumed; a replay entry is freed when the far end no longer needs it — an entirely different event, a full round trip later, and possibly after several retransmissions (§12).
"A sent object can be freed." Sending is the first of four events, and only the last one — resolution — may retire the entry. Freeing at the send destroys the copy the retry mechanism exists to use, and simultaneously makes the ring's occupancy a measure of the pipeline rather than of outstanding reliability (§44, §45).
"One identity is enough for protocol, Adapter and PHY." Three namespaces with three lifetimes, recycled by three layers on three schedules. A retransmission reuses the sequence identity and creates no new object and no new semantic operation (§8).
"A replay is another semantic request." It is the same object, re-sent. Treating it as new allocates a second object, consumes a second credit, and may produce a second semantic delivery at the far end (§8, §15).
"CRC may be checked after delivery." Delivering first and checking after hands the protocol engine an object that may be corrupt, a duplicate, or outside the window — three of the four validity-and-novelty combinations handled wrongly, with the transport behaving exactly as specified (§31).
"A response counter can track fragment completeness." A duplicated fragment — a correct transport retry — increments the count while a different fragment is still missing, and the object is delivered incomplete (§36).
"One last-sequence register is enough to suppress duplicates." It detects only an immediate retransmission with nothing in between, which is the least likely pattern — because a retransmission follows a round trip during which other objects were almost certainly sent (§33).
"Credits are global." A scalar stalls everything when one class is exhausted, and admits an object for an exhausted class when another has room. The first is a throughput loss and the second is the overflow flow control existed to prevent (§39).
"Recovery should clear retry state." Clearing the ring loses objects the peer never received; clearing the window duplicates every object it retransmits. Both happen at once, the local side cannot tell which applies to any object, and the link reports a successful recovery (§50).
"Replay traffic should always have absolute priority." Correct as a transient policy and fatal as a permanent one — under a persistent error rate, replays regenerate replays and new traffic never runs. The fix is a budget on consecutive replays, because what must be broken is a run (§42).
"Buffer full is a performance issue, not a correctness issue." A full replay ring is a correctness constraint: an object accepted without a slot to retain it cannot honour the delivery promise, which is why the slot is an admission term rather than a downstream check (§9, §10).
"A clean CRC proves exactly-once delivery." Integrity proves the bytes survived. Exactly-once needs the window, the reassembly bitmap and the gate ordering — and every one of them can fail with perfect integrity throughout (§30–§37).
61. Understanding Check
62. Summary and What Comes Next
The Adapter owns transport reliability and not semantics. An accepted object is delivered exactly once or explicitly failed, while attempts underneath vary from zero to many.
Reserve before you accept. A replay slot is an admission term, because accepting an object the design cannot retain is a promise it cannot keep — and all three ways of discovering that later are failures.
Staging and replay are different lifetimes even in one memory: one is pipeline depth, the other is a round trip plus retries, and freeing on the send destroys the copy the retry exists to use.
Three pointers, one extra bit. Allocation passing retirement substitutes one object's data under another's identity with every check in the system passing — and a derived occupancy cannot drift.
Integrity is an alignment problem as much as a coverage problem. A covered field that changes during a stall, or a result one stage from its object, rejects perfectly correct data — and one of the two passes every single-object test.
Check before you deliver, and remember more than the last sequence. Three of four validity-and-novelty combinations are mishandled by a design that delivers first, and a window of one detects the least likely duplicate pattern.
Retire on resolution. It is the last of four events, and picking the first breaks the retry contract and defeats the admission discipline simultaneously.
Bound the scheduler in both directions, because starving replay collapses throughput and starving new traffic delivers nothing at all.
And a recovery re-baselines rather than clears — clearing loses some objects, duplicates others, and destroys the state that could have told you which.
The Adapter now has several kinds of retained state — staging, replay history, reassembly, the receive window and the protocol-facing queues. The next chapter stops treating those as generic FIFOs and asks the architectural questions that decide whether they can absorb latency without overflowing, deadlocking, or losing ownership: what each kind is for, how deep each must be, when backpressure must begin, and what a buffer's capacity has to do with correctness.
- 19.4 — UCIe Buffering — buffer sizing, watermarks, ping-pong design.
Browse the full path on the UCIe tutorials index.