Skip to content

UCIe · Module 19

Protocol Engines

What the RTL above the UCIe Adapter must remember so PCIe, CXL and Streaming traffic keep their meaning while sharing one transport — normalisation that must be lossless, semantic state allocated at acceptance rather than at transmission, identity plus generation so a late response cannot alias a new request, a pending-parts bitmap instead of a counter, per-protocol completion policy, and a semantic table that survives every transport recovery.

Chapter 19.1 partitioned the link into blocks and gave every piece of state an owner. This chapter opens the first of those blocks — the one that sits above the Adapter and holds the meaning.

1. The One-Sentence Model

A protocol engine converts a semantic obligation into transportable objects without transferring ownership of the semantic obligation to the transport. It owns the operation until the higher-level protocol says it is complete — which is later than transmission, later than delivery, and later than the Adapter's own retirement.

2. What This Chapter Owns

QuestionWhere it is answered
The link's top-level partition and state-ownership table19.1 — Link Architecture
PCIe over UCIe — mapping, ordering, completionModule 10
CXL over UCIe — the three subprotocols, transport, integration11.111.5 · 16.5
Streaming — model, packets, ordering, reliability, flow control9.19.5
Request, response and transaction lifecycle12.1 · 12.2 · 12.4
Adapter reliability — CRC, replay, duplicate suppression19.3 — Adapter Design
Buffer structures, sizing, watermarks, ping-pong19.4 — UCIe Buffering
The credit machine in RTL19.5 — Flow-Control Logic (planned)

This chapter does not re-teach any protocol's semantics. It teaches the engine that holds them: what state exists, when it is allocated, when it may be released, and what must survive.

Five things exist only here:

Normalisation and its loss hazard (§8–§11). The Adapter must not understand higher protocols, so the engine produces a normalised object — and §10 is the normalisation that discards a distinction reconstruction needs.

The allocation point (§17–§19). Semantic state is allocated when the client's request is accepted, not when the Adapter takes it — and §18 is a 200-cycle window in which an accepted obligation has no record.

Identity, generation and the aliasing failure (§20–§23), which is how a late response attaches to a new request.

Multi-object transactions (§31–§34), where a counter retires a transaction with a part still missing.

And per-protocol completion (§35–§36) — one universal "response received" event is correct for at most one protocol class.

3. Sourcing

4. Three Objects, at This Boundary

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 semantic operation        — what the client asked for; the ENGINE owns it

N normalised objects        — request metadata, data, completion (§31)

N Adapter transport objects — 19.3's subject

≥N physical attempts        — retries add attempts, not objects
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
semantic allocations per operation   == 1        <- Section 19
normalised objects per operation     >= 1        <- Section 31
transport objects per normalised obj == 1
attempts per transport object        >= 1        <- 19.3
semantic completions per operation   == 1        <- Section 35

The engine's entire job is to keep the first and last lines true while everything between them varies. Every failure in this chapter is one of those five lines broken.

5. The Engine

A UCIe protocol layer drawn as nine blocks. Three protocol clients — PCIe, CXL and Streaming — feed three per protocol ingress queues, which are selected by an arbiter with a reserved progress class. The selected request passes through a normalisation stage that produces a common internal object, and simultaneously allocates an entry in a semantic transaction table that holds the operation's identity, generation, protocol class and state. Normalised objects are handed across the FDI boundary to the Adapter. On the return path, normalised responses come back from the Adapter into a response matching stage, which looks the response up in the semantic table by identity and generation rather than by arrival order, and only then signals completion to the originating protocol client. The point of the drawing is that the semantic table sits above and apart from the Adapter, and outlives every object the Adapter handles.PCIe / CXL /Streamthree clientsPer-protocolqueuesno shared head (§13)Arbiterprogress classreservedNormalisationlossless or §10Semantic tableallocated atACCEPTANCEAdapter (FDI)19.3's subjectResponse matchingby identity, notorderCompletion policyper protocol class(§35)Protocol configrequested / active12
Three protocol front ends, one normalisation stage, and a semantic table that is deliberately separate from the Adapter. The return path matches responses by identity against that table — never by arrival order — and completion is decided per protocol class.

Read the semantic table's position. It is above the Adapter and connected to both the acceptance path and the return path — and to nothing in the transport. That placement is the chapter.

6. One Engine, or Several?

Unified enginePer-protocol front ends, common back end
Logic areasmallerlarger
Per-protocol reasoningburied in one condition treeisolated and reviewable
Head-of-line blockingacross protocols (§13)removed at the front
Verificationevery test exercises every protocol's logicper-protocol testbenches possible
Adding a protocoledit the shared treeadd a front end
Corner-case riska PCIe fix can break CXLcontained

Neither is universally correct, and the choice depends on how different the protocols' semantics actually are in a given design.

Two observations that usually decide it.

The shared back end is where the value is, not the shared front end. Normalisation, the semantic table, identity allocation, response matching and completion are the same machinery for every protocol — so a design can share all of that while keeping the front ends separate, and get most of the area benefit with none of the coupling.

And row 5 is the one that bites later. A unified engine's condition tree grows with each protocol, and a change made for one protocol is a change to the logic all of them use. That is a maintenance property rather than a correctness one, and it is why the hybrid is common.

7. The Protocol Class

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. NOT a UCIe encoding, and the values carry no normative meaning
// (Section 3). The class exists so that later stages can apply per-protocol
// policy without re-deriving it from the payload.
typedef enum logic [1:0] {
  PROTO_PCIE   = 2'd0,
  PROTO_CXL    = 2'd1,
  PROTO_STREAM = 2'd2
} proto_class_e;

Architecture. A carried tag rather than a derived one. The class is decided once, at the front end that produced the object, and every later stage reads it — which is what lets the completion policy (§35), the arbiter (§14) and the response matcher (§28) each apply protocol-specific behaviour without parsing anything.

Failure. Deriving the class downstream from the payload's shape, which puts the normalisation stage — and eventually the Adapter — in the business of understanding protocols (§9).

8. The Normalised Object

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE internal object. NOT a UCIe, PCIe or CXL format, and no field
// corresponds to any specified field of any of them (Section 3).
typedef struct packed {
  logic [SEM_ID_W-1:0]  semantic_id;    // the ENGINE's identity for the operation
  logic [GEN_W-1:0]     generation;     // Section 22
  proto_class_e         proto;          // carried, not derived (Section 7)
  logic [TYPE_W-1:0]    type_class;     // the protocol's own operation category
  logic [DEST_W-1:0]    destination;    // captured at acceptance, never recomputed
  logic [LEN_W-1:0]     length;
  logic [PART_W-1:0]    part_idx;       // which part of a multi-object operation
  logic [PART_W-1:0]    part_count;     // Section 31
  logic [CFG_EPOCH_W-1:0] cfg_epoch;    // Section 42
  logic                 has_data;
  logic [META_W-1:0]    metadata;       // protocol-specific, carried OPAQUELY
} protocol_object_t;

Architecture. A common shape with one deliberately opaque field. metadata is carried and never interpreted by anything below the front end — that opacity is the structural guarantee that the layering below cannot erode (§9).

State. One register per pipeline stage, plus the semantic-table entry it references.

Cycle behaviour. Formed at normalisation and held stable while offered (§40). No field is recomputed downstream — destination in particular is an acceptance-time decision, the same rule as 17.2 §16 and 18.2 §10.

Contract. The Adapter needs length, has_data and enough to frame the object. The remote engine needs everything required to reconstruct the original semantic operation, which is §11's property and the reason type_class and metadata exist at all.

Failure. §10. Also omitting part_idx and part_count, which makes a multi-object operation's completeness underivable (§32).

DV. §11's lossless property; assert destination and cfg_epoch immutable while the entry is live.

9. Why Normalisation Exists

Without a normalised object, the Adapter would have to understand every protocol it carries. That violates the layering, multiplies the Adapter's state by the number of protocols, and makes every protocol addition an Adapter change.

With normalisationWithout
the Adapter sees a transport contractthe Adapter sees three protocols
adding a protocol adds a front endadding a protocol changes the Adapter
Adapter verification is protocol-agnosticevery Adapter test needs protocol knowledge
the risk moves to normalisation being lossy (§10)the risk is Adapter complexity

The trade is real and worth naming. Normalisation does not remove the difficulty; it relocates it to one stage where it can be verified once — and §11 is that verification.

10. Wrong Normalisation — a Discarded Distinction

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — two higher-layer operations with different semantics are normalised
// to the same internal type, because they "look the same" at this level.
always_comb
  case (client_op)
    OP_A, OP_B: norm_obj.type_class = TYPE_READ;   // ← A and B are NOT the same
    default:    norm_obj.type_class = TYPE_OTHER;
  endcase

Suppose operations A and B differ in some ordering or completion obligation — the specific difference does not matter here, only that it exists and that the far end needs it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. A and B both become TYPE_READ.
2. The object crosses. Transport is perfect: clean integrity, no retries.
3. The remote engine reconstructs a TYPE_READ.
4. It cannot tell whether this was an A or a B.
5. -> it applies one protocol's rule to the other's operation.

Four properties.

Every layer below normalisation is blameless. The Adapter framed and delivered exactly what it was given; the physical layer transmitted exactly those bits. Transport verification passes completely.

And the remote object is well-formed. It is not malformed, so nothing rejects it. It is a valid object of the wrong kind, which only a semantic model can detect (§50).

The failure surfaces far from its cause. The wrong rule applied at the far end produces a misordering or a premature completion many cycles later, in a different subsystem.

And the fix is not "add a bit" — it is a verification obligation. Any field the far end needs must be provably carried, which is why §11 is written as a reference-model property rather than as a review checklist.

11. SVA — Normalisation Is Lossless

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. For every accepted client request, the normalised object retains
// every field the far end needs to reconstruct the operation.
//
// tb_required_fields() is TESTBENCH knowledge: it is the reference model's
// statement of what reconstruction needs, derived from the protocol, not from
// the design. Synthesising it would compare the design to itself.
property p_normalisation_lossless(int unsigned rid);
  @(posedge clk) disable iff (!rst_n)
    (norm_fire && (tb_ref_id == rid))
      |-> tb_fields_preserved(rid, norm_obj);
endproperty
a_normalisation_lossless: assert property (p_normalisation_lossless(REF_UT));
 
// The class is carried, not re-derived downstream.
property p_class_stable_through_pipeline;
  @(posedge clk) disable iff (!rst_n)
    obj_live(IDX) |-> $stable(obj_q[IDX].proto);
