UCIe · Module 19
Link Architecture
If you had to implement one side of a UCIe link in RTL, what blocks exist, what state does each own, and what crosses each boundary — the state-ownership table that is the chapter's backbone, a reset hierarchy that must not collapse into one tree, the dual-ownership window between the TX queue and the replay buffer, a receive path that must not deliver before the verdict, three cooperating state machines, and the recovery ordering that captures the fault before retraining erases it.
Eighteen modules have established what a UCIe-based system must do. This chapter asks the question all of it eventually becomes: if you had to build one side of the link, what would be in it?
1. The One-Sentence Model
A UCIe link is not one state machine. It is a hierarchy of state machines and queues whose lifetimes overlap — semantic obligations, reliability state, flow-control state, link-management state, training state, lane state and physical state — and the entire architecture is the discipline of never letting one of them reset, release or decide on behalf of another.
2. What This Chapter Owns
This chapter synthesises; it does not repeat. Every mechanism below has a chapter that teaches it in depth, and the value here is the partition, the boundaries, and the state lifetimes — which no single earlier chapter could show, because each was looking at one mechanism.
| Mechanism | Taught in | What this chapter adds |
|---|---|---|
| Layer responsibilities, Protocol / Adapter / PHY | 4.2 | which blocks own which state (§7) |
| Request and response flow, transaction lifecycle | 12.1 · 12.2 · 12.4 | where the queues sit in the top level (§12, §22) |
| Credit-based flow control | 13.1 | the credit manager as a block (§20) |
| CRC, detection, the delivery gate | 14.1 | the RX pipeline that implements it (§24) |
| Error recovery, ordering, first-fault capture | 14.2 · 14.5 | the recovery protocol at top level (§37) |
| Replay, retirement, duplicate suppression | 14.3 | the ownership handoff to the replay block (§17) |
| Lane repair, requested-versus-active configuration | 14.4 | the configuration block and its commit (§41) |
| Clock domains and CDC bridges at package scope | 18.4 §17 | the crossings inside one link (§45) |
| Protocol-layer engine RTL | 19.2 — Protocol Engines | — |
The three things that exist only here:
§7's state-ownership table. Every piece of state in a link, with its owner, its index, its allocation and release events, and whether it survives a recovery and a reset. It is the chapter's backbone and everything else refers to it.
§17's dual-ownership window. The only place in this curriculum where an object is deliberately owned by two structures at once — and the exact rule for when that window opens and closes.
And §30's three-FSM partition. Link management, training and lane management are three machines with three lifetimes, and §33 shows what a single merged FSM does wrong.
3. Sourcing
4. The Top Level
Read the three control blocks on the right. They are beside the datapath, not in it — because a link manager's state lives for the link's whole operational life, a training controller's for one training sequence, and a datapath object's for a few dozen cycles. §30 is why merging them is wrong.
5. The Block Inventory
| Block | Owns | Lifetime of its state |
|---|---|---|
ucie_tx_ingress | the TX queue; accepted-but-unframed objects | per object |
ucie_adapter_tx | framing, CRC generation, TX pipeline registers | per object, per stage |
ucie_replay | retained objects, the three pointers, attempt counts | until acknowledged |
ucie_credit_mgr | per-class credit counters | per link epoch |
ucie_rx_ingress | reconstruction, validation, the duplicate history | per object |
ucie_rx_egress | the RX queue and the delivery gate | per object |
ucie_link_mgr | operational and recovery policy | the link's operational life |
ucie_training_ctrl | the training sequence | one training attempt |
ucie_lane_mgr | lane map, repair state, requested/active width | configuration epoch |
ucie_cfg | requested and active configuration, the epoch | configuration epoch |
ucie_fault_mgr | first-fault record, escalation, counters | deliberate reset only |
ucie_perf | performance counters, the stall classifier | deliberate reset only |
6. A Top-Level Skeleton
// ILLUSTRATIVE top-level. NOT a UCIe-defined module, port list or hierarchy.
// The point is the block boundaries and which clocks and resets reach where.
module ucie_link_top #(
parameter int PROTO_W = 256, // protocol-side payload width
parameter int NUM_LANES = 16,
parameter int TX_Q_DEPTH = 32,
parameter int RX_Q_DEPTH = 32,
parameter int REPLAY_DEPTH = 64,
parameter int NUM_CLASSES = 4,
parameter int CREDIT_INIT = 16
) (
// --- Clocks and resets. THREE domains, not one (Section 45).
input logic proto_clk,
input logic proto_rst_n, // power-on scope
input logic adapter_clk,
input logic adapter_rst_n,
input logic phy_clk,
input logic phy_rst_n,
// --- Protocol-side interface (conceptually FDI-facing).
input logic tx_valid,
output logic tx_ready,
input logic [PROTO_W-1:0] tx_payload,
input logic [$clog2(NUM_CLASSES)-1:0] tx_class,
output logic rx_valid,
input logic rx_ready,
output logic [PROTO_W-1:0] rx_payload,
// --- PHY-side interface (conceptually RDI-facing).
output logic [NUM_LANES-1:0] phy_tx_data,
output logic phy_tx_valid,
input logic phy_tx_ready,
input logic [NUM_LANES-1:0] phy_rx_data,
input logic phy_rx_valid,
// --- Control and observability.
input logic cfg_write,
input logic [CFG_W-1:0] cfg_wdata,
output logic link_active,
output logic link_degraded,
output logic [FAULT_W-1:0] first_fault
);
// Parameter sanity — a bad parameter should fail the build (18.4's argument).
initial begin
assert (TX_Q_DEPTH > 0) else $fatal(1, "TX_Q_DEPTH must be positive");
assert (REPLAY_DEPTH >= TX_Q_DEPTH)
else $fatal(1, "replay must hold at least the TX queue's worth (Section 18)");
assert (CREDIT_INIT > 0) else $fatal(1, "CREDIT_INIT must be positive");
assert (NUM_LANES > 0) else $fatal(1, "NUM_LANES must be positive");
end
// --- Datapath blocks ---------------------------------------------------
ucie_tx_ingress #(.DEPTH(TX_Q_DEPTH)) u_tx_ingress (...);
ucie_credit_mgr #(.NUM_CLASSES(NUM_CLASSES), .INIT(CREDIT_INIT)) u_credit (...);
ucie_adapter_tx u_adapter_tx (...);
ucie_replay #(.DEPTH(REPLAY_DEPTH)) u_replay (...);
ucie_rx_ingress u_rx_ingress (...);
ucie_rx_egress #(.DEPTH(RX_Q_DEPTH)) u_rx_egress (...);
// --- Control blocks — beside the datapath (Section 4) -------------------
ucie_link_mgr u_link_mgr (...);
ucie_training_ctrl u_training (...);
ucie_lane_mgr #(.NUM_LANES(NUM_LANES)) u_lane_mgr (...);
ucie_cfg u_cfg (...);
// --- Diagnostics — a reset domain of their own (Sections 8, 39) ---------
ucie_fault_mgr u_fault_mgr (...);
ucie_perf u_perf (...);
endmoduleArchitecture. Twelve instances, three clock inputs, three reset inputs, and an elaboration check that encodes one real design rule: the replay buffer must be at least as deep as the TX queue, or the queue can accept objects the replay path cannot retain (§18).
State. None at this level — the top is structural.
Cycle behaviour. Nothing. A top level that contains logic is a top level whose boundaries were not decided, and the discipline of keeping it structural is what makes the block list reviewable.
Contract. Each block's clock and reset are explicit at the port list. A reader can see, without opening a block, which domain it lives in — which is what makes §45's CDC analysis possible from the top.
Failure. One clk and one rst_n for everything, which forces every CDC decision inside the blocks where nobody reviews them, and makes §9's single-reset bug the path of least resistance.
DV. Elaboration checks; a connectivity check that each block's reset comes from the intended domain.
7. The State-Ownership Table
The chapter's backbone. Every subsequent section refers to a row of it.
| State | Owner block | Indexed by | Allocated when | Released when | Survives recovery? | Survives a deliberate reset? |
|---|---|---|---|---|---|---|
| Semantic request/obligation | the protocol client | semantic identity | the client issues it | the protocol's completion point | yes | policy-defined |
| TX queue entry | ucie_tx_ingress | queue slot | on tx_valid && tx_ready | when the object is owned by replay (§17) | yes — held | no |
| TX pipeline registers | ucie_adapter_tx | stage | on stage advance | on the next advance | flushed or held, by policy | no |
| Replay entry | ucie_replay | replay sequence | when the Adapter accepts the object | on acknowledgement (14.3 §14) | re-baselined with the peer | no |
| Attempt count | ucie_replay | replay sequence | with the entry | with the entry | survives — it is evidence | no |
| Credit | ucie_credit_mgr | class | at advertisement | consumed on transfer; returned on release | re-advertised | no |
| Duplicate history | ucie_rx_ingress | transport sequence | on delivery | after the quarantine window | re-baselined | no |
| RX queue entry | ucie_rx_egress | queue slot | on validated delivery | on consumption | yes — held | no |
| Active configuration | ucie_cfg | — | at a commit | at the next commit | re-negotiated | re-established |
| Requested configuration | ucie_cfg | — | on a write | at a commit | yes | no |
| Configuration epoch | ucie_cfg | — | with the active config | with the active config | advances | resets |
| Lane map / repair state | ucie_lane_mgr | lane | at training or repair | at the next training | re-established | re-established |
| Link-manager state | ucie_link_mgr | — | at reset | never | transitions, not cleared | resets |
| Training state | ucie_training_ctrl | — | at training entry | at training exit | re-run | resets |
| First-fault record | ucie_fault_mgr | — | at the first error | deliberate clear only | yes — this is the point | should survive |
| Performance counters | ucie_perf | various | at reset | deliberate clear only | yes | should survive |
Three readings, and each is a section.
Look at the "survives recovery" column. Almost every row says yes, held, or re-established — and only the transport rows are rebuilt. That column is 14.2 §4's rule expressed as an implementation requirement, and §9 is the design that turns the whole column into "no".
Look at the last two rows. Diagnostics survive a deliberate reset in a correct design, because state cleared by the event you are diagnosing has no evidentiary value (14.5). That is a reset-domain decision made once, at the top level, and easy to omit.
And look at rows 2 and 4 together. The TX queue entry is released when replay owns the object, not when the PHY sends it. That handoff is §17, and it is the only deliberate dual-ownership window in the design.
8. The Reset Hierarchy
| Scope | Trigger | Clears |
|---|---|---|
| Power-on reset | power sequencing | everything, including diagnostics |
| Deliberate soft reset | a system-level action | most state; diagnostics should survive (§7) |
| Protocol flush | a policy decision, if the architecture permits | semantic queues at the protocol boundary |
| Adapter recovery | a recoverable transport fault | reliability and link state — not semantics |
| PHY retrain | a training request | training and lane state |
// ILLUSTRATIVE reset and recovery controls. FIVE separate signals, because
// there are five scopes — and Section 9 is the design that has one.
logic por_n; // power-on: everything
logic soft_reset; // deliberate, system-level
logic protocol_flush; // semantic queues, by policy only
logic adapter_soft_reset; // reliability/link state — NOT a reset of semantics
logic phy_retrain; // training and lane stateA link recovery drives
adapter_soft_resetandphy_retrain. It drives neitherprotocol_flushnorsoft_reset, and it never reaches the fault manager or the performance counters. Every arrow in that sentence is a decision, and §9 is what happens when they are collapsed.
9. Wrong Top-Level Architecture — One Recovery Reset
// WRONG — one reset, driven by everything.
assign reset_all = !rst_n || link_recovery;
// ...and everything uses it:
ucie_tx_ingress u_tx (.rst_n(!reset_all), ...);
ucie_replay u_rep (.rst_n(!reset_all), ...);
ucie_credit_mgr u_cr (.rst_n(!reset_all), ...);
ucie_fault_mgr u_fm (.rst_n(!reset_all), ...); // ← the worst one
ucie_perf u_pf (.rst_n(!reset_all), ...);Walk §7's table with this line in place.
| State | Should be, on recovery | Actually |
|---|---|---|
| TX queue entries | held | cleared — accepted objects vanish |
| Replay entries | re-baselined with the peer | cleared — nothing can be retransmitted |
| Credits | re-advertised | cleared — and re-advertised, so this row survives by luck |
| RX queue entries | held | cleared — validated objects discarded |
| First-fault record | preserved | cleared — the cause is erased |
| Performance counters | preserved | cleared — the history is erased |
Five properties, and this is the chapter's flagship failure.
Accepted objects vanish. The protocol client handed them over; the TX queue accepted them; now they are gone with no error and no record — which is 10.1 §10's rule violated at the top level.
The link comes back and reports success. Training completes, the link reaches its active state, link_active asserts. Every link-level metric says the recovery worked.
The semantic layer is left with obligations that will never complete. Its outstanding table still holds them; nothing will ever respond; and it will eventually time out and face 12.4 §16's ambiguity with no evidence at all.
And the evidence of the cause is gone. The first-fault record — the one structure whose entire purpose is to survive the event (14.5) — was cleared by the event. Post-silicon sees a timeout and nothing else, and the same failure recurs indefinitely because nobody can diagnose it.
The line looks like careful engineering. reset_all reads as thorough, conservative, safe. It is the single most destructive line in this chapter, and it is one expression.
10. SVA — Semantic and Diagnostic State Survive a Recovery
// MANDATORY. The property that makes Section 9 impossible.
property p_recovery_preserves_semantics;
@(posedge adapter_clk) disable iff (!por_n)
link_recovery_entered |=> ($stable(tx_q_occupancy)
&& $stable(rx_q_occupancy)
&& $stable(first_fault_q)
&& $stable(perf_counters_q));
endproperty
a_recovery_preserves_semantics:
assert property (p_recovery_preserves_semantics);
// A recovery never asserts a semantic flush or a soft reset.
property p_recovery_is_not_a_reset;
@(posedge adapter_clk) disable iff (!por_n)
link_recovery_entered |-> (!protocol_flush && !soft_reset);
endproperty
a_recovery_is_not_a_reset: assert property (p_recovery_is_not_a_reset);
// The fault record survives everything except power-on.
property p_first_fault_survives_all_but_por;
@(posedge adapter_clk) disable iff (!por_n)
(link_recovery_entered || adapter_soft_reset || phy_retrain || soft_reset)
|=> $stable(first_fault_q);
endproperty
a_first_fault_survives_all_but_por:
assert property (p_first_fault_survives_all_but_por);Architecture. Three properties: preservation, scope separation, and evidence.
Why every one is disable iff (!por_n) and not the local reset. The properties are about surviving those resets. Disabling them on the reset they are checking makes them vacuous — the most common way a survival property silently does nothing.
Why the second is a negative property. It forbids a specific wiring — a recovery signal reaching a reset — which is exactly §9's line, and it fails immediately rather than waiting for a symptom.
DV. Inject a recovery with a full TX queue, a full RX queue, live replay entries and a captured fault. All three must hold.
11. The Transmit Path, Walked
1. The protocol client offers an object. (§12 — admission)
2. tx_ingress accepts it into the TX queue. -> the QUEUE owns it
3. The credit manager gates progress. (§20)
4. adapter_tx frames it; CRC is generated. (§3 — where applicable)
5. THE REPLAY BLOCK TAKES OWNERSHIP. (§17 — the dual window opens)
6. The TX queue entry is released. (§17 — the window closes)
7. The object crosses RDI to the PHY.
8. The lanes transmit it. -> a physical ATTEMPT
9. An acknowledgement resolves the replay entry. (§16)Two things to notice before any RTL.
Ownership changes hands exactly once in that list, at steps 5 and 6 — and those are two steps rather than one because the handoff is not instantaneous (§17).
And the object is never unowned. At every cycle it is held by the queue, by both, or by replay. §18 is the design where a cycle exists in which neither holds it.
12. The TX Ingress Queue
// ILLUSTRATIVE TX ingress. Production-style: one owner per counter, explicit
// simultaneous handling, payload RAM not reset.
typedef struct packed {
logic [PROTO_W-1:0] payload;
logic [$clog2(NUM_CLASSES)-1:0] tclass;
logic [SEM_ID_W-1:0] sem_id; // the client's identity
logic [CFG_EPOCH_W-1:0] cfg_epoch; // Section 42
} tx_entry_t;
tx_entry_t tx_mem [TX_Q_DEPTH]; // inferred RAM, NOT reset
logic [PTR_W-1:0] tx_wr_q, tx_rd_q;
logic [OCC_W-1:0] tx_occ_q;
wire tx_full = (tx_occ_q == TX_Q_DEPTH[OCC_W-1:0]);
wire tx_empty = (tx_occ_q == '0);
// Admission is a conjunction — every downstream stage that must hold it.
assign tx_ready = !tx_full
&& credit_available[tx_class] // Section 20
&& replay_space_available // Section 18 — CRITICAL
&& link_admits_new_traffic // Section 32
&& cfg_stable; // Section 43
always_ff @(posedge adapter_clk or negedge adapter_rst_n)
if (!adapter_rst_n) begin
tx_wr_q <= '0;
tx_rd_q <= '0;
tx_occ_q <= '0;
end else begin
if (tx_push_fire) tx_wr_q <= tx_wr_q + 1'b1;
if (tx_pop_fire) tx_rd_q <= tx_rd_q + 1'b1;
// ONE owner, all four combinations enumerated (Section 13 is the alternative).
unique case ({tx_push_fire, tx_pop_fire})
2'b10: tx_occ_q <= tx_occ_q + 1'b1;
2'b01: tx_occ_q <= tx_occ_q - 1'b1;
default: ; // 2'b00 and 2'b11 hold
endcase
end
always_ff @(posedge adapter_clk)
if (tx_push_fire) tx_mem[tx_wr_q] <= tx_push_data;
assign tx_head = tx_mem[tx_rd_q];Architecture. A standard queue with a five-term admission conjunction. The third term is the one that matters most: replay_space_available must be checked at admission, not at framing — accepting an object the replay path cannot retain makes it unretransmittable (§18), and by then it is too late to refuse.
State. TX_Q_DEPTH payload entries plus three small registers. The payload is inferred RAM and deliberately not reset — occupancy starts at zero and gates every pop, so contents are unobservable before they are written (17.4 §14). A reset loop here would prevent RAM inference.
Cycle behaviour. Push and pop are handshake-qualified. The unique case handles the simultaneous cycle in one place, and §13 is what two independent statements do.
Contract. The Adapter reads tx_head and relies on it being stable while it is not popped (§14). The protocol client relies on tx_ready being a complete answer.
Failure. §13 for the counter, §18 for the admission term. Also making tx_ready equal !tx_full alone — which accepts objects there are no credits for, no replay space for, and no link to carry.
DV. §14's properties; cover empty, mid, full, and the simultaneous push-pop cycle.
13. Wrong RTL — Independent Count Assignments
// WRONG — two independent statements for one counter.
always_ff @(posedge adapter_clk) begin
if (tx_push_fire) tx_occ_q <= tx_occ_q + 1'b1;
if (tx_pop_fire) tx_occ_q <= tx_occ_q - 1'b1; // ← the second wins on a tie
endOn a simultaneous push and pop, the second assignment overwrites the first, so the count decrements when it should hold.
| Cycle | Push | Pop | Should be | Actually |
|---|---|---|---|---|
| n | ✓ | ✓ | hold at 8 | 7 |
| n+1 | ✓ | ✓ | hold at 8 | 6 |
| n+2 | — | ✓ | 7 | 5 |
Four properties.
The drift is monotonic in one direction. Every simultaneous cycle loses one, and the count trends downward — so the queue eventually reports space it does not have and overwrites a live entry.
It is invisible at low load. Simultaneous push and pop requires the producer and the consumer to be active in the same cycle, which happens only when the queue is being kept partly full — i.e. at exactly the throughput the design was built for.
Pointers and occupancy diverge. The pointers are correct because they are independently maintained; only the derived occupancy is wrong, so a check comparing the two catches it and a check on either alone does not.
And the fix has no cost. One unique case with four arms is the same logic, written so that the tie is resolved deliberately. The two-statement form is not simpler; it is merely shorter.
14. SVA — Queue Safety
// MANDATORY. No overflow, no underflow, and the head is stable while stalled.
property p_no_overflow;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
tx_push_fire |-> !tx_full;
endproperty
a_no_overflow: assert property (p_no_overflow);
property p_no_underflow;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
tx_pop_fire |-> !tx_empty;
endproperty
a_no_underflow: assert property (p_no_underflow);
// The offered head does not change while the consumer is not taking it.
property p_head_stable_under_stall;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(!tx_empty && !tx_pop_fire) |=> $stable(tx_head);
endproperty
a_head_stable_under_stall: assert property (p_head_stable_under_stall);
// Occupancy equals the pointer difference — this is what catches Section 13.
property p_occupancy_matches_pointers;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(tx_occ_q == ptr_diff(tx_wr_q, tx_rd_q));
endproperty
a_occupancy_matches_pointers: assert property (p_occupancy_matches_pointers);
// A simultaneous push and pop leaves occupancy unchanged.
property p_simultaneous_holds;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(tx_push_fire && tx_pop_fire) |=> (tx_occ_q == $past(tx_occ_q));
endproperty
a_simultaneous_holds: assert property (p_simultaneous_holds);Architecture. Five properties, and the fourth is the one worth emphasising.
Why occupancy-against-pointers catches §13 and bounds do not. The drift stays within [0, DEPTH] for a long time before it breaches anything. The pointer comparison diverges on the very first simultaneous cycle, which is the difference between finding the bug in the first minute of a soak and finding it in the field.
DV. All five always-on. The fifth requires the simultaneous cycle to occur, which is a coverage bin (§53) and not a certainty.
15. The Replay Block at Top Level
14.3 teaches replay in full — three pointers, the retirement point, duplicate suppression, the retry FSM. What the top level adds is where it sits and what it owns:
| Question | Answer at top level |
|---|---|
| When does replay take ownership? | when the Adapter accepts the object for transmission (§17) |
| When does it release? | on acknowledgement (14.3 §14) — never on PHY send |
| What backpressures the TX queue? | replay_space_available, at admission (§12) |
| What survives a recovery? | the entries are re-baselined with the peer, and the attempt counts survive as evidence (§7) |
| What must never happen? | a cycle in which neither the queue nor replay owns the object (§18) |
16. The Dual-Ownership Window
Between the TX queue and the replay block there is a deliberate window in which both own the object. It opens when replay commits the entry and closes when the queue releases it — and the window exists because the handoff takes at least one cycle.
// ILLUSTRATIVE. The queue releases only after replay CONFIRMS it holds the
// object. Both own it in between, and that is correct.
logic replay_commit; // replay has written the entry and it is durable
logic tx_release; // the queue may now free its slot
assign replay_commit = replay_alloc_fire && replay_entry_written;
assign tx_release = replay_commit; // NOT phy_tx_fire
assign tx_pop_fire = tx_release;Architecture. The release is gated on the replay block's confirmation, not on the Adapter accepting or on the PHY sending. That is the ordering constraint, and it is the only reason the window is deliberate rather than accidental.
Cycle behaviour.
| Cycle | TX queue holds it | Replay holds it | Safe? |
|---|---|---|---|
| n | ✓ | — | ✓ — the queue can re-offer |
| n+1 | ✓ | ✓ | ✓ — the deliberate window |
| n+2 | — | ✓ | ✓ — replay can retransmit |
| (the bug) | — | — | ✗ — §18 |
Contract. The queue 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 §19 asserts the conjunction.
Failure. §18.
DV. §19's ownership property, and coverage of the window occurring at all.
17. Wrong Handoff — the Queue Pops Before Replay Commits
// WRONG — the TX queue frees its entry when the Adapter accepts, and replay
// allocates a cycle later.
assign tx_pop_fire = adapter_accept_fire; // ← one cycle too early
// ... replay_alloc_fire occurs on the following cycleCycle n : the queue pops. The queue no longer holds the object.
Cycle n : replay has NOT yet allocated. Replay does not hold it either.
Cycle n : an error occurs, or a recovery is triggered.
-> NO STRUCTURE HOLDS THE OBJECT.
-> it cannot be retransmitted, and it cannot be re-offered.
-> the object is lost, silently.Four properties.
The window is one cycle wide, so it needs an error in exactly that cycle. The probability is low and the consequence is unbounded — a lost object with no record.
Nothing detects it. The queue's accounting is correct; replay's accounting is correct; the object simply is not in either. No overflow, no underflow, no CRC failure, no assertion of the ordinary kind.
It is a classic timing-window bug and it will pass every directed test. Only an ownership property — checking that every live object is held by at least one structure at every cycle — catches it (§19).
And the fix is one signal. Gate the pop on replay's confirmation rather than on the Adapter's acceptance. The cost is one cycle of extra queue occupancy, which is what the elaboration check in §6 sizes for.
18. SVA — Every Object Is Always Owned
// MANDATORY. The property that catches Section 17. Uses a verification
// reference model, because "which object is this" is testbench knowledge.
//
// For each object the testbench is tracking, at least one structure holds it.
property p_object_always_owned(int unsigned rid);
@(posedge adapter_clk) disable iff (!adapter_rst_n)
tb_object_live(rid) |-> (tb_in_tx_queue(rid) || tb_in_replay(rid));
endproperty
a_object_always_owned: assert property (p_object_always_owned(REF_UT));
// The queue releases only after replay confirms.
property p_release_after_replay_commit;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
tx_pop_fire |-> replay_commit;
endproperty
a_release_after_replay_commit:
assert property (p_release_after_replay_commit);
// Replay never allocates without space — the admission term was real.
property p_replay_alloc_has_space;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
replay_alloc_fire |-> !replay_full;
endproperty
a_replay_alloc_has_space: assert property (p_replay_alloc_has_space);
// The replay entry is not released at PHY send (14.3 Section 15, at top level).
property p_replay_not_freed_on_phy_send;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(phy_tx_fire && !replay_ack_fire) |=> $stable(replay_occupancy);
endproperty
a_replay_not_freed_on_phy_send:
assert property (p_replay_not_freed_on_phy_send);Architecture. Four properties: universal ownership, the handoff order, replay admission, and the retirement point.
Why the first must use a verification reference. The design has no signal saying "object 47 exists". The testbench generated it and therefore knows — and building that knowledge into the design would create a third tracker with its own bugs.
Why the fourth restates a Module 14 property here. It is the same rule, and at top level it is a connectivity claim: that phy_tx_fire is not wired to the replay release. That is exactly the kind of mistake integration introduces and a block-level testbench cannot see.
DV. The first needs an error injected in the one-cycle window, which is §53's cp_error_in_handoff bin.
19. The Credit Manager
// ILLUSTRATIVE. Per class, one owner, simultaneous consume-and-return explicit.
// No normative class count is claimed (Section 3).
logic [CREDIT_W-1:0] tx_credit_q [NUM_CLASSES];
always_ff @(posedge adapter_clk or negedge adapter_rst_n)
if (!adapter_rst_n) begin
for (int c = 0; c < NUM_CLASSES; c++) tx_credit_q[c] <= CREDIT_INIT[CREDIT_W-1:0];
end else begin
for (int c = 0; c < NUM_CLASSES; c++)
unique case ({consume_fire && (consume_class == c[$clog2(NUM_CLASSES)-1:0]),
return_fire && (return_class == c[$clog2(NUM_CLASSES)-1:0])})
2'b10: tx_credit_q[c] <= tx_credit_q[c] - 1'b1;
2'b01: tx_credit_q[c] <= tx_credit_q[c] + 1'b1;
default: ; // both or neither: hold
endcase
end
assign credit_available[c] = (tx_credit_q[c] != '0);Architecture. One counter per class with one owner. Consumption is on an actual transfer, not on a grant or an admission — an object admitted and then held has not consumed a downstream slot.
State. NUM_CLASSES counters, initialised to what the peer advertised. CREDIT_INIT must equal the peer's actual capacity; advertising more overflows it and advertising fewer wastes it, and only conservation (§20) detects either.
Cycle behaviour. Returns arrive from the peer and may be delayed arbitrarily — including across a clock boundary, which is §47's hazard.
Contract. tx_ready depends on this (§12). The entire flow-control guarantee rests on the counter being exactly right.
Failure. Two independent statements (§13's shape, at a different counter). Or consuming on tx_ready && tx_valid at the protocol boundary rather than on the actual transfer to the peer — which consumes a credit for an object still sitting in the TX queue.
DV. §20.
20. SVA — Credit Conservation
// MANDATORY. Conservation, not just bounds — bounds miss a slow leak (18.4 §21).
property p_credit_conserved(int c);
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(tx_credit_q[c] + tb_outstanding_at_peer(c) == CREDIT_INIT);
endproperty
a_credit_conserved: assert property (p_credit_conserved(CLS_UT));
// Two-sided bounds — the upper one catches a mis-indexed return.
property p_credit_bounded(int c);
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(tx_credit_q[c] <= CREDIT_INIT);
endproperty
a_credit_bounded: assert property (p_credit_bounded(CLS_UT));
// No transfer without a credit.
property p_no_transfer_without_credit;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
consume_fire |-> ($past(tx_credit_q[consume_class]) != '0);
endproperty
a_no_transfer_without_credit: assert property (p_no_transfer_without_credit);Architecture. Conservation, two-sided bounds, and the flow-control guarantee.
Why conservation rather than bounds alone. A lost credit return (18.4 §20) drifts the count downward inside its legal range, and the link stops permanently after a long run with no bound ever breached. Conservation fires on the first lost return.
DV. Run long enough to drain a class; inject a dropped credit return across the CDC (§47).
21. The Receive Path, Walked
1. The PHY delivers received data across RDI.
2. rx_ingress aligns and reconstructs the object.
3. Integrity is checked. (§3 — where applicable)
4. The duplicate history is consulted. ([14.1 §26](/protocols/ucie/ucie-error-detection))
5. THE DELIVERY GATE evaluates. (§23)
6. Only then is the object placed in the RX queue. -> semantic delivery
7. The consumer takes it. -> the entry is releasedThe critical ordering is that step 5 precedes step 6, and §22 is the design where it does not.
22. Wrong RX Architecture — Deliver Before the Verdict
// WRONG — the object is pushed into the RX queue as it is reconstructed, and
// the checks run afterwards.
always_ff @(posedge adapter_clk)
if (rx_object_complete) begin
rx_push(rx_reconstructed); // ← delivered
crc_check_start(rx_reconstructed); // ← checked
endTwo independent failures.
A corrupted object is delivered and then found to be corrupt. The consumer may already have acted on it. 14.1 §20's rule — check, then act, never the reverse — violated at the top level.
And a retransmitted object is delivered twice. A retry re-sends the same object; without the history check before the push, the consumer sees two semantic deliveries of one object — which for anything with an effect is a duplicate action (14.3 §22).
| The object is | Correct outcome | With this design |
|---|---|---|
| valid and new | delivered once | delivered once |
| corrupt | discarded; replay requested | delivered, then flagged |
| a duplicate | suppressed; the reply re-sent | delivered twice |
Three of the four combinations of validity and novelty are handled wrongly, and the transport is behaving exactly as specified in each case.
23. The Delivery Gate
// ILLUSTRATIVE. A conjunction of named terms between reconstruction and the
// RX queue. Combinational into the push — Section 22 is the registered version.
assign rx_semantic_deliver =
rx_object_complete // the whole object arrived
&& rx_integrity_ok // the checks passed (Section 3)
&& !rx_duplicate // history says this is not a repeat
&& rx_epoch_current // not from a dead link epoch (Section 42)
&& rx_q_space; // and there is somewhere to put it
assign rx_push_fire = rx_semantic_deliver;Architecture. Five named terms. Naming them separately is the diagnostic value: a non-delivery is immediately attributable to one of five causes rather than being "the RX path dropped it".
Cycle behaviour. Combinational into the push. A registered gate delivers one object before deciding not to (14.1 §20) — the object is in the queue and the consumer may already have taken it.
Contract. Everything above the RX queue assumes exactly-once semantic delivery. That assumption has no signal at the interface, which is why §24 asserts it.
Failure. §22. Also omitting rx_epoch_current, which admits an object from before a recovery into a queue drained after it.
DV. Force each term false alone and confirm no push — five directed tests, which catch the "counted but not gated" case no random regression finds.
24. SVA — Semantic Delivery At Most Once
// MANDATORY. The property the entire receive architecture exists for.
int unsigned tb_deliveries [int]; // reference object id -> semantic deliveries
always @(posedge adapter_clk) if (rx_push_fire)
tb_deliveries[tb_ref_id_of(rx_object)]++;
property p_delivery_at_most_once(int unsigned rid);
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(tb_deliveries[rid] <= 1);
endproperty
a_delivery_at_most_once: assert property (p_delivery_at_most_once(REF_UT));
// A duplicate attempt pushes nothing.
property p_duplicate_pushes_nothing;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(rx_object_complete && rx_duplicate) |-> !rx_push_fire;
endproperty
a_duplicate_pushes_nothing: assert property (p_duplicate_pushes_nothing);
// A failed integrity check pushes nothing.
property p_corrupt_pushes_nothing;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(rx_object_complete && !rx_integrity_ok) |-> !rx_push_fire;
endproperty
a_corrupt_pushes_nothing: assert property (p_corrupt_pushes_nothing);
// And the reply is still sent for a recognised duplicate (14.3 Section 22).
property p_duplicate_still_acknowledged;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(rx_object_complete && rx_duplicate) |-> ##[1:ACK_BOUND] rx_ack_sent;
endproperty
a_duplicate_still_acknowledged:
assert property (p_duplicate_still_acknowledged);Architecture. Four properties: exactly-once, and the three specific gate conditions.
Why the fourth is not optional. Suppressing the effect and also suppressing the reply leaves the peer retransmitting forever (14.3 §22). A design that gets duplicate suppression right and acknowledgement wrong has replaced a corruption with a livelock.
DV. The first needs a retry that actually occurs while traffic is live (§53).
25. Metadata Travels With Its Payload
// ILLUSTRATIVE. One packed bundle through every pipeline stage — the fourth
// appearance of this rule in the curriculum (16.4 §20, 17.4 §35, 18.2 §41).
typedef struct packed {
logic [PROTO_W-1:0] payload;
logic [$clog2(NUM_CLASSES)-1:0] tclass;
logic [SEM_ID_W-1:0] sem_id;
logic [CFG_EPOCH_W-1:0] cfg_epoch;
logic valid;
logic error;
} link_bundle_t;
link_bundle_t stage_q [PIPE_DEPTH];
always_ff @(posedge adapter_clk)
if (stage_advance)
for (int s = PIPE_DEPTH-1; s > 0; s--)
stage_q[s] <= stage_q[s-1];Architecture. One packed object shifted as a unit. valid and error are inside the bundle — not beside it — because a validity or error flag that advances at a different rate attaches to the wrong payload.
State. PIPE_DEPTH copies. More area than a narrow tag beside a wide bus, and it buys a structural guarantee.
Contract. Every stage associates a payload with a class, an identity and a configuration epoch. If the association can be wrong, every downstream decision is about the wrong object — with the payload perfectly intact.
Failure. §26.
DV. §27, plus an end-to-end check comparing metadata recovered at the far end against what was assigned at acceptance.
26. Wrong RTL — Separate Valid and Payload Pipelines
// WRONG — the valid/metadata path is one stage shorter than the payload path.
always_ff @(posedge adapter_clk) begin
meta_q1 <= meta_in; meta_q2 <= meta_q1; // 2 stages
data_q1 <= data_in; data_q2 <= data_q1; data_q3 <= data_q2; // 3 stages
end
assign out_meta = meta_q2;
assign out_data = data_q3;Every object after the first carries the previous object's metadata.
| Cycle | Payload | Metadata applied | Result |
|---|---|---|---|
| n | D0 | M0 | ✓ by luck — the first |
| n+1 | D1 | M0 | ✗ wrong class, wrong identity, wrong epoch |
| n+2 | D2 | M1 | ✗ |
Four properties.
The payload is perfect and the transport is perfect. Every bit crossed correctly. It is being interpreted as belonging to a different object.
A valid bit in the shorter path is the worst case, because an object can be marked valid when it is not, or invalid when it is — which either delivers garbage or silently drops a good object.
It is systematic and off by exactly one, so every waveform cycle looks reasonable and the pattern is invisible unless you look at two consecutive objects.
And it needs at least two back-to-back objects with different metadata to detect — a trivial test that a single-object smoke test does not contain (§27, §53).
27. SVA — the Bundle Is Stable and Aligned
// MANDATORY. An offered bundle does not change while it is waiting.
property p_bundle_stable_under_stall;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(out_valid && !out_ready) |=> (out_valid && $stable(out_bundle));
endproperty
a_bundle_stable_under_stall: assert property (p_bundle_stable_under_stall);
// Metadata and payload advance together.
property p_meta_payload_together;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
$changed(out_bundle.payload) |-> $changed(out_bundle.sem_id) || payload_repeats;
endproperty
// The identity recovered at the far end equals the one assigned at acceptance.
property p_identity_survives_the_link(int unsigned rid);
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(rx_push_fire && (tb_ref_id == rid)) |-> (rx_bundle.sem_id == tb_sem_id_of(rid));
endproperty
a_identity_survives_the_link:
assert property (p_identity_survives_the_link(REF_UT));Architecture. Stability, joint movement, and the end-to-end identity check.
Why the third uses a testbench reference. The wire carries an identity field. The knowledge that "this payload was assigned identity X at acceptance" belongs to whatever generated the traffic — synthesising it would build a second copy of the same logic with the same bug.
DV. §26's off-by-one is caught on the second object of any stream.
28. Three State Machines, Not One
| Machine | Owns | Its state lives for | Its inputs |
|---|---|---|---|
| Link manager | operational and recovery policy | the link's operational life | errors, training results, requests |
| Training controller | the physical training sequence | one training attempt | PHY status, timers |
| Lane manager | lane map, repair, width/rate configuration | a configuration epoch | training results, error locations |
Three lifetimes, three machines. The link manager decides whether to train; the training controller decides how; the lane manager decides with what. §31 shows what a single merged machine gets wrong, and it is not merely inelegant.
29. The Link-Management FSM
Read the two admitting states. ACTIVE and DEGRADED both admit traffic — because a degraded link works, more slowly (14.4). A design with no DEGRADED state must classify a reduced-capability link as either fully healthy (and over-schedule it) or failed (and lose it).
30. The Link-Manager RTL
// ILLUSTRATIVE. One next-state owner; outputs are state-derived or registered,
// never combinational from a raw error input (Section 31).
typedef enum logic [2:0] {
LNK_RESET = 3'd0,
LNK_INIT = 3'd1,
LNK_TRAIN = 3'd2,
LNK_ACTIVE = 3'd3,
LNK_RECOVERY = 3'd4,
LNK_DEGRADED = 3'd5,
LNK_FAILED = 3'd6
} link_state_e;
link_state_e link_state_q, link_state_d;
// Inputs are QUALIFIED and SYNCHRONISED before they reach the FSM (Section 31).
logic reset_done_s, train_done_s, train_fail_s;
logic recoverable_err_s, fatal_err_s, recovery_done_s, degrade_required_s;
logic [ATT_W-1:0] retrain_attempts_q;
always_comb begin
link_state_d = link_state_q;
unique case (link_state_q)
LNK_RESET: if (reset_done_s) link_state_d = LNK_INIT;
LNK_INIT: if (fatal_err_s) link_state_d = LNK_FAILED;
else link_state_d = LNK_TRAIN;
LNK_TRAIN: if (fatal_err_s || train_fail_s) link_state_d = LNK_FAILED;
else if (train_done_s) link_state_d = LNK_ACTIVE;
LNK_ACTIVE: if (fatal_err_s) link_state_d = LNK_FAILED;
else if (recoverable_err_s) link_state_d = LNK_RECOVERY;
LNK_RECOVERY: if (fatal_err_s) link_state_d = LNK_FAILED;
else if (retrain_attempts_q >= ATTEMPT_BUDGET)
link_state_d = LNK_FAILED;
else if (recovery_done_s && degrade_required_s)
link_state_d = LNK_DEGRADED;
else if (recovery_done_s) link_state_d = LNK_ACTIVE;
LNK_DEGRADED: if (fatal_err_s) link_state_d = LNK_FAILED;
else if (recoverable_err_s) link_state_d = LNK_RECOVERY;
else if (full_capability_restored_s) link_state_d = LNK_ACTIVE;
LNK_FAILED: if (soft_reset) link_state_d = LNK_RESET;
default: link_state_d = LNK_FAILED;
endcase
end
always_ff @(posedge adapter_clk or negedge adapter_rst_n)
if (!adapter_rst_n) link_state_q <= LNK_RESET;
else link_state_q <= link_state_d;
// Outputs are derived from the STATE, not from the raw events (Section 32).
assign link_admits_new_traffic = (link_state_q == LNK_ACTIVE)
|| (link_state_q == LNK_DEGRADED);
assign start_training = (link_state_q == LNK_TRAIN);
assign enter_recovery = (link_state_q == LNK_RECOVERY);
assign notify_fault = (link_state_q == LNK_FAILED);
assign link_active = (link_state_q == LNK_ACTIVE);
assign link_degraded = (link_state_q == LNK_DEGRADED);Architecture. Seven states, one owner, one unique case. fatal_err_s is checked first in every state, so a fatal error cannot be overtaken by a completion in the same cycle — an explicit priority rather than an emergent one.
State. Three bits plus an attempt counter. The attempt budget is what makes FAILED reachable rather than looping forever (14.3 §37).
Cycle behaviour. All inputs are synchronised and qualified before they reach the FSM (§31). Outputs are pure functions of the current state.
Contract. ucie_tx_ingress reads link_admits_new_traffic; the training controller reads start_training; the fault manager reads notify_fault. Three consumers, three state-derived outputs, no overlap.
Failure. §31. Also making LNK_FAILED reachable only through the attempt budget and not through fatal_err_s, which leaves a fatal error retrying repeatedly.
DV. Cover every state and every legal transition; assert no illegal transition (12.4 §10).
31. Wrong FSM — Combinational Outputs From Raw Events
// WRONG — outputs derived combinationally from raw, unsynchronised inputs.
assign enter_recovery = raw_error_from_phy; // async, glitchy
assign link_admits_new_traffic = !raw_error_from_phy; // ← the same signalThree failures, and the third is the one that corrupts.
A glitch on raw_error_from_phy produces a one-cycle recovery entry and exit. Nothing completes; nothing is captured; the link twitches and continues, and the event is invisible afterwards.
A signal from another clock domain may be metastable (§45). Fanning it out combinationally to several consumers means they may resolve it differently in the same cycle — one block believing recovery is active and another believing it is not.
And the same raw signal drives admission. So during the glitch, admission is disabled for one cycle in one block and not in another, which can admit an object into a path that is simultaneously being told to quiesce. That is how an object is accepted and then lost.
The correct discipline is three rules:
// ILLUSTRATIVE. Synchronise, qualify, then derive from state.
logic raw_err_meta_q, raw_err_sync_q;
always_ff @(posedge adapter_clk or negedge adapter_rst_n)
if (!adapter_rst_n) begin
raw_err_meta_q <= 1'b0;
raw_err_sync_q <= 1'b0;
end else begin
raw_err_meta_q <= raw_error_from_phy; // 1: synchronise
raw_err_sync_q <= raw_err_meta_q;
end
assign recoverable_err_s = raw_err_sync_q && link_error_qualified; // 2: qualify
// 3: outputs come from link_state_q, never from recoverable_err_s (Section 30).32. SVA — FSM Discipline
// MANDATORY. Only legal transitions occur.
property p_legal_transitions;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
$changed(link_state_q) |-> is_legal_transition($past(link_state_q), link_state_q);
endproperty
a_legal_transitions: assert property (p_legal_transitions);
// New traffic is not admitted outside the admitting states.
property p_no_admission_outside_active;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
tx_push_fire |-> ((link_state_q == LNK_ACTIVE) || (link_state_q == LNK_DEGRADED));
endproperty
a_no_admission_outside_active:
assert property (p_no_admission_outside_active);
// No admission during recovery — the specific case that matters (Section 37).
property p_no_admission_during_recovery;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(link_state_q == LNK_RECOVERY) |-> !tx_push_fire;
endproperty
a_no_admission_during_recovery:
assert property (p_no_admission_during_recovery);
// LIVENESS: a recoverable link eventually reaches an admitting state.
// A1: training completes or fails within a bound
// A2: the error source eventually stops asserting
// A3: the attempt budget is finite
property p_recovery_terminates;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(link_state_q == LNK_RECOVERY)
|-> ##[1:RECOVERY_BOUND] ((link_state_q == LNK_ACTIVE)
|| (link_state_q == LNK_DEGRADED)
|| (link_state_q == LNK_FAILED));
endproperty
a_recovery_terminates: assert property (p_recovery_terminates);Architecture. Three safety properties and one bounded liveness property.
Why the liveness consequent admits FAILED. The requirement is termination, not success (14.2 §30). A recovery that gives up explicitly is a correct outcome; one that loops forever is not, and only a property that accepts FAILED distinguishes them.
DV. Cover every state; inject a fatal error in each state; exhaust the attempt budget.
33. Recovery Entry, In Order
The ordering is the architecture, and §34 is what happens when it is not followed.
1. STOP new admission. -> link_admits_new_traffic deasserts (§30)
2. PRESERVE semantic and replay obligations. -> nothing is cleared (§7, §10)
3. CAPTURE the first fault. -> BEFORE anything can overwrite it (§35)
4. QUIESCE as the architecture requires.
5. RETRAIN / RECONFIGURE. -> the training controller runs
6. VALIDATE the result.
7. COMMIT the configuration and RESUME. -> atomically (Section 36)Two orderings carry the whole sequence.
Step 3 precedes step 5. Retraining rewrites PHY status, lane state and error registers. A capture after retraining captures the retraining, not the fault (§34).
And step 6 precedes step 7. The configuration is validated before it is committed, so a bad configuration is refused rather than discovered by the first object that uses it (14.2's recovery controller makes this precise).
34. Wrong Recovery Order — Reset Before Capture
// WRONG — the retrain is started as soon as recovery is entered, and the fault
// is captured afterwards "when things settle".
always_ff @(posedge adapter_clk)
if (link_state_q == LNK_RECOVERY) begin
phy_retrain <= 1'b1; // ← step 5 before step 3
if (retrain_done) capture_fault(); // ← by now the syndrome is gone
end| What existed at the moment of the fault | After the retrain |
|---|---|
| the failing lane's error status | cleared by training |
| the receive alignment state | re-established |
| the error type and syndrome | overwritten |
| the configuration in force | replaced |
| a timestamp or sequence position | usually still available |
Three properties.
Post-silicon sees a timeout and a successful retrain, and nothing else. The link recovered; a transaction failed; and there is no information connecting the two.
The same failure then recurs indefinitely, because each occurrence destroys its own evidence. A design can spend months in this state.
And the fix costs a few cycles. Capturing a first-fault record before asserting phy_retrain delays the recovery by the capture latency — illustratively a handful of cycles against a retrain measured in thousands. The trade is not close.
35. The First-Fault Record
// ILLUSTRATIVE. 14.5's model, at top level. The point here is its RESET DOMAIN.
typedef struct packed {
logic valid;
logic [ERR_W-1:0] error_type;
logic [LANE_W-1:0] lane;
logic [CFG_EPOCH_W-1:0] cfg_epoch; // which configuration was in force
logic [SEQ_W-1:0] seq_at_fault; // where in the stream
logic [TS_W-1:0] timestamp;
} first_fault_t;
first_fault_t first_fault_q;
// FIRST, not latest: the record is written once and held until deliberately cleared.
always_ff @(posedge adapter_clk or negedge por_n) // <- POR only
if (!por_n)
first_fault_q <= '0;
else if (fault_detected && !first_fault_q.valid) // <- guarded: FIRST
first_fault_q <= capture_fault_context();
else if (fault_record_clear) // <- deliberate only
first_fault_q <= '0;Architecture. A write-once record with a deliberate clear. The guard !first_fault_q.valid is what makes it the first fault rather than the latest (14.5) — and the latest is almost always a consequence rather than a cause.
State. One record. Its reset is por_n, not adapter_rst_n — which is the entire architectural point of this section, and it is one line in a sensitivity list.
Cycle behaviour. Captured combinationally from the fault context at the moment of detection, before any retrain can begin (§33).
Contract. Every post-silicon investigation depends on it. Its value is entirely a function of surviving the event that produced it, which is why §10's third property exists.
Failure. Using adapter_rst_n in the sensitivity list, which is §9. Or omitting the !valid guard, which makes it a latest-fault register whose contents are the last symptom rather than the first cause.
DV. §10's third property; inject several faults and confirm the first is retained.
36. Requested and Active Configuration
// ILLUSTRATIVE. 14.4's requested-versus-active discipline, as a top-level block.
typedef struct packed {
logic [RATE_W-1:0] rate;
logic [WIDTH_W-1:0] width;
logic [NUM_LANES-1:0] lane_enable;
logic [MODE_W-1:0] mode;
} link_cfg_t;
link_cfg_t requested_cfg_q; // software and training write here
link_cfg_t active_cfg_q; // the datapath reads here
logic [CFG_EPOCH_W-1:0] active_cfg_epoch_q;
assign cfg_commit_allowed =
cfg_validated // the staged configuration is legal
&& (tx_q_occupancy == '0) // the pipeline has drained
&& (replay_outstanding == '0) // nothing awaits retransmission
&& (link_state_q == LNK_RECOVERY); // only at a defined safe point
always_ff @(posedge adapter_clk or negedge adapter_rst_n)
if (!adapter_rst_n) begin
active_cfg_epoch_q <= '0;
end else if (cfg_commit_fire) begin
active_cfg_q <= requested_cfg_q; // atomic, whole structure
active_cfg_epoch_q <= active_cfg_epoch_q + 1'b1;
end
assign cfg_stable = !cfg_commit_pending;Architecture. Two copies committed atomically, with a four-term commit guard. The third and fourth terms are the ones a block-level design forgets: nothing may await retransmission, because a replayed object framed under the old configuration would be transmitted under the new one; and the commit happens only at a defined safe point.
State. Two configuration structures plus an epoch.
Cycle behaviour. The whole structure transfers in one cycle. Not field by field — a partial commit is a window in which the rate and the width disagree, and an object transmitted in it is framed inconsistently.
Contract. The datapath, the lane manager and the PHY interface all read active_cfg_q. They must see one complete configuration or the previous one, never a mixture.
Failure. §37.
DV. §38's properties; cover a commit attempted with a non-empty TX queue and with outstanding replay entries.
37. Wrong RTL — Live Configuration Change
// WRONG — configuration fields are written directly, while traffic is live.
always_ff @(posedge adapter_clk)
if (cfg_write) begin
active_cfg_q.rate <= cfg_wdata[RATE_FIELD];
active_cfg_q.width <= cfg_wdata[WIDTH_FIELD]; // ← different cycle, live traffic
end1. An object is in the TX pipeline, framed under width = 16.
2. Software writes a new width of 8. active_cfg_q.width changes.
3. The object's remaining stages are processed under width = 8.
4. -> the object is framed inconsistently: its early stages assumed one
configuration and its later stages another.
5. The far end reconstructs something that is not what was sent, and the
CRC — computed over what WAS sent — fails, or worse, passes over a
coincidentally-consistent result.Four properties.
Different pipeline stages interpret the same object under different configurations, which is a class of corruption that no single stage can detect — each stage behaved correctly under the configuration it saw.
And a replayed object is framed under the new configuration while the peer expects the old — which is why the commit guard includes the replay-outstanding term (§36).
The window is small and the traffic is high, so the probability per write is low and the number of writes over a product's life is not.
The correct form is requested-then-committed (§36), and the commit guard is not optional: committing with a drained pipeline but outstanding replay entries produces exactly the same failure on the first retransmission.
38. SVA — Configuration Lifetime
// MANDATORY. The active configuration changes only at a guarded commit.
property p_cfg_changes_only_at_commit;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
$changed(active_cfg_q) |-> $past(cfg_commit_fire);
endproperty
a_cfg_changes_only_at_commit: assert property (p_cfg_changes_only_at_commit);
// The commit requires a drained pipeline and no outstanding replay.
property p_commit_requires_quiesce;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
cfg_commit_fire |-> ((tx_q_occupancy == '0) && (replay_outstanding == '0));
endproperty
a_commit_requires_quiesce: assert property (p_commit_requires_quiesce);
// The epoch advances with the configuration — they can never disagree.
property p_epoch_tracks_cfg;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
cfg_commit_fire |=> (active_cfg_epoch_q == $past(active_cfg_epoch_q) + 1);
endproperty
a_epoch_tracks_cfg: assert property (p_epoch_tracks_cfg);
// An object's configuration epoch is stable for its whole life in the link.
property p_object_cfg_epoch_stable;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
tb_object_live(REF_UT) |-> $stable(tb_object_cfg_epoch(REF_UT));
endproperty
a_object_cfg_epoch_stable: assert property (p_object_cfg_epoch_stable);Architecture. Four properties: commit-only changes, the guard, epoch coupling, and per-object stability.
Why the fourth exists given the first three. They protect the configuration; the fourth protects the object — it says no live object ever spans two epochs, which is §37's failure stated from the object's point of view rather than the register's.
DV. Force a commit with a non-empty queue and confirm the second fires.
39. Clock Domains Inside One Link
| Boundary | Typical reason | What crosses |
|---|---|---|
| protocol ↔ Adapter | the protocol client's clock differs | payload streams, backpressure |
| Adapter ↔ PHY | the PHY's clock is derived from the link rate | payload streams, status, credit returns |
| What crosses | Correct bridge | Wrong bridge |
|---|---|---|
| a payload stream | an asynchronous FIFO (18.4 §18) | a plain register |
| a level or state | a two-flop synchroniser | a plain register |
| a one-cycle pulse | a toggle or a handshake | a synchroniser — the pulse can be missed |
| a multi-bit value | Gray-coded, or a handshake | per-bit synchronisers |
| a credit return | a Gray-coded count (§40) | a pulse — §40 |
| reset | asynchronous assert, synchronous deassert | a plain register |
40. Wrong CDC — a Credit Return as a Pulse
// WRONG — a one-cycle credit-return pulse crossed with a synchroniser.
always_ff @(posedge adapter_clk)
cr_sync_q <= {cr_sync_q[0], phy_credit_return_pulse};
assign credit_return_fire = cr_sync_q[1];If the Adapter clock is slower than the PHY clock, a pulse can fall entirely between two Adapter edges and is never sampled.
1. Light traffic: returns are sparse, nearly all are sampled. Works perfectly.
2. Sustained traffic: returns are frequent, a fraction fall between edges
and are LOST.
3. Every lost pulse is a credit that will never be returned.
4. tx_credit_q decreases monotonically.
5. -> after a long run it reaches zero and the link stops PERMANENTLY.
No error, no assertion, no CRC failure. It simply stops.Four properties, and this is the best bug in the chapter.
It is a leak, not a corruption. Nothing is ever wrong; there is progressively less capacity. The symptom is "the link worked for six hours and then stopped".
It is load-dependent in the direction that hides it — light traffic loses few pulses, so it survives every short test.
And §20's conservation assertion catches it on the very first lost pulse, while every bounds check stays silent to the end. That single property is the difference between finding it in simulation and finding it in a customer's rack.
The fix is to cross a state, not an event:
// ILLUSTRATIVE. A monotonic Gray-coded COUNT. A missed sample is recovered by
// the next one, because the destination takes the DIFFERENCE.
logic [CNT_W-1:0] cr_count_q; // PHY domain, monotonic
logic [CNT_W-1:0] cr_gray_q;
logic [CNT_W-1:0] cr_gray_sync_q [2]; // Adapter domain
logic [CNT_W-1:0] cr_seen_q;
always_ff @(posedge phy_clk)
if (phy_credit_return) begin
cr_count_q <= cr_count_q + 1'b1;
cr_gray_q <= bin2gray(cr_count_q + 1'b1);
end
wire [CNT_W-1:0] cr_bin = gray2bin(cr_gray_sync_q[1]);
wire [CNT_W-1:0] cr_delta = cr_bin - cr_seen_q; // how many to add41. TX and RX Are Independent
Full duplex means the two directions progress independently — and a design that couples them halves its throughput or deadlocks.
// ILLUSTRATIVE. Separate readiness for each direction, with no shared term.
assign tx_ready = !tx_full && credit_available[tx_class] && replay_space_available
&& link_admits_new_traffic && cfg_stable;
assign rx_ready_to_phy = rx_q_space; // depends ONLY on the receive pathContract. The peer relies on the receive path accepting regardless of the transmit path's congestion — and vice versa. 12.2 §12 makes the general argument; at top level it is a wiring question.
42. Wrong RTL — a Shared Ready
// WRONG — one readiness signal for both directions.
assign link_ready = !tx_full && rx_q_space;
assign tx_ready = link_ready;
assign rx_ready_to_phy = link_ready; // ← RX now depends on TX congestion1. The RX queue fills because the local consumer is slow.
2. link_ready deasserts.
3. TX also stops — even though the transmit path has credits, replay
space, and an operational link.
4. The peer, waiting for our transmissions, cannot make progress.
5. It therefore cannot consume what it has received, so it stops
returning credits or acknowledgements.
6. -> our TX blocks harder, our RX stays full, and the link deadlocks
with both directions idle.Four properties.
At best it halves throughput, because either direction's congestion stops both.
At worst it deadlocks, and the cycle in step 6 is real: the peer's progress depends on our transmissions, and our transmissions depend on our receive path draining, which depends on the peer.
The signal name makes it look correct. link_ready reads as a property of the link. It is actually a conjunction of two unrelated conditions, and nothing about the name suggests that.
And it is a top-level integration bug. Both blocks are correct; the error is one shared wire, which is exactly the class of failure that block-level verification cannot see and this chapter exists to catch.
43. SVA — Direction Independence
// MANDATORY. RX readiness does not depend on TX state.
property p_rx_ready_independent_of_tx;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(rx_q_space && !link_recovery_active) |-> rx_ready_to_phy;
endproperty
a_rx_ready_independent_of_tx:
assert property (p_rx_ready_independent_of_tx);
// TX readiness does not depend on RX occupancy.
property p_tx_ready_independent_of_rx;
@(posedge adapter_clk) disable iff (!adapter_rst_n)
(!tx_full && credit_available[tx_class] && replay_space_available
&& link_admits_new_traffic && cfg_stable) |-> tx_ready;
endproperty
a_tx_ready_independent_of_rx:
assert property (p_tx_ready_independent_of_rx);Architecture. Two properties, one per direction, each written as a complete implication: given the conditions that should enable readiness, readiness is enabled.
Why written this way rather than as a stability check. A stability property would pass on a design that never asserts readiness at all. These say "if the legitimate conditions hold, you must be ready" — which is what forbids an extra term from another direction sneaking in.
DV. Fill the RX queue and confirm tx_ready still asserts; fill the TX queue and confirm rx_ready_to_phy still asserts.
44. Performance Counters and the Stall Classifier
// Diagnostic only, and in the POR reset domain (Section 7).
logic [63:0] tx_payload_bytes_q; // useful bytes transmitted
logic [63:0] tx_replay_bytes_q; // bytes retransmitted (not useful)
logic [63:0] tx_credit_stall_q;
logic [63:0] rx_payload_bytes_q;
logic [63:0] rx_duplicate_q; // suppressed duplicates (Section 23)
logic [63:0] recovery_cycles_q;
logic [63:0] retrain_count_q;
logic [63:0] degraded_cycles_q;
logic [63:0] cfg_commit_count_q;// ILLUSTRATIVE. 15.5 Section 10's causal-priority classifier at TX ingress.
typedef enum logic [3:0] {
TXS_TRANSFER = 4'd0,
TXS_SOURCE_EMPTY= 4'd1, // nothing offered <- must be early
TXS_LINK_DOWN = 4'd2,
TXS_RECOVERY = 4'd3,
TXS_CFG_COMMIT = 4'd4, // a deliberate quiesce
TXS_NO_QUEUE = 4'd5,
TXS_NO_CREDIT = 4'd6,
TXS_REPLAY_FULL = 4'd7,
TXS_PHY_BLOCKED = 4'd8,
TXS_UNATTRIB = 4'd9 // must stay at zero
} tx_stall_e;
tx_stall_e tx_reason_d;
always_comb begin
unique case (1'b1)
tx_push_fire : tx_reason_d = TXS_TRANSFER;
!tx_valid : tx_reason_d = TXS_SOURCE_EMPTY;
(link_state_q == LNK_FAILED) : tx_reason_d = TXS_LINK_DOWN;
(link_state_q == LNK_RECOVERY) : tx_reason_d = TXS_RECOVERY;
!cfg_stable : tx_reason_d = TXS_CFG_COMMIT;
tx_full : tx_reason_d = TXS_NO_QUEUE;
!credit_available[tx_class] : tx_reason_d = TXS_NO_CREDIT;
!replay_space_available : tx_reason_d = TXS_REPLAY_FULL;
!phy_tx_ready : tx_reason_d = TXS_PHY_BLOCKED;
default : tx_reason_d = TXS_UNATTRIB;
endcase
endArchitecture. Ten mutually exclusive reasons in causal priority, one counter each, summing to elapsed cycles.
TXS_SOURCE_EMPTY is second so idle cycles never inflate a resource bin — the most common way a classifier lies (17.2 §32).
And TXS_UNATTRIB must stay at zero. A non-zero value means a stall cause exists that the design does not model — which tells you the instrument is incomplete, and that is the most valuable thing an instrument can tell you.
45. The Conservation Model
Four invariants that tie the layers together, and each is checkable.
SEMANTIC
accepted semantic objects = completed + outstanding + explicitly failed
TRANSPORT
physical attempts >= semantic deliveries (retries add attempts, not deliveries)
semantic deliveries per object <= 1 (Section 24)
BUFFERS
queue occupancy = pushes - pops (Section 14)
CREDITS
initial + returned - consumed = current (Section 20)
OWNERSHIP
every live object is in the TX queue, in replay, or in both (Section 18)Each invariant is owned by one layer and checkable by one scoreboard model (§46). A violation identifies the layer, which is what makes the four-layer structure worth its cost.
46. The Top-Level Scoreboard
// Verification-only. FOUR models, one per layer of Section 45's invariants.
class ucie_link_scoreboard;
// ---- Layer 1: SEMANTIC model — what the client handed over and expects.
typedef struct {
int sem_id;
bit accepted;
int semantic_deliveries; // MUST be <= 1
bit completed;
bit explicitly_failed;
bit crossed_recovery;
} semantic_model_t;
semantic_model_t sem [int];
// ---- Layer 2: ADAPTER / REPLAY model — objects and physical attempts.
typedef struct {
int sem_id;
int attempts; // >= 1
bit retained; // replay holds it
bit in_tx_queue; // the queue holds it
bit acknowledged;
} transport_model_t;
transport_model_t tr [int];
// ---- Layer 3: RESOURCE model — credits and occupancies.
typedef struct {
int credits_available;
int credits_outstanding;
int tx_occupancy, rx_occupancy, replay_occupancy;
} resource_model_t;
resource_model_t res [int]; // per class where applicable
// ---- Layer 4: LINK-MANAGEMENT model — state, config epoch, faults.
typedef struct {
int link_state;
int cfg_epoch;
bit first_fault_valid;
int first_fault_type;
int retrain_count;
} link_mgmt_model_t;
link_mgmt_model_t mgmt;
// ---- Catches Section 17 — an object owned by nothing.
function void check_ownership(int id);
if (!tr[id].in_tx_queue && !tr[id].retained && !tr[id].acknowledged)
$error("OBJECT %0d owned by NOTHING (Section 17)", id);
endfunction
// ---- Catches Section 22 — duplicate or premature semantic delivery.
function void check_delivery(int id);
if (sem[id].semantic_deliveries > 1)
$error("OBJECT %0d delivered %0d times (must be <= 1)", id, sem[id].semantic_deliveries);
if (tr[id].attempts < sem[id].semantic_deliveries)
$error("OBJECT %0d: %0d deliveries from %0d attempts", id,
sem[id].semantic_deliveries, tr[id].attempts);
endfunction
// ---- Catches Sections 9 and 40 — state lost, or credits leaked.
function void check_recovery_survival(int id);
if (sem[id].crossed_recovery && !sem[id].accepted)
$error("SEMANTIC OBJECT %0d lost across a recovery (Section 9)", id);
endfunction
function void check_credit_conservation(int cls);
if (res[cls].credits_available + res[cls].credits_outstanding != CREDIT_INIT)
$error("CREDITS NOT CONSERVED class %0d: %0d + %0d != %0d (Section 40)",
cls, res[cls].credits_available, res[cls].credits_outstanding, CREDIT_INIT);
endfunction
// ---- Catches Section 34 — the fault record destroyed by the retrain.
function void check_fault_preserved();
if (mgmt.retrain_count > 0 && !mgmt.first_fault_valid)
$error("A RETRAIN OCCURRED WITH NO FAULT RECORD (Section 34)");
endfunction
endclassArchitecture. Four models: what the client expects, what physically happened, what resources are held, and what the link's management state is.
Which divergence identifies which subsystem is the payoff:
| Layer 1 diverges | a semantic failure — an obligation lost or completed wrongly | | Layer 2 diverges | an Adapter or replay failure — ownership, attempts, acknowledgement | | Layer 3 diverges | a flow-control or buffering failure — credits, occupancy | | Layer 4 diverges | a link-management failure — state, configuration, fault capture |
And Layer 2's in_tx_queue and retained are tracked separately rather than as one "somewhere in the link" flag — because §17's bug is precisely the cycle in which both are false.
47. Flagship Trace 1 — a Clean Transmission and Response
Illustrative. Cycle numbers illustrative.
| Cyc | TX queue | Credit | Replay | PHY | RX (peer side) | Semantic |
|---|---|---|---|---|---|---|
| 0 | 0 | 8 | 0 | idle | — | — |
| 1 | accepted | 8 | 0 | idle | — | client hands over |
| 2 | 1 | 8 | 0 | idle | — | queue owns it |
| 3 | 1 | 8 | framing | idle | — | — |
| 4 | 1 | 8 | committed | idle | — | dual-ownership window |
| 5 | 0 | 8 | 1 | idle | — | queue releases (§16) |
| 6 | 0 | 7 | 1 | attempt 1 | — | credit consumed on transfer |
| 9 | 0 | 7 | 1 | in flight | — | — |
| 11 | 0 | 7 | 1 | delivered | integrity ok | — |
| 12 | 0 | 7 | 1 | — | gate: not duplicate | — |
| 13 | 0 | 7 | 1 | — | pushed — delivery ×1 | semantic delivery |
| 18 | 0 | 7 | retired | — | ack received | replay releases |
| 20 | 0 | 8 | 0 | — | — | credit returned |
| 30 | 0 | 8 | 0 | — | response returns | — |
| 34 | 0 | 8 | 0 | — | — | semantic completion |
Six readings.
Cycle 4 is the dual-ownership window — the queue and replay both hold it. §17's bug is the queue releasing at cycle 3, before replay committed.
Cycle 6 consumes the credit on the actual transfer, not on acceptance at cycle 1. Consuming at acceptance would credit an object still sitting in the queue.
Cycles 11 to 13 are the delivery gate — integrity, then duplicate, then push. §22's design pushes at cycle 11 and checks afterwards.
Cycle 18 retires the replay entry on the acknowledgement, not at cycle 6 when the PHY sent it (14.3 §14, and §18's fourth property).
Cycle 34 is the semantic completion — twenty-eight cycles after the replay entry was created and sixteen after it retired. Four candidate events, and the last one is correct.
And the object was owned at every cycle from 1 to 18. §46's ownership check holds throughout.
48. Flagship Trace 2 — a Credit Stall
| Cyc | TX head | tx_credit_q | tx_occ_q | tx_reason_d | Head stable? |
|---|---|---|---|---|---|
| 40 | object A | 1 | 3 | TRANSFER | — |
| 41 | object B | 0 | 3 | NO_CREDIT | ✓ |
| 45 | object B | 0 | 3 | NO_CREDIT | ✓ |
| 60 | object B | 0 | 3 | NO_CREDIT | ✓ |
| 61 | object B | 1 — returned | 3 | TRANSFER | — |
| 62 | object C | 0 | 2 | NO_CREDIT | ✓ |
Four readings.
The head is stable for twenty cycles — §14's third property, and the reason the Adapter can sample it whenever it is ready.
The occupancy does not drift. No push, no pop, no simultaneous cycle — and §13's bug would not show here, which is why the simultaneous cycle needs its own coverage bin.
TXS_NO_CREDIT is charged for every stalled cycle, and TXS_PHY_BLOCKED for none. Two counters, two causes (§44): this was flow control, not the PHY.
And nothing about the receive path changed during the stall — §43's independence property, observed.
49. Flagship Trace 3 — a CRC Failure and a Replay
| Cyc | Replay entry | PHY | Peer RX | Semantic deliveries | Attempts |
|---|---|---|---|---|---|
| 6 | held | attempt 1 | — | 0 | 1 |
| 11 | held | delivered | integrity FAIL | 0 | 1 |
| 12 | held | — | discarded — gate blocks | 0 | 1 |
| 14 | held | — | NAK sent | 0 | 1 |
| 20 | held | attempt 2 — same object | — | 0 | 2 |
| 25 | held | delivered | integrity ok | — | 2 |
| 26 | held | — | not a duplicate — pushed | 1 | 2 |
| 32 | retired | — | ack received | 1 | 2 |
Four readings.
Two attempts, one semantic delivery. §45's transport invariant, holding.
The replay entry is held from cycle 6 to cycle 32 — through the failure, the NAK and the retransmission. §18's fourth property is that it is not released at cycle 6 when the PHY sent it.
Cycle 12: the gate discards the corrupt object before it reaches the RX queue. §22's design pushes it and flags it afterwards, by which time the consumer may have taken it.
And cycle 26 pushes because the history says this is not a duplicate — the first attempt was never delivered, so the second is the first delivery. The history must distinguish "transmitted twice" from "delivered twice", which is exactly what 14.1 §26's history provides.
50. Flagship Trace 4 — Recovery With Live Transactions
The strongest trace in the chapter.
| Cyc | Link state | Admit | TX queue | Replay | First fault | Config epoch | Semantic |
|---|---|---|---|---|---|---|---|
| 100 | ACTIVE | ✓ | 4 | 3 | — | 7 | 6 outstanding |
| 104 | ACTIVE | ✓ | 4 | 3 | — | 7 | error detected |
| 105 | RECOVERY | ✗ stops | 4 | 3 | — | 7 | 6 — unchanged |
| 106 | RECOVERY | ✗ | 4 | 3 | CAPTURED | 7 | 6 |
| 107 | RECOVERY | ✗ | 4 | 3 | held | 7 | capture BEFORE retrain (§33) |
| 110 | RECOVERY | ✗ | 4 | 3 | held | 7 | quiesce — nothing cleared |
| 115 | RECOVERY | ✗ | 4 | 3 | held | 7 | retraining |
| 150 | RECOVERY | ✗ | 4 | 3 | held | 7 | trained, validating |
| 158 | RECOVERY | ✗ | 0 — drained | 0 — resolved | held | 7 | commit conditions met |
| 159 | RECOVERY | ✗ | 0 | 0 | held | 8 | config committed atomically |
| 160 | DEGRADED | ✓ resumes | 0 | 0 | held | 8 | x16 → x8 |
| 165 | DEGRADED | ✓ | 2 | 2 | held | 8 | traffic resumes |
| 210 | DEGRADED | ✓ | 0 | 0 | held | 8 | all 6 completed |
Seven readings, and this is what the whole chapter has been building toward.
Cycle 105: admission stops and nothing else changes. Six semantic obligations, four queue entries, three replay entries — all preserved. §9's design clears every one of them here.
Cycle 106: the fault is captured before the retrain begins at 115. §34's design retrains first and captures at 150, by which time the syndrome is gone.
Cycles 110 to 158: the queue and replay drain naturally, because admission stopped and the outstanding work resolved. The commit guard at 158 requires exactly that (§36).
Cycle 159: the configuration commits atomically, and the epoch advances with it. §37's design writes fields live during any of these cycles.
Cycle 160: the link returns as DEGRADED at half width. It admits traffic — a degraded link works (§29) — and every semantic obligation is unchanged.
Cycle 210: all six original obligations completed, on a link that recovered at reduced capability. Slower, not different.
And the first-fault record is still held at cycle 210, available to whoever asks. §9's design cleared it at cycle 105.
51. Flagship Trace 5 — Lane Degradation
| Cyc | active_cfg_q.width | Epoch | Objects in flight | Semantic identity | Throughput |
|---|---|---|---|---|---|
| 300 | 16 | 8 | 5 | unchanged | 1.0× |
| 340 | 16 | 8 | 5 | unchanged | 1.0× — lane fault detected |
| 345 | 16 | 8 | 5 | unchanged | recovery entered, admission stops |
| 380 | 16 | 8 | 0 — drained | unchanged | — |
| 381 | 8 | 9 | 0 | unchanged | committed |
| 385 | 8 | 9 | 3 | unchanged | ~0.5× |
| 450 | 8 | 9 | 0 | unchanged | ~0.5× |
Four readings.
No object spanned the configuration change — the pipeline drained at 380 and the commit was at 381. §38's fourth property, observed.
Not one semantic identity changed, and every obligation completed. The link is slower and not different (16.5 §26).
Throughput halves and degraded_cycles_q accumulates (§44) — so the performance change is reported as a performance change, not as a fault.
And any bound derived from the width must be rescaled at cycle 381 (16.5 §27), or the very next object is declared failed on a link that just recovered successfully.
52. The Assertion Inventory
| # | Property | Section | Class |
|---|---|---|---|
| 1 | no TX queue overflow or underflow | §14 | safety |
| 2 | the head is stable under stall | §14 | safety |
| 3 | occupancy matches the pointer difference | §14 | safety |
| 4 | a simultaneous push and pop holds occupancy | §14 | safety |
| 5 | every live object is owned by the queue, replay, or both | §18 | safety |
| 6 | the queue releases only after replay commits | §18 | safety |
| 7 | replay is not released at PHY send | §18 | safety |
| 8 | credits are conserved and two-sided bounded | §20 | safety |
| 9 | no transfer without a credit | §20 | safety |
| 10 | semantic delivery at most once | §24 | safety |
| 11 | a duplicate pushes nothing but is still acknowledged | §24 | safety + liveness |
| 12 | a corrupt object pushes nothing | §24 | safety |
| 13 | the bundle is stable under stall | §27 | safety |
| 14 | identity survives the link | §27 | safety |
| 15 | only legal FSM transitions occur | §32 | safety |
| 16 | no admission outside the admitting states | §32 | safety |
| 17 | no admission during recovery | §32 | safety |
| 18 | recovery terminates, under assumptions | §32 | liveness |
| 19 | semantic and diagnostic state survive a recovery | §10 | safety |
| 20 | a recovery is not a reset | §10 | safety |
| 21 | the first fault survives everything but power-on | §10 | observability |
| 22 | the active configuration changes only at a guarded commit | §38 | safety |
| 23 | the epoch advances with the configuration | §38 | safety |
| 24 | an object's configuration epoch is stable for its life | §38 | safety |
| 25 | RX readiness is independent of TX state | §43 | liveness |
| 26 | TX readiness is independent of RX occupancy | §43 | liveness |
| 27 | the stall classifier is one-hot and conserves cycles | §44 | observability |
Read the class column. Four are liveness, two are observability, and the rest are safety — and the two observability properties are the ones most often omitted, because they protect the ability to diagnose rather than the ability to function.
53. Coverage
covergroup cg_ucie_link @(posedge adapter_clk);
option.per_instance = 1;
// --- Datapath (Sections 12-14, 19).
cp_tx_occ : coverpoint tx_occ_q {
bins empty = {0}; bins mid = {[1:TX_Q_DEPTH-1]}; bins full = {TX_Q_DEPTH};
}
cp_rx_occ : coverpoint rx_occ_q {
bins empty = {0}; bins mid = {[1:RX_Q_DEPTH-1]}; bins full = {RX_Q_DEPTH};
}
cp_simul_push_pop : coverpoint tx_push_and_pop_same_cycle; // Section 13
cp_credit : coverpoint tx_credit_q_ut {
bins zero = {0}; bins low = {[1:2]}; bins full = {CREDIT_INIT};
}
cp_credit_simul : coverpoint consume_and_return_same_cycle;
cp_replay_occ : coverpoint replay_occupancy {
bins empty = {0}; bins mid = {[1:REPLAY_DEPTH-1]}; bins full = {REPLAY_DEPTH};
}
cp_replay_wrap : coverpoint replay_pointer_wrapped;
// --- Ownership handoff (Sections 16-18).
cp_dual_window : coverpoint dual_ownership_window_active;
cp_error_in_handoff : coverpoint error_during_handoff_window; // Section 17
// --- Receive path (Sections 21-24).
cp_rx_outcome : coverpoint rx_object_outcome {
bins delivered = {0};
bins corrupt = {1};
bins duplicate = {2};
bins stale_epoch = {3};
bins no_space = {4}; // all five gate terms
}
cp_duplicate_acked : coverpoint duplicate_still_acknowledged; // Section 24
// --- Pipeline (Sections 25-27).
cp_back_to_back : coverpoint consecutive_objects_differing_metadata; // Section 26
cp_stall_during_pipe : coverpoint stall_with_objects_in_pipeline;
// --- Link management (Sections 29-32).
cp_link_state : coverpoint link_state_q { bins each[] = {[0:6]}; }
cp_transition : coverpoint link_transition { bins each[] = {[0:10]}; }
cp_attempts : coverpoint retrain_attempts_q {
bins none = {0}; bins some = {[1:ATTEMPT_BUDGET-1]}; bins budget = {ATTEMPT_BUDGET};
}
// --- Recovery and faults (Sections 33-35).
cp_recovery_context : coverpoint recovery_with_state {
bins idle = {0};
bins tx_queue_nonempty = {1};
bins replay_outstanding = {2};
bins both = {3}; // THE case — Section 50
}
cp_fault_order : coverpoint fault_captured_before_retrain; // Section 34
cp_multiple_faults : coverpoint faults_before_clear {
bins one = {1}; bins several = {[2:$]}; // first, not latest
}
// --- Configuration (Sections 36-38).
cp_cfg_context : coverpoint cfg_commit_context {
bins idle = {0}; bins blocked_by_queue = {1};
bins blocked_by_replay = {2}; bins forced = {3};
}
cp_width_change : coverpoint width_after_commit {
bins same = {0}; bins narrower = {1}; // Section 51
}
// --- CDC (Sections 39-40).
cp_clock_ratio : coverpoint adapter_phy_clock_ratio {
bins same = {0}; bins adapter_slower = {1}; bins adapter_faster = {2};
}
cp_credit_pulse_lost : coverpoint credit_return_sample_missed; // Section 40
// --- Direction independence (Sections 41-43).
cp_direction : coverpoint traffic_direction {
bins tx_only = {0}; bins rx_only = {1}; bins full_duplex = {2};
}
cp_rx_full_tx_active : coverpoint rx_full_while_tx_progresses; // Section 42
// --- Attribution (Section 44).
cp_stall_reason : coverpoint tx_reason_d { bins each[] = {[0:9]}; }
// --- Crosses that carry the information.
x_recovery_state : cross cp_recovery_context, cp_link_state; // Section 50
x_clock_credit : cross cp_clock_ratio, cp_credit; // Section 40
x_direction_occ : cross cp_direction, cp_rx_occ; // Section 42
x_cfg_inflight : cross cp_cfg_context, cp_tx_occ; // Section 37
endcovergroupNine bins worth calling out:
cp_recovery_context.both crossed with the link state. §50's trace — a recovery with a non-empty TX queue and outstanding replay entries. Every property in §10 depends on reaching it, and it never occurs spontaneously.
cp_error_in_handoff. §17's one-cycle window, which requires an error injected in exactly that cycle.
cp_rx_outcome — all five. The five gate terms exercised individually (§23), which is the "counted but not gated" test.
cp_back_to_back. §26's off-by-one, detectable only with two consecutive objects carrying different metadata.
cp_clock_ratio.adapter_slower crossed with cp_credit.zero. §40's leak — unreachable at a 1:1 ratio.
cp_rx_full_tx_active. §42 — the RX queue full while TX still progresses, which is the state a shared-ready design cannot produce.
cp_fault_order. §34 — the capture happening before the retrain, proven rather than assumed.
cp_multiple_faults.several. §35's first-versus-latest guard.
And cp_stall_reason.TXS_UNATTRIB must stay at zero, while every other reason should be reachable (§44).
54. Debug Taxonomy
| Signature | Most likely cause | First instrument |
|---|---|---|
| The link never reaches an admitting state | §30 — a training or link-manager handshake never completes | link_state_q and where it stalls; retrain_attempts_q |
| Works at low load, stalls forever later | §13 or §40 — a counter drift or a credit leak | occupancy against pointers; credit conservation |
| A duplicate semantic object after a retry | §22 — delivery before the history check | is the gate combinational into the push? |
| A response is lost after a recovery | §9 — semantic or queue state cleared by the recovery | what changed at the recovery cycle |
| The first failure is never available | §34 or §9 — capture after retrain, or the wrong reset domain | is first_fault_q in the POR domain? |
| Data and metadata mismatched | §26 — pipelines of different depth | metadata at the far end against acceptance |
| Fails only when the clocks differ | §40 — a pulse crossed as a pulse | which events cross domains, and how |
| Throughput unexpectedly halved | §42 — a shared ready coupling TX and RX | does rx_ready depend on any TX term? |
| The link works until a configuration change | §37 — a live configuration write | does active_cfg_q change outside a commit? |
| An object disappears with no error | §17 — the one-cycle unowned window | ownership model; is the pop gated on replay commit? |
| Retries never stop on one object | §24 — a duplicate suppressed without an acknowledgement | is the reply sent for a recognised duplicate? |
| A recoverable error becomes a permanent failure | §30 — the attempt budget too small, or no DEGRADED state | retrain_attempts_q; is DEGRADED reachable? |
Row 10 is the hardest. An object disappears with no error anywhere is the one-cycle handoff window, and the only detector is a model that checks at every cycle that every live object is held by something.
55. Debug Checklist
- Which semantic object, by the client's identity?
- Who owns it right now — the TX queue, replay, both, or neither? (§18)
- Which TX queue entry, and what is the occupancy? (§12)
- Does occupancy match the pointer difference? (§14)
- Was a replay entry allocated, and when relative to the queue pop? (§16, §17)
- What are the replay pointers, and has the buffer wrapped? (14.3)
- How many physical attempts has this object had? (§45)
- How many semantic deliveries? Must be ≤ 1. (§24)
- Which class, and how many credits does it have? (§19)
- Do the credits conserve — available plus outstanding equals advertised? (§20)
- Was a credit consumed on an actual transfer, or at acceptance? (§19)
- Which configuration epoch did the object carry? (§36)
- Did the active configuration change during its life? (§38)
- What is the link-manager state, and how did it get there? (§30)
- Is training active, and how many attempts have been made? (§30)
- What is the lane map and the active width? (§36)
- Is a recovery active, and is admission disabled? (§32)
- Was the first fault captured, and before the retrain? (§34, §35)
- Which reset scopes have been asserted? (§8)
- What survived each, against §7's table?
- Which clock domain is the signal in question in? (§39)
- Does any event cross a domain as a pulse? (§40)
- Was the RX gate's verdict evaluated before the push? (§23)
- Which of the five gate terms refused? (§23)
- Does RX readiness depend on any TX term? (§43)
- What does the stall histogram say, and is
TXS_UNATTRIBnon-zero? (§44) - Which of the four scoreboard layers diverged first? (§46)
56. Common Misconceptions
"A UCIe link is one FSM." It is at least three cooperating machines — link management, training and lane management — plus a datapath with its own per-object state, and six or more distinct state lifetimes. A single merged machine cannot express a policy decision that outlives a training attempt (§28, §30).
"Recovery is just reset." A recovery re-establishes a link. It must not clear semantic obligations, queue contents, the fault record or the performance counters — and a design with one reset_all driven by link_recovery loses accepted objects, makes them unretransmittable, and erases the evidence of what caused the event (§9, §10).
"The replay buffer owns the semantic transaction." Replay owns a transport object until it is acknowledged. The semantic obligation belongs to the protocol client and outlives the replay entry — in the worked trace, by sixteen cycles (§7, §47).
"PHY send means the TX object can be forgotten." The TX queue may release only after replay confirms it holds the object, and replay may release only on acknowledgement. A pop gated on PHY send opens a window in which nothing owns it (§16, §17).
"A clean CRC means semantic delivery is safe." Integrity is one of five gate terms. An object can be intact and be a duplicate, be from a dead epoch, or have nowhere to go — and pushing before the verdict handles three of four validity-and-novelty combinations wrongly (§22, §23).
"Credits are just performance counters." They are the flow-control guarantee, and a leak — from a mis-index or a lost CDC pulse — drifts the count downward inside its legal range until the link stops permanently with no error anywhere. Conservation catches it on the first lost return; bounds never do (§19, §20, §40).
"All state can use one reset." There are at least five reset scopes with different triggers, and §7's table shows sixteen kinds of state with four different survival behaviours. Diagnostics in particular must survive the events that clear everything else (§8, §35).
"Training and link management should be one FSM." They have different lifetimes: a link manager's state lives for the link's operational life and a training controller's for one attempt. Merging them means a policy decision is destroyed every time training restarts (§28).
"Configuration can be changed live if each field is valid." Different pipeline stages then interpret one object under different configurations, and a replayed object is framed under a configuration the peer does not expect. The commit needs a drained pipeline and no outstanding replay entries (§36, §37).
"TX and RX should share one ready signal." A shared link_ready halves throughput at best and deadlocks at worst, because the peer's progress depends on our transmissions and our transmissions then depend on our receive path draining (§42).
"CDC is a physical implementation detail." A one-cycle credit-return pulse crossed with a synchroniser is lost at some clock ratios, and every lost pulse is a permanent credit leak. The link works for hours and then stops (§40).
"A top-level block diagram is enough to define the architecture." The diagram shows blocks; the architecture is §7's table — who owns each piece of state, when it is allocated and released, and what it survives. Two designs with identical block diagrams and different ownership tables behave completely differently under recovery.
"The fastest way to implement UCIe is one large module." It removes every boundary at which ownership, reset scope and clock domain could have been reviewed — which are precisely the three things this chapter's failures are made of.
"Assertions belong after the RTL is complete." Five of §52's twenty-seven properties are connectivity claims — that a recovery does not reach a reset, that a pop is gated on replay, that RX readiness has no TX term. Those are decided when the top level is wired, and writing them afterwards means discovering them afterwards.
"A recovery that returns to an active state was successful." In §9's design the link returns active, reports success, and has lost every accepted object and the record of why. Reaching an admitting state is a link-management outcome, not a semantic one (§9, §50).
57. Understanding Check
58. Summary and What Comes Next
A UCIe link is a hierarchy of state machines and queues whose lifetimes overlap, and the architecture is the discipline of never letting one of them reset, release or decide on behalf of another.
The state-ownership table is the design. Sixteen kinds of state, twelve owners, and four different survival behaviours — and two designs with identical block diagrams and different tables behave completely differently under recovery.
Five reset scopes, and a recovery is not one of them. One reset_all driven by link_recovery loses accepted objects, makes them unretransmittable, and erases the record of why — while the link comes back and reports success.
The object is never unowned. The TX queue releases only after replay confirms, which opens a deliberate one-cycle dual-ownership window — and the alternative opens a one-cycle window in which nothing owns it and an error loses it silently.
Check, then act. The receive gate is five terms evaluated combinationally before the push; pushing first mishandles three of four validity-and-novelty combinations while the transport behaves exactly as specified.
Three machines, not one. Link management, training and lane management have three lifetimes, and their outputs come from state rather than from raw asynchronous events.
Capture before you retrain, or the retrain overwrites the evidence and the same failure recurs indefinitely with nothing to diagnose it.
Commit configuration atomically, with a drained pipeline and no outstanding replay — because a replayed object framed under the old configuration is transmitted under the new one.
Cross state, not events. A one-cycle credit-return pulse across a clock boundary is a permanent leak that stops the link after hours, and only conservation catches it.
And TX and RX are independent. One shared ready signal halves throughput at best and closes a deadlock cycle through the peer at worst.
This chapter partitioned the link into blocks and assigned every piece of state an owner. The next chapter opens the first of those blocks. The Protocol Layer is where the mapped protocols actually live — PCIe, CXL and the streaming path — and its RTL is where a protocol's transactions become objects the Adapter can carry.
- 19.2 — Protocol Engines — RTL for the Protocol layer's PCIe, CXL and Streaming engines.
Browse the full path on the UCIe tutorials index.