endproperty
a_class_stable_through_pipeline:
  assert property (p_class_stable_through_pipeline);
 
// The reconstructed operation at the far end equals what was accepted here.
property p_reconstruction_matches(int unsigned rid);
  @(posedge clk) disable iff (!rst_n)
    (remote_reconstruct_fire && (tb_ref_id == rid))
      |-> (remote_op_signature == tb_op_signature(rid));
endproperty
a_reconstruction_matches: assert property (p_reconstruction_matches(REF_UT));

Architecture. Three properties: field preservation, class stability, and end-to-end reconstruction.

Why the third is the one that matters. The first two check the object; the third checks that the far end actually rebuilt the right operation — which is §10's failure, and it is invisible to any check performed on the object alone.

Why it must be a testbench reference. What reconstruction needs is a property of the protocol, not of this design. A design-side check would encode the same assumption that produced the bug.

DV. Requires a reference model per protocol class. That is the cost, and it is the only thing standing between §10 and silicon.

12. Per-Protocol Ingress Queues

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. One queue per protocol class, so a burst in one cannot block
// another's head — 18.2 Section 14's argument, at the protocol boundary.
typedef struct packed {
  logic [SEM_ID_W-1:0]  client_tag;      // the client's own identity
  logic [TYPE_W-1:0]    type_class;
  logic [DEST_W-1:0]    destination;
  logic [LEN_W-1:0]     length;
  logic [META_W-1:0]    metadata;
} ingress_entry_t;
 
// Payload storage — inferred RAM, deliberately NOT reset (19.1 Section 12).
ingress_entry_t   ing_mem [NUM_PROTO][ING_DEPTH];
logic [PTR_W-1:0] ing_wr_q  [NUM_PROTO];
logic [PTR_W-1:0] ing_rd_q  [NUM_PROTO];
logic [OCC_W-1:0] ing_occ_q [NUM_PROTO];
 
logic [NUM_PROTO-1:0] ing_request;
always_comb
  for (int p = 0; p < NUM_PROTO; p++)
    ing_request[p] = (ing_occ_q[p] != '0);
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    for (int p = 0; p < NUM_PROTO; p++) begin
      ing_wr_q[p]  <= '0;
      ing_rd_q[p]  <= '0;
      ing_occ_q[p] <= '0;
    end
  end else begin
    for (int p = 0; p < NUM_PROTO; p++) begin
      if (ing_push_fire[p]) ing_wr_q[p] <= ing_wr_q[p] + 1'b1;
      if (ing_pop_fire[p])  ing_rd_q[p] <= ing_rd_q[p] + 1'b1;
 
      unique case ({ing_push_fire[p], ing_pop_fire[p]})
        2'b10: ing_occ_q[p] <= ing_occ_q[p] + 1'b1;
        2'b01: ing_occ_q[p] <= ing_occ_q[p] - 1'b1;
        default: ;                        // both or neither: hold
      endcase
    end
  end

Architecture. One queue per class, so each class presents its own head to the arbiter — which is the only structure that removes §13's blocking, since no arbitration policy can act on a packet the queue does not offer.

State. NUM_PROTO × ING_DEPTH entries plus three registers per class. The payload is inferred RAM and not reset; occupancy starts at zero and gates every pop.

Cycle behaviour. Push and pop are handshake-qualified; the simultaneous cycle is handled in one unique case.

Contract. The arbiter reads ing_request; admission reads ing_occ_q for the request's own class, never an aggregate (17.4 §17).

Failure. Sizing all classes equally when their traffic shapes differ by orders of magnitude — a streaming class needs depth; a control class needs low latency and very little depth.

DV. Cover each class empty, mid and full; cover the simultaneous push-pop cycle per class.

13. Wrong Architecture — One Shared FIFO Before Classification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — every protocol pushes into one queue, and classification happens
// after the pop.
assign shared_push = pcie_valid || cxl_valid || stream_valid;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. A streaming burst of 64 objects fills the shared queue.
2. A latency-sensitive completion from another protocol arrives and queues
   behind all 64.
3. It waits for the entire burst.
4. If that completion is what RELEASES a resource the streaming traffic
   needs, the burst cannot drain either.
5. -> deadlock, on a design where every block is individually correct.

Four properties.

Step 3 alone is a latency disaster. An illustrative 64-object burst at 8 cycles per object is 512 cycles of pure queueing for an object whose own service is a fraction of that.

Step 4 turns it into a correctness failure. 18.2 §31's principle applies exactly: a message that releases a resource must never queue behind one that acquires resources — and a shared FIFO guarantees it can.

No arbitration policy fixes it, because the queue offers only its head. The fix is structural (§12).

And it is invisible with one protocol active. A design tested one protocol at a time never produces the condition — which is why §51's simultaneous-class bin exists.

14. The Arbiter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE arbiter over per-protocol queues. Rotating fairness plus a
// bounded override for the class that releases resources.
logic [NUM_PROTO-1:0] req, grant;
logic [PR_W-1:0]      last_grant_q;
logic [AGE_W-1:0]     wait_age_q [NUM_PROTO];      // saturating
logic [NUM_PROTO-1:0] is_progress_class;           // static configuration
 
logic [NUM_PROTO-1:0] rotated;
always_comb
  for (int p = 0; p < NUM_PROTO; p++)
    rotated[p] = req[(p + last_grant_q + 1) % NUM_PROTO];
 
logic [NUM_PROTO-1:0] aged_out;
always_comb
  for (int p = 0; p < NUM_PROTO; p++)
    aged_out[p] = req[p] && (wait_age_q[p] >= class_bound(p));
 
always_comb begin
  grant = '0;
  if (!downstream_can_accept)                    grant = '0;
  else if (|(aged_out & is_progress_class))      grant = lowest_set(aged_out & is_progress_class);
  else if (|aged_out)                            grant = lowest_set(aged_out);
  else if (|rotated)                             grant = unrotate(lowest_set(rotated), last_grant_q);
end
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    last_grant_q <= '0;
    for (int p = 0; p < NUM_PROTO; p++) wait_age_q[p] <= '0;
  end else begin
    // THE rule: advance only on an actual transfer.
    if (|grant && xfer_fire)
      last_grant_q <= onehot_to_index(grant);
 
    for (int p = 0; p < NUM_PROTO; p++) begin
      if (req[p] && !(grant[p] && xfer_fire))
        wait_age_q[p] <= (wait_age_q[p] == AGE_MAX) ? AGE_MAX : wait_age_q[p] + 1'b1;
      else if (grant[p] && xfer_fire)
        wait_age_q[p] <= '0;
    end
  end

Architecture. Rotation for fairness plus a per-class bounded override for liveness. class_bound(p) differs per class — a control class's bound is tight because its latency requirement is; a streaming class's is loose because it does not have one.

State. A rotation pointer and one saturating age per class. Saturating, because a wrapping age reports a fresh requester at the moment it has waited longest.

Cycle behaviour. grant is combinational and gated by downstream_can_accept, so the arbiter never grants into a stage that cannot take it. Both the pointer advance and the age reset are qualified by xfer_firethe seventh appearance of that rule in this curriculum (13.4 §18, 17.3 §17, 17.4 §15, 18.1 §8, 18.2 §18, 18.3 §22, and here).

Contract. Each class relies on its own bound; the progress class relies on the override. Neither is visible at the interface, which is why §15 asserts both.

Failure. Advancing the pointer on grant rather than xfer_fire, which grants a blocked class repeatedly and starves it while the grant histogram looks uniform (18.2 §18).

DV. Saturate every class; measure worst-case per-class wait against each bound.

15. SVA — Arbitration Safety and Bounded Service

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. At most one grant, only to a requester, and the pointer moves
// only on a transfer.
property p_one_grant;
  @(posedge clk) disable iff (!rst_n) $onehot0(grant);
endproperty
a_one_grant: assert property (p_one_grant);
 
property p_grant_implies_request;
  @(posedge clk) disable iff (!rst_n)
    (grant != '0) |-> ((grant & req) == grant);
endproperty
a_grant_implies_request: assert property (p_grant_implies_request);
 
property p_pointer_on_transfer_only;
  @(posedge clk) disable iff (!rst_n)
    $changed(last_grant_q) |-> $past(|grant && xfer_fire);
endproperty
a_pointer_on_transfer_only: assert property (p_pointer_on_transfer_only);
 
// LIVENESS, per class, bounded, with assumptions stated (15.2 §36).
//   A1: the downstream eventually accepts
//   A2: a requesting class keeps requesting until served
assume property (@(posedge clk) disable iff (!rst_n)
  (|grant) |-> ##[1:XFER_BOUND] xfer_fire);
assume property (@(posedge clk) disable iff (!rst_n)
  (req[P] && !xfer_fire) |=> req[P]);
 
property p_class_served_within_its_bound(int p);
  @(posedge clk) disable iff (!rst_n)
    req[p] |-> ##[1:CLASS_BOUND(p)] (grant[p] && xfer_fire);
endproperty
a_pcie_served:   assert property (p_class_served_within_its_bound(PROTO_PCIE));
a_cxl_served:    assert property (p_class_served_within_its_bound(PROTO_CXL));
a_stream_served: assert property (p_class_served_within_its_bound(PROTO_STREAM));

Architecture. Three safety properties and one bounded liveness property per class.

Why per-class bounds rather than one. A single generous bound passes for a design that starves the control class for thousands of cycles. Writing them separately is what makes each class's requirement reviewable (18.3 §24).

DV. Prove all; then set every bound equal and confirm the tightest one fails.

16. The Semantic Transaction Table

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. This is PROTOCOL-ENGINE state. It is NOT replay state — that
// belongs to the Adapter (19.3) and has an entirely different lifetime.
typedef enum logic [2:0] {
  SEM_FREE        = 3'd0,
  SEM_ACCEPTED    = 3'd1,   // the client's request is owned; no object formed yet
  SEM_OBJECTS_OUT = 3'd2,   // one or more normalised objects handed to the Adapter
  SEM_AWAIT_RESP  = 3'd3,
  SEM_PARTIAL     = 3'd4,   // some parts resolved, some pending (Section 32)
  SEM_COMPLETE    = 3'd5,
  SEM_FAILED      = 3'd6
} sem_state_e;
 
typedef struct packed {
  logic                    valid;
  sem_state_e              state;
  proto_class_e            proto;
  logic [SEM_ID_W-1:0]     semantic_id;
  logic [GEN_W-1:0]        generation;
  logic [DEST_W-1:0]       destination;     // captured at acceptance
  logic [CFG_EPOCH_W-1:0]  cfg_epoch;
  logic [MAX_PARTS-1:0]    pending_parts;   // Section 32 — a BITMAP
  logic                    response_expected;
  logic [POLICY_W-1:0]     completion_policy;  // per protocol (Section 35)
} semantic_entry_t;
 
semantic_entry_t sem_q [NUM_SEM_ENTRIES];

Architecture. One entry per live semantic operation. Seven states, and SEM_ACCEPTED is the one that matters most: it exists so that an operation the client has handed over has a record before any object has been formed (§17).

State. NUM_SEM_ENTRIES entries. This depth bounds semantic concurrency and is a first-order performance parameter, not a convenience.

Cycle behaviour. Allocated at client acceptance (§17), freed at protocol-defined completion (§35). One next-state owner18.3 §15's race is available here too.

Contract. The response matcher (§28), the completion policy (§35) and the recovery path (§44) all read it. It is the single record of what was promised and to whom.

Failure. §18. Also indexing it by the client's own tag without proving that tag is unique in this scope (§20).

DV. §19, §23, §27's properties.

17. The Allocation Point

A semantic entry is allocated when the client's request is accepted. Not when the object is normalised, not when the Adapter takes it, and not when the PHY sends it.

Candidate eventWhy it is wrong
the Adapter accepts the objectthe client was told yes long before — §18
the object is normalisednormalisation may be a pipeline stage away from acceptance
the PHY sendsmany cycles later, and it may never happen
the client's request is acceptedthe moment the obligation is transferred

The rule generalises: allocate the record at the moment the obligation is taken on, because that is the moment something can go wrong that the record is needed to reason about.

18. Wrong RTL — Allocating at Adapter Acceptance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the semantic entry is created when the Adapter takes the object.
always_ff @(posedge clk)
  if (adapter_accept_fire) begin
    sem_q[alloc_id].valid <= 1'b1;
    sem_q[alloc_id].state <= SEM_OBJECTS_OUT;
  end
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. The client's request is accepted at cycle 10. client_ready was high.
2. The Adapter is backpressured — no credit, staging full, recovery.
3. It does not accept the object until cycle 210.
4. Between cycles 10 and 210 the operation EXISTS and has NO RECORD.

What cannot be done during those 200 cycles:

Event in the windowConsequence with no record
the client cancels the operationnothing to cancel — it proceeds anyway
an error requires failing itnothing to fail, and nothing to report
a recovery occursthe operation is invisible to the recovery logic
the semantic table's occupancy is readunderstated — admission over-commits
a timeout must be startedno entry to age

Four properties.

The window is exactly as long as the Adapter's backpressure, which under load is precisely when it is longest. The design works when it is idle and has a 200-cycle blind spot when it is busy.

Row 4 compounds it. With occupancy understated, the engine accepts more requests than it has entries for — and then cannot allocate when the Adapter finally drains.

And nothing detects the window. The operation completes normally in the common case; only an event inside the window exposes it, which is a coverage requirement rather than a lucky test.

The correct form allocates at acceptance and advances state later:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Allocate on acceptance; the state machine tracks the rest.
always_ff @(posedge clk)
  if (client_accept_fire) begin
    sem_q[alloc_id].valid       <= 1'b1;
    sem_q[alloc_id].state       <= SEM_ACCEPTED;   // <- exists immediately
    sem_q[alloc_id].generation  <= gen_q[alloc_id];
    sem_q[alloc_id].destination <= client_dest;
    sem_q[alloc_id].cfg_epoch   <= active_cfg_epoch_q;
  end

19. SVA — Acceptance Implies Ownership State

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. An accepted client request has a semantic entry immediately.
property p_acceptance_implies_entry;
  @(posedge clk) disable iff (!rst_n)
    client_accept_fire |=> (sem_q[$past(alloc_id)].valid
                         && (sem_q[$past(alloc_id)].state != SEM_FREE));
endproperty
a_acceptance_implies_entry: assert property (p_acceptance_implies_entry);
 
// No client request is ever accepted without a free entry to hold it.
property p_no_accept_without_slot;
  @(posedge clk) disable iff (!rst_n)
    client_accept_fire |-> sem_slot_available;
endproperty
a_no_accept_without_slot: assert property (p_no_accept_without_slot);
 
// Exactly one allocation per accepted operation.
property p_one_allocation_per_operation(int unsigned rid);
  @(posedge clk) disable iff (!rst_n)
    (tb_allocations_for(rid) <= 1);
endproperty
a_one_allocation_per_operation:
  assert property (p_one_allocation_per_operation(REF_UT));
 
// The captured destination and epoch never change under a live entry.
property p_capture_immutable;
  @(posedge clk) disable iff (!rst_n)
    sem_q[IDX].valid |-> ($stable(sem_q[IDX].destination)
                       && $stable(sem_q[IDX].cfg_epoch)
                       && $stable(sem_q[IDX].proto));
endproperty
a_capture_immutable: assert property (p_capture_immutable);

Architecture. Four properties: immediate ownership, admission safety, single allocation, and capture immutability.

Why the third needs a reference model. The design has no signal saying "this is operation 47". The testbench generated it and therefore knows — and a transport retry re-driving the accept path must not produce a second allocation.

DV. §18's window is caught by injecting a cancellation while the Adapter is backpressured (§51).

20. Identity Allocation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Do NOT index the table by the client's own tag unless that tag
// is provably unique across every client sharing this engine.
logic [NUM_SEM_ENTRIES-1:0] free_q;
 
// Lowest free entry — a priority encoder, not a search.
logic [SEM_ID_W-1:0] alloc_id;
logic                sem_slot_available;
 
always_comb begin
  sem_slot_available = (free_q != '0);
  alloc_id           = lowest_set_index(free_q);
end
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    free_q <= '1;                                   // all entries free
  end else begin
    unique case ({client_accept_fire, sem_retire_fire})
      2'b10: free_q <= free_q & ~(1 << alloc_id);
      2'b01: free_q <= free_q |  (1 << retire_id);
      2'b11: free_q <= (free_q & ~(1 << alloc_id)) | (1 << retire_id);
      default: ;
    endcase
  end

Architecture. A free bitmap with a priority-encoder allocator. The bitmap scales to the table's size and the encoder is one combinational cone — a linear search would not meet timing at any useful depth.

State. NUM_SEM_ENTRIES bits. One owner, with the simultaneous allocate-and-retire cycle written out explicitly — the same discipline as every counter in this curriculum, and here a missed update strands an entry permanently.

Cycle behaviour. Cleared at acceptance, set at semantic retirement (§35) — not at Adapter acceptance and not at response arrival.

Contract. sem_slot_available gates client acceptance (§37). The bitmap is the authority on capacity, so a stranded bit is capacity permanently lost and a doubly-set bit is two operations sharing an entry.

Failure. §21. Also returning the bit on the response rather than on completion, which frees the identity while the operation may still have parts outstanding (§32).

DV. Assert the bitmap's population equals the count of invalid entries; cover the simultaneous cycle; cover full occupancy.

21. Wrong Identity Reuse

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the entry is freed when the Adapter takes the last object.
always_ff @(posedge clk)
  if (adapter_took_last_object[id])
    free_q <= free_q | (1 << id);          // ← the response has not arrived
CycleEntry idIn flightThe engine believes
40freed at Adapter acceptanceoperation A outstandingnothing outstanding on id
90reallocated to operation BA outstanding, B dispatchedB is outstanding
140live (B)A's response returns naming idB completed
141freedB still outstandingB's real response finds nothing

Four properties.

Row 140 is a wrong answer delivered confidently. A's response is handed to B's client — different operation, different expectation, possibly a different protocol class entirely.

And it is worse than the equivalent bug at other layers, because the two operations may belong to different protocols. A PCIe client can receive a CXL response, which is a category error that no downstream check anticipates.

Nothing reports an error. At the transport level everything was delivered exactly once to a live identity.

The fix is two-part and both parts are needed: free the identity only at semantic completion (§35), and carry a generation so even a mistakenly early free cannot alias (§22).

22. Generation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Identity plus generation — 12.2 Section 24's quarantine, at the
// protocol engine.
logic [GEN_W-1:0] gen_q [NUM_SEM_ENTRIES];
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n)
    for (int i = 0; i < NUM_SEM_ENTRIES; i++) gen_q[i] <= '0;
  else if (client_accept_fire)
    gen_q[alloc_id] <= gen_q[alloc_id] + 1'b1;

Architecture. A counter per identity, incremented at every allocation. {semantic_id, generation} is unique over a far longer window than the identity alone.

State. One counter per entry. GEN_W is derived from the maximum time a response can be delayed — which after a transport recovery can be very long — not chosen for convenience.

Cycle behaviour. Incremented at allocation only. The generation travels in the normalised object (§8) and returns in the response (§28).

Contract. The response matcher compares both fields. Carrying the generation without checking it has paid the cost and kept none of the benefit.

Failure. A one-bit toggle, which distinguishes consecutive uses and fails on the third. Or resetting generations on a transport recovery, which destroys the history exactly when stale responses are most likely (§44).

DV. Cover a response arriving with a stale generation and confirm it is reported as an orphan.

23. SVA — Identity Uniqueness

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. No identity is live twice, and the free bitmap is consistent.
property p_id_unique_while_live;
  @(posedge clk) disable iff (!rst_n)
    client_accept_fire |-> !sem_q[alloc_id].valid;
endproperty
a_id_unique_while_live: assert property (p_id_unique_while_live);
 
property p_free_bitmap_consistent;
  @(posedge clk) disable iff (!rst_n)
    (free_q[IDX] == !sem_q[IDX].valid);
endproperty
a_free_bitmap_consistent: assert property (p_free_bitmap_consistent);
 
// A response must name a live entry AND its current generation.
property p_response_matches_generation;
  @(posedge clk) disable iff (!rst_n)
    resp_valid |-> (sem_q[resp_id].valid && (sem_q[resp_id].generation == resp_gen));
endproperty
a_response_matches_generation:
  assert property (p_response_matches_generation);
 
// The identity is not returned to the pool before semantic completion.
property p_free_only_at_completion;
  @(posedge clk) disable iff (!rst_n)
    $rose(free_q[IDX]) |-> $past(sem_retire_fire && (retire_id == IDX));
endproperty
a_free_only_at_completion: assert property (p_free_only_at_completion);

Architecture. Four properties: uniqueness, bitmap consistency, generation-checked matching, and release discipline.

Why the second is worth its cost. The bitmap and the valid bits are two representations of one fact. They can drift — through a missed simultaneous update (§20) — and the drift is silent: an entry marked free while valid gets overwritten; one marked busy while invalid is capacity permanently lost.

DV. The fourth catches §21 directly, at the cycle of the early free rather than at the alias fifty cycles later.

24. The Semantic Engine State Machine

An illustrative seven-state protocol engine transaction controller. From FREE, accepting a client request moves to ACCEPTED, where the operation is owned but no object has been formed. From ACCEPTED, handing objects to the Adapter moves to OBJECTS OUT. From OBJECTS OUT, once all objects are away the state moves to AWAIT RESPONSE. From AWAIT RESPONSE, a partial resolution moves to PARTIAL, where some parts are resolved and others are still pending, and PARTIAL returns to AWAIT RESPONSE as further parts arrive. When the per protocol completion policy is satisfied, either AWAIT RESPONSE or PARTIAL moves to COMPLETE. A fault from any working state moves to FAILED. Both COMPLETE and FAILED return to FREE once the client has acknowledged. Note that these are illustrative implementation control states rather than any protocol's normative transaction states.FREEACCEPTEDOBJECTSOUTAWAITRESPPARTIALCOMPLETEFAILEDclient acceptedclient acceptedobjects handed overobjects handed overall objects awayall objects awaypartial resolvepartial resolvemore partsmore partspolicy satisfiedpolicy satisfiedpolicy satisfiedpolicy satisfiedfaultfaultfaultfaultclient ackclient ackclient ackclient ack
Illustrative protocol-engine control states, not any protocol's normative transaction states. The operation is owned from acceptance; objects are handed to the Adapter; responses resolve parts; and completion is decided by a per-protocol policy rather than by a single universal event.

Read the two states after FREE. ACCEPTED exists so the operation is owned before any object is formed (§17), and PARTIAL exists so a multi-object operation has somewhere to be while some parts are resolved and others are not (§32). A design without either state has nowhere to wait and therefore does not.

25. The Next-State Function

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. ONE always_comb, ONE always_ff, and every simultaneous event
// resolved by an explicit arm order.
always_comb begin
  nxt = sem_q[i].state;
  unique case (sem_q[i].state)
    SEM_FREE:        if (client_accept_fire && (alloc_id == i[SEM_ID_W-1:0]))
                                                              nxt = SEM_ACCEPTED;
 
    SEM_ACCEPTED:    if (fault[i])                            nxt = SEM_FAILED;
                     else if (cancel[i])                      nxt = SEM_FAILED;
                     else if (objects_handed[i])              nxt = SEM_OBJECTS_OUT;
 
    SEM_OBJECTS_OUT: if (fault[i])                            nxt = SEM_FAILED;
                     else if (all_objects_away[i])            nxt = SEM_AWAIT_RESP;
 
    SEM_AWAIT_RESP:  if (fault[i])                            nxt = SEM_FAILED;
                     else if (policy_satisfied[i])            nxt = SEM_COMPLETE;
                     else if (part_resolved[i])               nxt = SEM_PARTIAL;
 
    SEM_PARTIAL:     if (fault[i])                            nxt = SEM_FAILED;
                     else if (policy_satisfied[i])            nxt = SEM_COMPLETE;
                     else if (part_resolved[i])               nxt = SEM_AWAIT_RESP;
 
    SEM_COMPLETE:    if (client_ack[i])                       nxt = SEM_FREE;
    SEM_FAILED:      if (client_ack[i])                       nxt = SEM_FREE;
    default:                                                  nxt = SEM_FAILED;
  endcase
end
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) sem_q[i].state <= SEM_FREE;
  else        sem_q[i].state <= nxt;

Architecture. One owner, one unique case. fault is checked first in every working state, so a fault cannot be overtaken by a completion in the same cycle — an explicit priority rather than an emergent one.

Cycle behaviour. Note the ordering in SEM_AWAIT_RESP: policy_satisfied is checked before part_resolved, so a final part that satisfies the policy completes rather than looping through PARTIAL. Reversing those two arms adds a cycle to every operation's completion — harmless, and worth noticing that the arm order is a design decision.

Contract. The response matcher, the completion policy and the allocator all read state. No block outside this one may write it18.3 §15's two-writer race is available here and behaves identically.

Failure. §26.

DV. §27; cover every state and every legal transition, and cover simultaneous fault-and-completion.

26. Wrong RTL — Two Writers to the State

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the acceptance path and the response path each write the state.
// In the front end:
always_ff @(posedge clk)
  if (objects_handed[i]) sem_q[i].state <= SEM_OBJECTS_OUT;
 
// In the response path, a different module:
always_ff @(posedge clk)
  if (part_resolved[i])  sem_q[i].state <= SEM_PARTIAL;

On a cycle where both fire, the last non-blocking assignment in the tool's ordering wins and the other transition is lost.

Four properties.

Simulation and synthesis may disagree, and with the writes in different modules and different gating conditions synthesis may not flag a conflict. The behaviour becomes tool-dependent, which is the worst class of bug to carry into silicon.

The lost transition raises no error. The operation sits in a state it should have left, and the symptom is a hang whose cause is a cycle thousands of cycles earlier.

It is rare and load-dependent, because the two events coincide only when the pipeline is deep enough for an early part to resolve while later objects are still being handed over. That is exactly the high-throughput case.

And the correct resolution is a design decision — should a resolving part beat an object handover? — which can only be reviewed if it is written down in one place (§25).

27. SVA — State Discipline

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Only legal transitions, and the state comes only from the
// next-state function.
property p_legal_transitions;
  @(posedge clk) disable iff (!rst_n)
    $changed(sem_q[IDX].state)
      |-> is_legal_sem_transition($past(sem_q[IDX].state), sem_q[IDX].state);
endproperty
a_legal_transitions: assert property (p_legal_transitions);
 
property p_state_from_next_state_fn;
  @(posedge clk) disable iff (!rst_n)
    $changed(sem_q[IDX].state) |-> (sem_q[IDX].state == $past(nxt_for(IDX)));
endproperty
a_state_from_next_state_fn: assert property (p_state_from_next_state_fn);
 
// An entry is never in a working state with no pending work.
property p_await_implies_pending;
  @(posedge clk) disable iff (!rst_n)
    ((sem_q[IDX].state == SEM_AWAIT_RESP) || (sem_q[IDX].state == SEM_PARTIAL))
      |-> (sem_q[IDX].pending_parts != '0);
endproperty
a_await_implies_pending: assert property (p_await_implies_pending);

Architecture. Three properties: legality, single-source state, and a consistency invariant.

Why the second catches §26. With two writers, the committed state on a conflicting cycle will not equal what the single next-state function computed. It fires on the exact cycle of the race, rather than on the hang much later.

DV. Cover simultaneous events; run the legal-transition function as a formal property where possible.

28. Response Matching

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Four fields checked, and arrival order is not one of them.
assign resp_ok =
      sem_q[resp_id].valid
   && (sem_q[resp_id].generation == resp_gen)     // Section 22
   && (sem_q[resp_id].proto      == resp_proto)   // the class must match
   && (sem_q[resp_id].cfg_epoch  == resp_cfg_epoch);

Architecture. A four-way match against the captured acceptance context.

Why the class is checked. A response naming a live identity but a different protocol class is either a normalisation error (§10), an identity alias (§21), or a corrupted field — and all three are worth catching at the boundary rather than downstream, where a PCIe client would be handed a CXL response.

Why the epoch is checked. A response generated under one protocol configuration and arriving after a commit must be identifiable (§42).

Failure. §29.

DV. §30; inject each of the four violations separately.

29. Wrong Matching — Arrival Order

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — responses are matched to requests by position.
always_ff @(posedge clk)
  if (resp_valid) begin
    complete(pending_fifo[deliver_ptr]);        // ← the oldest request gets it
    deliver_ptr <= deliver_ptr + 1'b1;
  end

Illustrative. Request A is issued to a distant destination; request B to a nearer one, one cycle later.

EventCorrectThis code
B's response returns firstcompleted as Bcompleted as A
A's response returns secondcompleted as Acompleted as B

Four properties.

Responses do not return in request order, for at least three independent reasons: the remote side reorders, different destinations have different latencies, and a retried object arrives later than an unretried one issued after it. 12.2 §14 establishes the general rule; the engine is where it is violated.

The transport is perfect. Both responses crossed with clean integrity, exactly once, and contain exactly the right bytes for their own requests. This is a pure association error.

And the pending_fifo looks like a scoreboard and is not one. It records order; order is precisely the property that does not survive.

The failure is data-dependent and load-dependent. If A and B happen to expect similar responses, nothing looks wrong. The rate tracks how often reordering occurs, which tracks the destination mix.

30. SVA — Responses Belong to Live Operations

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Identity, generation, class and epoch must all agree.
property p_response_belongs_to_live_entry;
  @(posedge clk) disable iff (!rst_n)
    resp_valid |-> (sem_q[resp_id].valid
                 && (sem_q[resp_id].generation == resp_gen)
                 && (sem_q[resp_id].proto      == resp_proto)
                 && (sem_q[resp_id].cfg_epoch  == resp_cfg_epoch));
endproperty
a_response_belongs_to_live_entry:
  assert property (p_response_belongs_to_live_entry);
 
// A response for a retired entry is reported, never silently dropped.
property p_orphan_response_reported;
  @(posedge clk) disable iff (!rst_n)
    (resp_valid && !sem_q[resp_id].valid) |=> orphan_response_flag;
endproperty
a_orphan_response_reported: assert property (p_orphan_response_reported);
 
// The completion delivered to a client names the operation that client issued.
property p_completion_to_correct_client(int unsigned rid);
  @(posedge clk) disable iff (!rst_n)
    (client_complete_fire && (tb_ref_id == rid))
      |-> (complete_client == tb_issuing_client(rid));
endproperty
a_completion_to_correct_client:
  assert property (p_completion_to_correct_client(REF_UT));

Architecture. Three properties: full-context matching, orphan reporting, and the end-to-end client check.

Why orphans must be reported rather than dropped. An orphan response is evidence — of an early free (§21), a stale generation, or a duplicate. Dropping it silently discards the only signal that something upstream is wrong.

Why the third exists. The first checks the match; the third checks that the completion reached the client that asked — which is §29's failure, and the first property passes on it because the entry it matched really was live.

DV. Inject each violation; the third needs a reference model that tracks which client issued which operation.

31. One Operation, Several Objects

A single semantic operation can become several normalised objects — for example a request descriptor, one or more data objects, and a completion. The exact decomposition is protocol-specific and this chapter asserts none (§3).

What is architectural is that the operation is not complete until every part it declared is resolved — and that a duplicate part must not discharge a missing one.

32. The Pending-Parts Bitmap

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. A BITMAP, for the sixth time in this curriculum. The guard is
// what makes a duplicate part idempotent.
logic [MAX_PARTS-1:0] pending_parts_q [NUM_SEM_ENTRIES];
 
always_ff @(posedge clk)
  if (client_accept_fire)
    pending_parts_q[alloc_id] <= part_mask(client_part_count);
  else if (part_resolve_valid
           && sem_q[part_id].valid
           && (sem_q[part_id].generation == part_gen)
           && pending_parts_q[part_id][part_idx])              // GUARDED
    pending_parts_q[part_id][part_idx] <= 1'b0;
 
assign all_parts_resolved[i] = (pending_parts_q[i] == '0);

Architecture. One bit per part, set at acceptance from the declared part count and cleared under a guard. The guard pending_parts_q[part_id][part_idx] makes a duplicate part harmless by construction — it finds the bit already clear and changes nothing.

State. NUM_SEM_ENTRIES × MAX_PARTS bits. Small, and it is the only structure that distinguishes "part 2 arrived twice" from "parts 2 and 3 arrived".

Cycle behaviour. Set as a whole at acceptance; cleared one bit at a time, and only for a live entry of the matching generation — so a part belonging to a retired operation cannot clear a bit in its successor.

Contract. The completion policy (§35) reads all_parts_resolved. A false assertion of it retires an operation with work outstanding, and the client is told the operation finished.

Failure. §33. Also building the mask from MAX_PARTS rather than the operation's own declared count, which leaves every operation permanently incomplete because bits it never expected are never cleared.

DV. §34's properties; cover a duplicate part, a missing part, and the maximum part count.

33. Wrong RTL — a Scalar Part Counter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a counter cannot distinguish a new part from a repeat.
always_ff @(posedge clk)
  if (client_accept_fire)      parts_left_q[alloc_id] <= client_part_count;
  else if (part_resolve_valid) parts_left_q[part_id]  <= parts_left_q[part_id] - 1'b1;

Illustrative, three parts, with part 1's resolution transported twice.

EventCounterReality
accepted, 3 parts3parts 1, 2, 3 pending
part 1 resolves2parts 2, 3 pending
part 1 replayed1parts 2, 3 still pending
part 2 resolves0part 3 STILL PENDING
complete✗ retired with part 3 outstanding

Four properties.

The operation is reported complete to the client with a part missing. For a read that means data the client will use and did not receive; for a write it means an acknowledgement of something that did not happen.

The duplicate is not a bug anywhere else. It is a transport retry doing exactly what 14.3 specifies. The counter is the only incorrect component.

And part 3's resolution then arrives at a freed entry, producing an orphan (§30) — so the design gets a symptom, at the wrong time, describing the wrong thing.

The bitmap fix is free: MAX_PARTS bits instead of log2(MAX_PARTS), plus one term in the clear condition. This is the sixth appearance of this argument (16.2 §21, 18.1 §35, 18.2 §26, 18.2 §52, 18.3 §27, and here).

34. SVA — Each Part Clears Once

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. A duplicate part changes nothing.
property p_duplicate_part_idempotent;
  @(posedge clk) disable iff (!rst_n)
    (part_resolve_valid && !pending_parts_q[part_id][part_idx])
      |=> $stable(pending_parts_q[part_id]);
endproperty
a_duplicate_part_idempotent: assert property (p_duplicate_part_idempotent);
 
// A part for a stale generation changes nothing.
property p_stale_generation_part_ignored;
  @(posedge clk) disable iff (!rst_n)
    (part_resolve_valid && (sem_q[part_id].generation != part_gen))
      |=> $stable(pending_parts_q[part_id]);
endproperty
a_stale_generation_part_ignored:
  assert property (p_stale_generation_part_ignored);
 
// Completion requires every declared part.
property p_completion_requires_all_parts;
  @(posedge clk) disable iff (!rst_n)
    sem_retire_fire |-> (pending_parts_q[retire_id] == '0);
endproperty
a_completion_requires_all_parts:
  assert property (p_completion_requires_all_parts);
 
// Only a declared part index can clear a bit.
property p_only_declared_part_clears;
  @(posedge clk) disable iff (!rst_n)
    (part_resolve_valid && (part_idx >= sem_part_count[part_id]))
      |=> $stable(pending_parts_q[part_id]);
endproperty
a_only_declared_part_clears: assert property (p_only_declared_part_clears);

Architecture. Four properties: idempotence, generation validity, the completion condition, and index validity.

Why the fourth is not paranoia. With several operations in flight, a part carries an entry index and a part index. A corrupted or mis-derived part index clears a bit of a part the operation never declared — retiring it early, which is §33's failure through a different door.

DV. All four; the first two must be injected.

35. Completion Is Per Protocol

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. An abstract policy function. The actual condition for each
// protocol class is THAT PROTOCOL'S to define, and this chapter deliberately
// does not define one (Section 3).
typedef enum logic [POLICY_W-1:0] {
  CPL_ALL_PARTS        = 'd0,   // every declared part resolved
  CPL_ALL_PARTS_AND_ACK= 'd1,   // ...and an explicit acknowledgement
  CPL_POSTED           = 'd2    // no response expected at all
} cpl_policy_e;
 
function automatic logic semantic_done(
  input cpl_policy_e         policy,
  input logic                all_parts,
  input logic                ack_seen,
  input logic                response_expected
);
  unique case (policy)
    CPL_ALL_PARTS:         semantic_done = all_parts;
    CPL_ALL_PARTS_AND_ACK: semantic_done = all_parts && ack_seen;
    CPL_POSTED:            semantic_done = !response_expected;
    default:               semantic_done = 1'b0;      // fail closed
  endcase
endfunction

Architecture. One function, one policy selector taken from the captured protocol class of the entry. The policy is per class; the events are per operation; and the function is the only place they meet.

State. None — a pure function; the policy lives in the semantic entry (§16).

Cycle behaviour. Combinational, evaluated where the entry would be retired. default returns false, so an unrecognised policy stalls visibly rather than completing invisibly — the same fail-closed choice as 17.5 §19, and for the same reason: the failure mode of completing too early is unbounded.

Contract. The state machine retires on this and on nothing else. Any other retirement path is a second, unpoliced definition of completion.

Failure. §36. Also deriving the policy from a live configuration table rather than the entry's captured class, which changes an operation's completion rule underneath it if the configuration commits mid-flight.

DV. Cover all three policies; assert the fail-closed default is unreachable in a valid configuration.

36. Wrong RTL — One Universal Completion Event

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — one rule for every protocol.
assign sem_done[i] = response_received[i];
Protocol classWhat this reportsWhat is actually true
a class whose operations complete on a responsecorrectcorrect
a class needing all parts and an acknowledgementcompletethe acknowledgement has not arrived
a posted class expecting no response at allnever completeit completed long ago — the entry leaks

Four properties.

It is correct for exactly one class and applied to all of them. A design that starts with one protocol and later adds another inherits a silent bug from a line that was right when written — the same shape as 17.5 §20's single completion bit.

Row 3 is a resource leak rather than a correctness error. A posted operation never completes, so its entry and its identity are never freed. The table fills with entries that will wait forever, and the symptom is that the engine stops accepting — long after, and with no connection to the cause.

Row 2 is a correctness error. The client is told the operation finished before it did.

And the two failures look nothing alike, so a design with both classes active exhibits a leak and a premature completion and they are debugged separately.

37. Backpressure to the Client

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Every resource the operation will need, checked BEFORE the
// client's request is accepted (Section 17).
assign client_ready =
      sem_slot_available                    // a semantic entry (Section 20)
   && ingress_space_available[client_proto] // its own class's queue (Section 12)
   && route_valid                           // the destination is known
   && protocol_enabled[client_proto]        // Section 42
   && cfg_stable;                           // not mid-commit

Architecture. Five terms, and the Adapter's readiness is not among them. The Adapter's backpressure is absorbed by the ingress queue and by the semantic table's depth; propagating it directly to the client couples two pipelines that were separated on purpose.

State. None of its own; a conjunction over five facts owned by five mechanisms.

Cycle behaviour. Evaluated at acceptance and bound to the accepted operation rather than polled underneath work already accepted (11.5 §9).

Contract. Accepting a request promises an entry, a queue slot, a known destination and an enabled protocol. Each is independently able to be the constraint, and none is a proxy for another.

Failure. §39. Also using an aggregate ingress occupancy rather than the client's own class (17.4 §17).

DV. Force each term false alone — five directed tests.

38. Wrong RTL — a Combinational Ready Loop

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — readiness propagates combinationally through the whole pipeline.
assign client_ready  = adapter_ready;
assign adapter_ready = credit_available && !staging_full;
assign staging_full  = (staging_occ == DEPTH) && !ingress_pop;
assign ingress_pop   = ingress_valid && adapter_ready;      // ← back to the start
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
adapter_ready → staging_full → ingress_pop → adapter_ready

A combinational cycle.

Three properties.

Simulation may or may not converge, and if it does, the value it converges to is not a design decision. Synthesis will report a combinational loop, which is at least a loud failure — but a design that "fixes" it by breaking the loop at an arbitrary point has made a timing decision by accident.

Even without a literal cycle, a long combinational ready chain is a timing disaster. 13.3 §5 establishes that a global ready chain cannot exist; this is the RTL form of that argument.

The fix is a registered boundary, and the standard one is a skid buffer (§39) — which breaks the path while retaining the item that was in flight when readiness fell.

39. The Skid Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE one-entry skid buffer. Breaks the combinational ready path
// without losing the item already offered when readiness falls.
typedef struct packed {
  logic                 valid;
  protocol_object_t     obj;
} skid_t;
 
skid_t skid_q;
 
// Upstream sees a REGISTERED ready: we can take an item whenever the skid is
// empty, regardless of what downstream is doing this cycle.
assign up_ready = !skid_q.valid;
 
// Downstream is offered the skid entry if it holds one, else the bypass path.
assign dn_valid   = skid_q.valid || up_valid;
assign dn_obj     = skid_q.valid ? skid_q.obj : up_obj;
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    skid_q.valid <= 1'b0;
  end else begin
    unique case ({skid_q.valid, dn_ready})
      // Holding an item and downstream took it: skid empties.
      2'b11: skid_q.valid <= 1'b0;
      // Holding an item and downstream stalled: hold it, unchanged.
      2'b10: ;
      // Empty and downstream stalled: capture the offered item if there is one.
      2'b00: if (up_valid) begin
               skid_q.valid <= 1'b1;
               skid_q.obj   <= up_obj;
             end
      // Empty and downstream ready: the item passes through; nothing stored.
      2'b01: ;
      default: ;
    endcase
  end

Architecture. One entry, and the key property is in up_ready: it depends on the skid's own occupancy and on nothing downstream. That is what breaks the path.

State. One valid bit and one object. One entry is sufficient because at most one item can be in flight when readiness falls — the upstream saw up_ready last cycle and can have sent exactly one.

Cycle behaviour. Four cases, all enumerated. The 2'b00 arm is the one that matters: downstream is stalled and the skid is empty, so the offered item is captured rather than lost.

Contract. Upstream relies on up_ready being registered-equivalent; downstream relies on dn_obj being stable while it stalls (§40).

Failure. §41's shape — capturing only when up_valid && !dn_ready without also handling the already-holding case, which drops the second item. Or making up_ready depend on dn_ready, which reintroduces the very path the skid exists to break.

DV. §40's properties; cover all four cases, and cover back-to-back items with downstream toggling ready every cycle.

40. SVA — the Skid Never Loses or Duplicates

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Payload stable while the downstream stalls.
property p_skid_payload_stable;
  @(posedge clk) disable iff (!rst_n)
    (dn_valid && !dn_ready) |=> (dn_valid && $stable(dn_obj));
endproperty
a_skid_payload_stable: assert property (p_skid_payload_stable);
 
// Nothing accepted upstream is ever lost — conservation across the skid.
property p_skid_conservation;
  @(posedge clk) disable iff (!rst_n)
    (tb_items_in == tb_items_out + (skid_q.valid ? 1 : 0));
endproperty
a_skid_conservation: assert property (p_skid_conservation);
 
// Upstream readiness does not depend on downstream readiness (Section 38).
property p_up_ready_independent;
  @(posedge clk) disable iff (!rst_n)
    !skid_q.valid |-> up_ready;
endproperty
a_up_ready_independent: assert property (p_up_ready_independent);
 
// The skid never overflows.
property p_skid_no_overflow;
  @(posedge clk) disable iff (!rst_n)
    (up_valid && up_ready) |-> !skid_q.valid || dn_ready;
endproperty
a_skid_no_overflow: assert property (p_skid_no_overflow);

Architecture. Four properties: stability, conservation, independence, and overflow safety.

Why conservation is the valuable one. Stability and overflow catch structural errors; conservation catches an item silently dropped in the 2'b00 case, which is the skid's entire reason for existing and the one case a naive implementation gets wrong.

Why the third is written as an implication from emptiness. It says: whenever the skid can take an item, it says so — which forbids a downstream term sneaking into up_ready and re-creating §38.

DV. All four always-on; the second needs a testbench item counter.

41. Protocol Enable and Configuration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Requested-versus-active, for the fifth time in this curriculum
// (14.4 lanes, 17.2 memory map, 17.4 channels, 18.4 topology, and here).
logic [NUM_PROTO-1:0]   requested_enable_q;
logic [NUM_PROTO-1:0]   active_enable_q;
logic [CFG_EPOCH_W-1:0] active_cfg_epoch_q;
 
assign cfg_commit_allowed =
      requested_cfg_validated
   && (live_ops_on_disabled_classes == '0);   // nothing outstanding on a class
                                              // this commit would disable
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    active_cfg_epoch_q <= '0;
  end else if (cfg_commit_fire) begin
    active_enable_q    <= requested_enable_q;   // atomic
    active_cfg_epoch_q <= active_cfg_epoch_q + 1'b1;
  end
 
assign cfg_stable = !cfg_commit_pending;

Architecture. Two copies committed atomically, with a guard that no live operation belongs to a class this commit would disable.

Cycle behaviour. The enables transfer in one cycle and the epoch advances with them. A software write that disables a class does not take effect until the commit, so live traffic is unaffected.

Contract. client_ready reads active_enable_q (§37); the response matcher reads the captured epoch (§28). Disabling a class must not drop work already accepted under it — which is what the guard enforces.

Failure. Writing active_enable_q directly, which strands every live operation on the disabled class: no new objects can be formed for it, and its responses may be refused by a matcher that reads the live enable.

DV. §42; cover a commit attempted with live operations on a class being disabled.

42. SVA — Configuration Stability

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The active configuration changes only at a guarded commit.
property p_cfg_changes_only_at_commit;
  @(posedge clk) disable iff (!rst_n)
    $changed(active_enable_q) |-> $past(cfg_commit_fire);
endproperty
a_cfg_changes_only_at_commit: assert property (p_cfg_changes_only_at_commit);
 
property p_commit_requires_no_live_ops_on_disabled;
  @(posedge clk) disable iff (!rst_n)
    cfg_commit_fire |-> ($past(live_ops_on_disabled_classes) == '0);
endproperty
a_commit_requires_no_live_ops_on_disabled:
  assert property (p_commit_requires_no_live_ops_on_disabled);
 
// A live operation's captured epoch never changes under it.
property p_entry_epoch_stable;
  @(posedge clk) disable iff (!rst_n)
    sem_q[IDX].valid |-> $stable(sem_q[IDX].cfg_epoch);
endproperty
a_entry_epoch_stable: assert property (p_entry_epoch_stable);

Architecture. Three properties: commit-only changes, the guard, and per-operation epoch stability.

Why the third is separate. The first two protect the configuration; the third protects the operation — no live operation spans two epochs, which is the failure stated from the operation's point of view.

DV. Force a commit with live work on the affected class and confirm the second fires.

43. Recovery Interaction

A UCIe transport recovery is a transport event (14.2 §4). The semantic table does not participate in it.

StateEffect of a transport recovery
Semantic entriesuntouched — the operations are unfinished
Their generationsuntouched — resetting them destroys §22's protection
Captured destinations and epochsuntouched, and not recomputed
Pending-parts bitmapsuntouched — those parts are still owed
Free bitmapuntouched
Ingress queue contentsheld, not dropped
Skid buffer contentsheld
Protocol configurationuntouched — a link event does not change it
Adapter objects, replay entries, credits19.3's problem, rebuilt or resolved there

The engine does not reallocate, does not free, and does not duplicate a completion. It waits — because from its point of view nothing has happened except that responses are taking longer.

44. Wrong Recovery — Clearing the Semantic Table

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the engine treats a transport event as a reason to forget semantics.
always_ff @(posedge clk)
  if (ucie_recovery_entered)
    for (int i = 0; i < NUM_SEM_ENTRIES; i++) sem_q[i].valid <= 1'b0;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. Twelve operations are live. The link enters recovery.
2. This code clears all twelve entries and frees all twelve identities.
3. The Adapter, correctly, retains and replays its objects (19.3).
4. The link recovers. Responses arrive for all twelve.
5. Every one of them finds no live entry.
6. -> twelve orphan responses; twelve clients waiting forever.
7. Meanwhile the freed identities have been reallocated to new operations,
   so some of those orphans MATCH a live entry of the wrong generation —
   and only the generation check (Section 22) stops them completing it.

Four properties.

The Adapter is blameless and did exactly the right thing. It retained its objects, replayed them, and delivered the responses. The engine threw away the only structure that could receive them.

Step 7 is the reason the generation check exists. Without it, this bug would silently complete twelve wrong operations rather than producing twelve orphans. Orphans are a much better failure, and they are what the generation buys.

Every client hangs, and the cause is a recovery that reported success thousands of cycles earlier.

And it is the same lifetime error this curriculum has now seen at eight layers14.3 §15, 15.2 §15, 11.5 §14, 16.3 §13, 16.4 §26, 16.5 §11, 17.2 §28, 19.1 §9, and here. Every time, a semantic obligation was released by a transport signal.

45. SVA — the Semantic Table Survives Recovery

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The property that makes Section 44 impossible.
property p_sem_table_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    ucie_recovery_entered |=> ($stable(sem_q[IDX].valid)
                            && $stable(sem_q[IDX].generation)
                            && $stable(sem_q[IDX].destination)
                            && $stable(sem_q[IDX].pending_parts));
endproperty
a_sem_table_survives_recovery:
  assert property (p_sem_table_survives_recovery);
 
// The free bitmap is untouched by a transport event.
property p_free_bitmap_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    ucie_recovery_entered |=> $stable(free_q);
endproperty
a_free_bitmap_survives_recovery:
  assert property (p_free_bitmap_survives_recovery);
 
// A recovery produces no allocation and no completion.
property p_recovery_produces_no_semantic_event;
  @(posedge clk) disable iff (!rst_n)
    ucie_recovery_entered |=> ($stable(sem_alloc_count) && $stable(sem_retire_count));
endproperty
a_recovery_produces_no_semantic_event:
  assert property (p_recovery_produces_no_semantic_event);
 
// Queued and skid-held work survives too.
property p_queued_work_survives_recovery;
  @(posedge clk) disable iff (!rst_n)
    ucie_recovery_entered |=> ($stable(ing_occ_q[P]) && $stable(skid_q.valid));
endproperty
a_queued_work_survives_recovery:
  assert property (p_queued_work_survives_recovery);

Architecture. Four properties covering entries, identities, events and queued work.

Why the third is the sharpest. It says a recovery causes neither an allocation nor a retirement — which forbids both the clearing bug and its mirror image, a design that "helpfully" completes outstanding operations as failed when the link goes down.

DV. Inject a recovery with a full semantic table, non-empty ingress queues, a held skid entry and outstanding parts (§51).

46. Three Kinds of Error

KindExampleOwnerCorrect response
Semantic errora malformed or unexpected client requestthe enginefail the operation explicitly and report
Transport failurethe Adapter cannot deliver, permanently19.3fail the operation with the transport's reason
Semantic timeoutno response within a boundambiguous(12.4 §16) the remote state is unknown

One error bit cannot express three different situations with three different correct responses. A semantic error is the engine's own fault and is deterministic; a transport failure is another layer's and is reportable; and a timeout is not evidence of anything except that no response arrived.

47. First-Error Context

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. 14.5's first-fault model at the protocol engine. Write-once,
// and cleared only deliberately.
typedef struct packed {
  logic                    valid;
  logic [ERR_W-1:0]        error_kind;      // Section 46's three kinds
  logic [SEM_ID_W-1:0]     semantic_id;
  logic [GEN_W-1:0]        generation;
  proto_class_e            proto;
  sem_state_e              state_at_error;
  logic [DEST_W-1:0]       destination;
  logic [CFG_EPOCH_W-1:0]  cfg_epoch;
} sem_fault_t;
 
sem_fault_t sem_first_fault_q;
 
always_ff @(posedge clk or negedge por_n)                 // <- POR scope only
  if (!por_n)
    sem_first_fault_q <= '0;
  else if (sem_error_detected && !sem_first_fault_q.valid) // <- FIRST, guarded
    sem_first_fault_q <= capture_sem_context();
  else if (sem_fault_clear)
    sem_first_fault_q <= '0;

Architecture. Eight fields, chosen so that a post-silicon reader can reconstruct which operation, of which protocol, in which state, under which configuration hit the first error.

State. One record, in the power-on reset domain (19.1 §35) — so a transport recovery, a soft reset or a configuration commit cannot erase it.

Cycle behaviour. Guarded on !valid, so it captures the first error rather than the latest. The latest is almost always a consequence.

Failure. Using the local reset in the sensitivity list. Or omitting state_at_error, which is often the single most informative field — an error in SEM_ACCEPTED and one in SEM_PARTIAL have entirely different causes.

DV. Inject several errors and confirm the first is retained; confirm it survives a recovery.

48. Instrumentation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Diagnostic only, in the POR reset domain.
logic [63:0] ops_accepted_q   [NUM_PROTO];
logic [63:0] ops_completed_q  [NUM_PROTO];
logic [63:0] ops_failed_q     [NUM_PROTO];
logic [63:0] sem_full_stall_q;                 // no semantic entry available
logic [63:0] ingress_full_stall_q [NUM_PROTO];
logic [63:0] adapter_stall_q;                  // objects waiting for the Adapter
logic [63:0] orphan_resp_q;                    // Section 30
logic [63:0] stale_gen_resp_q;                 // Section 22
logic [63:0] duplicate_part_q;                 // Section 32
logic [63:0] class_wait_max_q  [NUM_PROTO];    // worst arbiter wait
CounterAnswersWithout it
sem_full_stall_qis the table the constraint, or the Adapter?a depth problem looks like a transport problem
orphan_resp_qis something freeing identities early?§21 and §44 are invisible until a client hangs
stale_gen_resp_qis the generation check actually firing?a dead check looks like a working one
duplicate_part_qhow often does the bitmap guard save us?§33's fix is unmeasured
class_wait_max_qis any protocol class starving?§15's bounds have no field evidence

Two properties.

orphan_resp_q is the highest-value counter here. It is the observable consequence of every identity-lifetime bug in the chapter, and a non-zero value in a correct design is impossible — so any count at all is a defect.

And sem_full_stall_q against adapter_stall_q is the pair that names the constraint. They look like one problem — "the engine is slow" — and have entirely different fixes.

49. The Protocol-Engine Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Verification-only. THREE models. Adapter and replay state belong to 19.3's
// scoreboard and are deliberately NOT modelled here.
class protocol_engine_scoreboard;
 
  // ---- Layer 1: SEMANTIC model — what the client asked for.
  typedef struct {
    int  semantic_id, generation;
    int  proto;
    int  destination;
    int  cfg_epoch;
    int  state;
    int  allocations;          // MUST be 1
    int  completions;          // MUST be <= 1
    int  completion_policy;
    bit  retired;
    bit  crossed_recovery;
  } sem_model_t;
  sem_model_t sem [int];       // keyed by {semantic_id, generation}
 
  // ---- Layer 2: OBJECTISATION model — what the operation SHOULD become.
  typedef struct {
    int        expected_parts;
    bit [63:0] parts_emitted;   // bitmap: which parts were handed over
    bit [63:0] parts_resolved;  // bitmap: which parts came back
    bit [63:0] required_fields; // Section 11 — what reconstruction needs
  } objectise_model_t;
  objectise_model_t obj [int];
 
  // ---- Layer 3: RESPONSE model — where each response should have gone.
  typedef struct {
    int  claimed_id, claimed_gen, claimed_proto;
    int  matched_key;            // what the design matched it to
    int  expected_key;           // what the model says it belongs to
  } resp_model_t;
  resp_model_t resp [int];
 
  // ---- Catches Section 10 — a distinction lost in normalisation.
  function void check_lossless(int key, bit [63:0] emitted_fields);
    if ((emitted_fields & obj[key].required_fields) != obj[key].required_fields)
      $error("NORMALISATION LOSSY key %0d: required %0h, emitted %0h (Section 10)",
             key, obj[key].required_fields, emitted_fields);
  endfunction
 
  // ---- Catches Sections 21 and 29 — a response bound to the wrong operation.
  function void check_response_binding(int r);
    if (resp[r].matched_key != resp[r].expected_key)
      $error("RESPONSE MISBOUND: matched key %0d, belongs to %0d (Sections 21, 29)",
             resp[r].matched_key, resp[r].expected_key);
  endfunction
 
  // ---- Catches Section 33 — retired with a part outstanding.
  function void check_parts(int key);
    if (sem[key].retired && (obj[key].parts_resolved != obj[key].parts_emitted))
      $error("RETIRED WITH PARTS OUTSTANDING key %0d: emitted %0h resolved %0h",
             key, obj[key].parts_emitted, obj[key].parts_resolved);
  endfunction
 
  // ---- Catches Sections 18 and 44 — allocation timing and survival.
  function void check_lifetime(int key);
    if (sem[key].allocations != 1)
      $error("OPERATION %0d allocated %0d times (must be 1)", key, sem[key].allocations);
    if (sem[key].crossed_recovery && sem[key].retired && (sem[key].completions == 0))
      $error("OPERATION %0d lost across a recovery (Section 44)", key);
  endfunction
 
endclass

Architecture. Three models keyed by operation, by operation, and by response.

Layer 2 tracks parts_emitted and parts_resolved as two separate bitmaps. A single "how many are left" field reproduces §33's bug exactly. Two bitmaps make "part 1 twice, part 3 never" immediately visible.

And Layer 2's required_fields is the reference model's statement of what reconstruction needs (§11) — derived from the protocol, not from the design, which is what makes §10 detectable at all.

Adapter and replay state are deliberately absent. They belong to 19.3's scoreboard, and collapsing the two would lose exactly the layering this chapter is about.

50. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_protocol_engine @(posedge clk);
  option.per_instance = 1;
 
  // --- Protocol mix (Sections 12-15).
  cp_proto : coverpoint accepted_proto { bins each[] = {[0:NUM_PROTO-1]}; }
  cp_simultaneous : coverpoint num_protos_requesting {
    bins one = {1}; bins two = {2}; bins all = {[3:$]};    // Section 13
  }
  cp_class_wait : coverpoint class_wait_ut {
    bins none = {0}; bins some = {[1:CLASS_BOUND_UT-1]}; bins at_bound = {CLASS_BOUND_UT};
  }
  cp_ingress_occ : coverpoint ing_occ_q_ut {
    bins empty = {0}; bins mid = {[1:ING_DEPTH-1]}; bins full = {ING_DEPTH};
  }
 
  // --- Semantic lifetime (Sections 16-23).
  cp_sem_state : coverpoint sem_state_q_ut { bins each[] = {[0:6]}; }
  cp_sem_occ : coverpoint sem_occupancy {
    bins empty = {0}; bins mid = {[1:NUM_SEM_ENTRIES-1]}; bins full = {NUM_SEM_ENTRIES};
  }
  cp_alloc_retire_simul : coverpoint alloc_and_retire_same_cycle;
  cp_id_wrap : coverpoint generation_wrapped;
  cp_event_in_stall_window : coverpoint semantic_event_while_adapter_stalled;  // Section 18
 
  // --- Responses (Sections 28-30).
  cp_resp_order : coverpoint response_reorder_distance {
    bins in_order = {0}; bins near = {[1:3]}; bins far = {[4:$]};   // Section 29
  }
  cp_resp_outcome : coverpoint response_outcome {
    bins matched = {0};
    bins orphan  = {1};
    bins stale_gen = {2};
    bins wrong_class = {3};
    bins wrong_epoch = {4};                 // all four rejection reasons
  }
 
  // --- Multi-part (Sections 31-34).
  cp_parts : coverpoint part_count {
    bins one = {1}; bins few = {[2:3]}; bins many = {[4:$]};
  }
  cp_part_event : coverpoint part_event {
    bins normal = {0}; bins duplicate = {1}; bins missing = {2}; bins stale_gen = {3};
  }
 
  // --- Completion policy (Sections 35-36).
  cp_policy : coverpoint active_policy {
    bins all_parts = {0}; bins all_parts_and_ack = {1}; bins posted = {2};
  }
  cp_policy_mix : coverpoint distinct_policies_active {
    bins one = {1}; bins several = {[2:$]};   // Section 36 needs >1
  }
 
  // --- Backpressure and configuration (Sections 37-42).
  cp_ready_refusal : coverpoint client_ready_false_reason {
    bins sem_full = {0}; bins ingress_full = {1}; bins route = {2};
    bins disabled = {3}; bins cfg_commit = {4};   // each term alone
  }
  cp_skid : coverpoint skid_case {
    bins pass_through = {0}; bins captured = {1}; bins held = {2}; bins drained = {3};
  }
  cp_cfg_context : coverpoint cfg_commit_context {
    bins idle = {0}; bins blocked_by_live_ops = {1}; bins forced = {2};
  }
 
  // --- Recovery (Sections 43-45).
  cp_recovery : coverpoint recovery_context {
    bins none = {0};
    bins table_nonempty = {1};
    bins parts_outstanding = {2};
    bins queues_nonempty = {3};
    bins all = {4};                          // THE case — Section 53
  }
 
  // --- Crosses that carry the information.
  x_simul_wait   : cross cp_simultaneous, cp_class_wait;     // Section 13
  x_parts_event  : cross cp_parts, cp_part_event;            // Section 33
  x_policy_mix   : cross cp_policy, cp_policy_mix;           // Section 36
  x_recovery_sem : cross cp_recovery, cp_sem_occ;            // Section 53
endcovergroup

Eight bins worth calling out:

cp_simultaneous.all. §13's precondition — every protocol class requesting at once, which a one-protocol-at-a-time regression never produces.

cp_event_in_stall_window. §18's blind spot: a cancellation or fault occurring while the Adapter is backpressured. It requires deliberate injection.

cp_resp_outcome — all five. The four rejection reasons exercised individually (§28), which is the "checked but not enforced" test.

cp_part_event.duplicate and .missing. §33's two cases.

cp_policy_mix.several. §36's bug is invisible with a single policy active, because one universal rule is correct when there is one rule.

cp_ready_refusal — every term. §37's five-term conjunction, each shown to gate alone.

cp_skid — all four cases. Especially captured, which is the case a naive skid drops.

And cp_recovery.all. §53's trace — a recovery with a non-empty table, outstanding parts and non-empty queues simultaneously. Every property in §45 depends on it.

51. Flagship Trace 1 — Two Protocols, Reordered Responses

Illustrative. PCIe and CXL clients request on the same cycle. Cycle numbers illustrative.

CycPCIe queueCXL queueGrantSem entryAdapterResponses
000ready
111PCIereadyboth clients accepted
111PCIeA alloc'd (id 3, gen 5)readyand B alloc'd (id 7, gen 2)
201CXLA: ACCEPTEDtakes A's object
300A: OBJECTS_OUTstallsB waits for the Adapter
400B: ACCEPTEDstalledB's entry exists — §18
4000both liveresumes
4100B: OBJECTS_OUTtakes B's object
44A, B: AWAIT_RESP
70B's response arrives first
71matched: id 7, gen 2, CXL ✓
72B: COMPLETEpolicy satisfied
95A's response arrives
96matched: id 3, gen 5, PCIe ✓
97A: COMPLETE

Six readings.

Cycle 1: both clients are accepted and both entries are allocated, even though only one object can be handed to the Adapter. §18's design allocates B at cycle 41, leaving a 37-cycle window.

Cycles 3 to 40: the Adapter is stalled and both entries are live. The engine's state is complete and queryable throughout — which is the entire benefit of the allocation point.

Cycle 70: B's response arrives first, though B was issued second. §29's design completes A with B's response here.

Cycles 71 and 96 match on four fields each — identity, generation, class and epoch. The class check is what would catch a CXL response landing on a PCIe entry.

Two protocols shared the engine without either blocking the other, because they had separate queues (§12). §13's design would have put B behind A's entire path.

And the arbiter granted PCIe then CXL, advancing its pointer only on the actual transfer at cycles 2 and 41.

52. Flagship Trace 2 — Recovery With Live Operations

CycSemantic tableParts pending (op A)IngressSkidLinkMust be true
1009 live{0,1,1} — parts 1,2 owed3heldoperational
1049 live{0,1,1}3helderror detected
1059 live{0,1,1}3heldrecovery enterednothing semantic changes
1109 live{0,1,1}3heldrecoveringno allocation, no retirement
1409 live{0,1,1}3heldrecoveringgenerations untouched
1609 live{0,1,1}3heldrecovered, degradedcapacity changed only
1689 live{0,1,1}3heldoperationalAdapter replays its objects
1809 live{0,0,1}2operationalpart 1 resolves
1849 live{0,0,1}2operationalpart 1 REPLAYED — bit already clear
2059 live{0,0,0}1operationalpart 2 resolves
2068 live1operationalA completes; identity freed

Six readings.

Cycle 105: nine entries, nine preserved. The generations, destinations, epochs and part bitmaps are all untouched. §44's design clears all nine here and produces nine orphans at cycle 180 onward.

Cycle 110's requirement is the sharpest: a recovery causes neither an allocation nor a retirement (§45's third property). A design that fails outstanding operations on a link event violates it just as surely as one that clears them.

Cycle 184: a replayed part finds its bit already clear and changes nothing. §33's counter decrements here and retires the operation with part 2 still owed.

Cycle 168: the Adapter replays. That is 19.3's machinery working correctly, and the engine neither knows nor cares — from its point of view responses were simply slow.

Cycle 206 frees the identity at completion, not at cycle 180 when the first part resolved. §21's design frees it early and opens the aliasing window.

And the link returned degraded at cycle 160. Not one semantic field changed; operations completed more slowly and correctly.

53. Debug Taxonomy

SignatureMost likely causeFirst instrument
Adapter clean, the client hangs§36 or §44 — a completion policy that never fires, or a cleared tablesem_state_q of the stuck entry; orphan_resp_q
Only simultaneous protocols fail§13 — a shared FIFO before classificationis there a queue per class? class_wait_max_q
A late response completes a new request§21 — the identity freed earlyis the free bitmap set at completion or at Adapter acceptance?
Responses swapped between clients§29 — matched by arrival orderis the match on identity, generation, class and epoch?
A recovery produces orphan responses§44 — the semantic table clearedwhat changed at the recovery cycle
Streaming load blocks control traffic§13, §14 — no per-class queue or no boundclass_wait_max_q per class
Payload valid but interpreted wrongly at the far end§10 — normalisation lost a distinctionreference model's required-fields check
An operation retires with data missing§33 — a part counter rather than a bitmapduplicate_part_q; parts emitted vs resolved
The engine stops accepting after a long run§36's posted case — entries never freedsem_occupancy; which policy the stuck entries use
A cancellation has no effect§18 — no entry exists yetwas the entry allocated at client acceptance?
Intermittent hang with an entry stuck in one state§26 — two writers to the statedid the committed state equal the next-state function?

Row 9 is the one that takes longest to find. The engine stops accepting after a long run is a slow leak of semantic entries, and only the occupancy counter plus the per-entry policy identifies which class is leaking.

54. Debug Checklist

  1. Which semantic operation — identity and generation? (§22)
  2. Which protocol class? (§7)
  3. What state is the entry in, and for how long? (§24)
  4. Did the committed state equal the next-state function's output? (§27)
  5. When was the entry allocated — at client acceptance or at Adapter acceptance? (§17)
  6. Is the free bitmap consistent with the valid bits? (§23)
  7. What is the semantic-table occupancy? (§48)
  8. Which arbiter class won, and what is each class's worst wait? (§14)
  9. Which ingress queue, and what is its occupancy? (§12)
  10. Was the object's destination and epoch captured at acceptance? (§8)
  11. Does the normalised object carry every field the far end needs? (§11)
  12. How many parts did this operation declare, and which are still pending? (§32)
  13. Did any part arrive twice, or with a stale generation? (§34)
  14. Which completion policy applies to this class? (§35)
  15. Was the policy satisfied, and by which events? (§35)
  16. Did the response match on all four fields? (§28)
  17. Are there any orphan responses, and how many? (§30, §48)
  18. Did a transport retry or recovery occur while the operation was live? (§43)
  19. Did the table, the bitmaps and the queues all survive it? (§45)
  20. Was a configuration commit attempted with live work on an affected class? (§41)
  21. Which of the five client_ready terms was false? (§37)
  22. Did the skid buffer hold an item across the stall? (§39)
  23. What does the first-error record say — kind, class, state, epoch? (§47)
  24. Which of the three scoreboard layers diverged first? (§49)

55. Common Misconceptions

"A protocol engine is just packet formatting." It owns the semantic operation's entire lifetime — allocation, identity, generation, multi-part completeness, per-protocol completion, and survival across every transport event. Formatting is one stage of one path (§4, §16).

"One generic READ/WRITE representation is always enough." Only if every distinction the far end needs survives it. Two operations normalised to the same internal type cannot be told apart on reconstruction, and the far end applies one protocol's rule to the other's operation — with perfect transport throughout (§10).

"Semantic state can be allocated when the Adapter accepts." The client was told yes long before. Between the two there is a window as long as the Adapter's backpressure — exactly when the system is busiest — in which an accepted operation has no record, cannot be cancelled, cannot be failed, and is invisible to recovery (§18).

"TX acceptance means the protocol transaction is complete." It means one object left. The operation may have several parts outstanding, may need an acknowledgement, and completes only when its own protocol's rule is satisfied (§4, §35).

"Responses return in request order." They do not, for at least three independent reasons, and matching by position hands each client the other's response with clean transport and correct bytes throughout (§29).

"One shared FIFO maximises utilisation." It maximises head-of-line blocking. A streaming burst delays a latency-sensitive completion behind all of it, and if that completion releases a resource the burst needs, the design deadlocks (§13).

"A counter is enough for multi-part completion." A duplicated part — a correct transport retry — decrements it and retires the operation with a part still owed. A guarded bitmap is idempotent by construction, for the sixth time in this curriculum (§33).

"Transport retry should reallocate protocol state." A retry re-sends the same object. Reallocating creates a second semantic operation from a mechanism that was working correctly (§19, §43).

"Protocol ID and replay ID are the same thing." Different namespaces owned by different layers with different lifetimes. The Adapter recycles its identities on its own schedule, typically while the semantic operation is still live (19.1 §7).

"All protocols can use the same completion event." One rule is correct for at most one class. Applied universally it completes one class early and leaks another's entries forever — two failures that look nothing alike (§36).

"Adapter readiness is enough to drive client ready." It is not one of the five terms. Accepting a request requires a semantic entry, a class queue slot, a known destination, an enabled protocol and a stable configuration — and coupling the client directly to the Adapter builds the combinational loop a skid buffer exists to break (§37, §38).

"Recovery should flush protocol state." It should change nothing semantic. The Adapter retains and replays its objects correctly, and the engine that cleared its table produces one orphan per live operation and one hung client per orphan (§44).

56. Understanding Check

57. Summary and What Comes Next

A protocol engine converts a semantic obligation into transportable objects without handing the obligation to the transport. It owns the operation until the higher-level protocol says it is complete.

Normalisation exists so the Adapter never learns a protocol — and its characteristic failure is discarding a distinction the far end needs, with perfect transport throughout.

Allocate at client acceptance. Allocating at Adapter acceptance opens a window as long as the backpressure, in which an accepted operation cannot be cancelled, failed, recovered or counted.

Identity plus generation, freed at completion. An identity returned early lets a late response complete a new operation — possibly of a different protocol.

Responses match on four fields and never on order, because reordering has at least three independent causes and the transport is blameless in all of them.

A multi-part operation needs a bitmap. A duplicated part — a correct transport retry — retires a counter-based operation with work still owed, and the missing part then arrives at a freed entry.

Completion is per protocol. One universal rule completes one class early and leaks another's entries forever.

Client readiness is five terms, and the Adapter's is not among them — coupling them directly builds the combinational loop the skid buffer exists to break.

And a transport recovery changes nothing semantic. No allocation, no retirement, no cleared table — because the Adapter is already retaining and replaying everything correctly.

The engines have now converted semantic work into stable transport objects and handed them across FDI. The next chapter owns those objects: admitting them only when they can be retained, protecting them, keeping a replayable copy until the far end confirms it no longer needs one, suppressing duplicates, and delivering exactly once across a physical layer that will not always cooperate.

Browse the full path on the UCIe tutorials index.