UCIe · Module 19
Reusable UCIe IP
Designing UCIe blocks for reuse across products — why the four kinds of configuration must never be mixed, why a block that compiles in a configuration nobody designed is the dangerous one, why an interface that exposes an internal signal name has no stable contract, why the reset matrix is a deliverable rather than an integration detail, and why a testbench that assumes the default configuration fails before the design does.
Chapters 19.1 to 19.5 built a link: a top-level architecture, protocol engines, an Adapter, buffers, and a credit machine. Every one of them was built for one product. This chapter asks what has to change when the next product wants a different lane count, a different protocol mix, a different queue depth, a different clocking topology and a different feature set — and what has to not change.
1. The One-Sentence Model
Reusable IP is not configurable RTL. It is a stable contract whose legal configurations are enumerated, checked, and independently verifiable — so the dangerous block is not the one that fails to compile in a new configuration, but the one that compiles cleanly in a configuration nobody designed.
That framing decides everything below. A parameter is not a feature; it is a claim that the block works across a range. A claim nobody checked is a defect with a delivery date, and the whole of this chapter is the machinery for making the claim true: legality functions, elaboration assertions, envelope tables, reset matrices, boundary contracts, and a verification collateral package that ships with the RTL.
2. What This Chapter Owns
| Question | Where it is answered |
|---|---|
| The link's block partition, state ownership, reset hierarchy | 19.1 |
| Protocol-engine internals, normalisation, semantic tables | 19.2 |
| Adapter internals — admission, staging, replay, integrity | 19.3 |
| Buffer structures, depths, watermarks, ping-pong, CDC FIFOs | 19.4 |
| The credit machine, its arithmetic, epochs and liveness | 19.5 |
| Verification methodology, monitors, reference models | 20.1 — Protocol Verification |
Those five chapters designed the blocks. This chapter designs the boundaries and the configuration space around them — and the distinction is what makes this an architecture chapter rather than a coding-style chapter:
Four kinds of configuration that must not be mixed (§4–§6), because putting a structural quantity in a runtime register costs area in every product and putting a policy value in a parameter costs a silicon revision.
The legal-configuration problem (§10–§13). A parameter range is a claim; §11 is the configuration that compiles, elaborates, simulates a smoke test, and loses a transport object on the first error.
Interfaces that survive an internal redesign (§16–§18), which is the difference between an IP block and a copy-paste source directory.
A reset contract as a deliverable (§26–§29), because the most common integration failure of a reusable block is not algorithmic.
Reuse by contract rather than by shape (§36–§38) — why a replay buffer and a FIFO must not be the same module even though both store data.
And a flagship parameterisation bug (§45) that works perfectly in the SKU it was written for and silently corrupts the next one.
3. Sourcing
4. Four Kinds of Configuration
The most valuable single table in this chapter. Every reuse failure below is, at root, a value placed in the wrong row.
| Kind | Examples | Bound at | Changing it costs | Wrong home symptom |
|---|---|---|---|---|
| Structural | lane count, data width, queue depth, number of protocol classes, number of ports, replay depth | elaboration | a synthesis run | in a register: area and timing in every product (§5) |
| Static configuration | which protocols are enabled, requested width and rate, feature enables | before operation, by software or straps | a quiesce and a commit | in a parameter: a silicon spin per SKU (§6) |
| Dynamic operational state | active width after degradation, recovery state, credits, current epoch | continuously, by hardware | nothing — it is the machine | in a config register: software races the hardware |
| Product policy | retry attempt budget, arbitration weights, timeout values, watermark thresholds | before operation, tunable | a register write | in a parameter: firmware cannot tune the field (§6) |
Three rules follow, and each one is a review question.
Structural quantities determine what hardware exists. If a value changes how many flops are instantiated, it is a parameter. Making it runtime-selectable does not give you flexibility — it gives you the maximum hardware in every product plus a multiplexer (§5).
Static configuration and product policy look identical and are not. Both are register-programmed before operation. The difference is who owns the number: static configuration is negotiated with the link or dictated by the system's topology, while policy is a tuning knob the product team owns. Putting policy in a parameter is the mistake that generates a new RTL release for every customer (§6).
And dynamic state is not configuration at all. 19.5 §36's active capacity, 19.1 §36's active configuration and 19.3 §47's configuration epoch are all hardware-owned. A register that software can write and hardware can also write is a race, and §23 is what that race does to a datapath.
The review question, in one sentence. For every configurable value in the block: does changing it change what hardware exists, what the link agreed to, what the hardware is currently doing, or what the product chose? If two engineers give different answers, the value is in the wrong row.
5. Wrong Architecture — Everything Is a Runtime Register
// WRONG — a structural quantity made runtime-selectable.
logic [6:0] num_lanes_cfg_q; // "supports 1 to 64 lanes, software selectable"What this actually builds. Synthesis must instantiate the datapath, the per-lane deskew, the per-lane training state and the per-lane repair logic for 64 lanes, because any of them may be selected at runtime. A product that ships an x8 link pays the area, the leakage and the timing closure of an x64 link — permanently, in every die.
Four consequences, in increasing order of how long they take to discover.
The area cost is immediate and visible. It is also the one that gets caught, because somebody looks at the synthesis report.
The timing cost is worse and less visible. The lane-select multiplexer sits in the datapath, and its depth grows with the maximum lane count rather than the used one. The x8 product misses timing because of lanes it does not have, and the fix looks like a datapath problem rather than a configuration-architecture problem.
Most of the "configurations" are physically impossible. A package with 8 bumps cannot be configured to 64 lanes. The RTL claims a range the product cannot reach, so a verification plan that covers the parameter's range spends most of its cycles on states no silicon can enter — and, worse, the states it cannot reach are not marked as unreachable anywhere.
And the verification state space is multiplied for nothing. Every assertion, every coverage cross and every reset scenario now has a lane-count dimension with 64 values instead of one. §35's coverage matrix is what that costs.
The correct split, and it is not "no runtime lane state". Lane count is structural; the active lane mask after degradation or repair is dynamic state (§25). A block needs both, and conflating them in one register is what produces this bug.
6. Wrong Architecture — Everything Is a Parameter
// WRONG — a policy value frozen at elaboration.
parameter int ARB_WEIGHT_BULK = 3;
parameter int ARB_WEIGHT_CTRL = 1;
parameter int RETRY_BUDGET = 4;
parameter int STALL_TIMEOUT = 4096;The mirror error, and the one that costs more over a product's life.
What goes wrong. A customer's traffic pattern needs the bulk weight at 2, not 3. The value is a parameter. The response is an RTL change, a synthesis run, a timing closure, a full regression, and a new IP release — for a number that could have been a register write.
Four properties.
The cost is recurring. Every product, every customer, every workload characterisation produces a new tuning request, and each one is a release.
It arrives after tape-out, which is the expensive moment. Arbitration weights, retry budgets and timeouts are exactly the values nobody can pick correctly before silicon, because they depend on real traffic and real latencies. Freezing them at elaboration guarantees they are frozen at the wrong values, since the information needed to choose them does not exist yet.
Timeout values in particular must be tunable. 19.5 §43 required that the credit flush timeout be shorter than the peer's stall timeout. The peer is not known at elaboration. A parameter here converts a cross-die relationship into a compile-time assumption about a die somebody else designed.
And the "just re-elaborate" answer does not survive an ecosystem. The entire premise of a die-to-die standard is that dies come from different teams and different companies. A block whose behaviour can only be tuned by rebuilding it cannot be delivered as hardened IP — it can only be delivered as source, which is a different product with different economics.
The test. If the value's correct setting depends on something outside this die — the peer's timeouts, the system's traffic mix, the customer's workload — it is policy and it belongs in a register. If it changes how many flops exist, it is structural. Nothing is both.
7. The Reusable Block, Drawn
Read the four surfaces, not the five internal blocks. The internal blocks are Chapters 19.1 to 19.5 and this chapter changes none of them. What this chapter adds is that each of the four surfaces has a different lifetime, a different owner and a different failure mode — and that a block delivering only the first two surfaces is source code, not IP.
Note where the verification contract sits. It is outside the implementation and alongside it, not inside it. An integrator who has to write their own boundary monitors has been given RTL and asked to re-derive the contract — which they will get subtly wrong, in a way that shows up as an integration bug blamed on the IP.
8. The Parameter Package, and Why Global Packages Couple
// ILLUSTRATIVE SKETCH ONLY — this file is not created in this repository (§2).
// A global parameter package. Convenient, and it has a real cost (below).
package ucie_ip_params_pkg;
parameter int NUM_LANES = 16;
parameter int DATA_W = 256;
parameter int TX_DEPTH = 16;
parameter int RX_DEPTH = 16;
parameter int NUM_CLASSES = 3;
parameter bit REPLAY_ENABLE = 1'b1;
parameter int REPLAY_DEPTH = 32;
endpackageWhat a global package buys. One place to look, no parameter threading through six levels of hierarchy, and consistent widths everywhere without a chain of #(...) overrides that is easy to get wrong.
What it costs, and this is the part that matters for reuse.
One instance per compilation unit. A package parameter is a single value for everything that imports it. Two instances of the block with different depths in the same design cannot both use the package — and "two instances with different configurations" is the defining case of reusable IP, not an exotic one. A multi-link package (18.4) has exactly this shape.
It creates an invisible dependency. A module that imports the package depends on it without saying so in its port list or parameter list. The dependency is not visible at the instantiation site, so an integrator who overrides TX_DEPTH at the top level and finds nothing changed has to read the source to discover why.
And it defeats parameter mutation testing (§52). If the testbench and the design both import the same package, changing the package changes both, and the testbench can never be caught assuming a default — which is §53's failure hiding behind the convenience.
The pragmatic split, and it is what most good IP does. Use a package for types and derived functions — the struct definitions, the enumerations, the legality function of §13 — and use module parameters for the values that must vary per instance. Types are genuinely global; depths are not.
9. Parameter-Derived Widths
// ILLUSTRATIVE. Three width expressions that look interchangeable and are not.
// Getting these wrong is 19.5 Section 11's bug, generalised to every structure.
// An INDEX into DEPTH entries: DEPTH distinct values, 0..DEPTH-1.
localparam int TX_PTR_W = (TX_DEPTH <= 1) ? 1 : $clog2(TX_DEPTH);
// A COUNT of occupancy: DEPTH+1 distinct values, 0..DEPTH.
localparam int TX_OCC_W = $clog2(TX_DEPTH + 1);
// A WRAP-SAFE pointer: one bit above the index, so full and empty differ.
localparam int TX_WPTR_W = TX_PTR_W + 1;
// A CLASS index: NUM_CLASSES values, and NUM_CLASSES may legitimately be 1.
localparam int CLS_W = (NUM_CLASSES <= 1) ? 1 : $clog2(NUM_CLASSES);Four expressions, four different rules, and each one has a failure mode.
TX_PTR_W needs the guard because $clog2(1) is 0, and a zero-width declaration is a tool-dependent error or a silent zero-bit signal. A single-entry queue is a legitimate configuration — it is what a skid buffer is (19.4 §19) — so this is not a defensive edge case, it is a supported one.
TX_OCC_W must not have that guard. $clog2(1 + 1) is 1, which is correct, and adding the (<= 1) ? 1 : wrapper is harmless here. What is not harmless is copying TX_PTR_W's expression and forgetting the + 1 — which is 19.5 §11's truncated credit counter, arriving in a different structure.
TX_WPTR_W is the derived-occupancy discipline from 19.4 §9: one extra bit so that a wide-pointer subtraction distinguishes full from empty without a separate counter that can drift.
And CLS_W needs the guard for the same reason as the pointer, with a sharper consequence: a single-class configuration is a common product choice — one protocol, one resource pool — and it is the configuration in which every $clog2(NUM_CLASSES) in the design becomes zero-width simultaneously. §46 is that failure in full.
The rule. Write each width expression once, in one
localparam, with a comment naming what it counts. Every place the design needs that width refers to thelocalparam. An inline$clog2in a port declaration is a copy that a later parameter change will not follow.
10. Elaboration Assertions Are the Cheapest Verification in the Chapter
// ILLUSTRATIVE. Elaboration-time legality. These cost nothing at runtime, run
// before simulation starts, and fail with a message naming the parameter.
initial begin : g_param_check
// Basic existence
assert (NUM_LANES > 0) else $fatal(1, "NUM_LANES must be > 0");
assert (TX_DEPTH > 0) else $fatal(1, "TX_DEPTH must be > 0");
assert (RX_DEPTH > 0) else $fatal(1, "RX_DEPTH must be > 0");
assert (NUM_CLASSES > 0) else $fatal(1, "NUM_CLASSES must be > 0");
// Relationships between parameters — the ones a range check cannot express
assert (DATA_W % NUM_LANES == 0)
else $fatal(1, "DATA_W=%0d not a multiple of NUM_LANES=%0d", DATA_W, NUM_LANES);
// Feature dependencies (§12)
assert (!REPLAY_ENABLE || (REPLAY_DEPTH > 0))
else $fatal(1, "REPLAY_ENABLE with REPLAY_DEPTH=0 (§11)");
assert (!REPLAY_ENABLE || INTEGRITY_ENABLE)
else $fatal(1, "REPLAY_ENABLE requires INTEGRITY_ENABLE in this architecture");
// Cross-block relationships that 19.4 and 19.5 derived rather than chose
assert (RX_HIGH_WATER <= RX_DEPTH - PRODUCER_PIPE_DEPTH)
else $fatal(1, "watermark leaves insufficient headroom (19.4 Section 29)");
assert (FLUSH_TIMEOUT < PEER_STALL_TIMEOUT)
else $fatal(1, "credit flush is slower than the peer's patience (19.5 Section 43)");
// Derived-width sanity — catches a hand-edited localparam
assert (TX_OCC_W >= $clog2(TX_DEPTH + 1))
else $fatal(1, "TX_OCC_W cannot represent TX_DEPTH");
endArchitecture. A single initial block of static checks, evaluated once, before any stimulus.
Why this is the highest-value verification per line in the chapter. A parameter defect produces legal-looking hardware. It does not X-propagate, it does not assert, and it frequently does not fail the smoke test. The only place the information needed to detect it exists is at elaboration, where the parameters are known and the hardware has not yet been built. After elaboration the evidence is gone.
Four categories worth checking, and the last two are the ones teams miss.
Existence — every structural parameter is positive. Trivial, and it catches an unconnected override that defaulted to zero.
Relationships — a width that must divide, a depth that must exceed a latency. These cannot be expressed as a range on any single parameter, which is why an "each parameter has a documented range" policy is insufficient.
Feature dependencies — §12. REPLAY_ENABLE with REPLAY_DEPTH == 0 is §11.
And cross-block relationships that earlier chapters derived. 19.4 §29's headroom equation and 19.5 §43's timeout ordering are both relationships between numbers owned by different blocks. Those chapters said the relationship belongs in an elaboration check rather than a comment; this is where that promise is kept. A comment is not checkable and does not survive the parameter being overridden by an integrator who never read it.
Failure. $fatal rather than $error, deliberately. An illegal configuration must not proceed to simulate, because a simulation that runs produces results, and results from an illegal configuration are worse than no results — somebody will act on them.
11. Wrong Parameter Combination — Replay Enabled, Depth Zero
// WRONG — the configuration is accepted, elaborates, and builds nothing.
parameter bit REPLAY_ENABLE = 1'b1;
parameter int REPLAY_DEPTH = 0; // set by an integrator "to save area"
generate
if (REPLAY_ENABLE) begin : g_replay
replay_entry_t replay_q [REPLAY_DEPTH]; // zero entries
// ... allocation, retirement, resolution logic, all present
end
endgenerateWhat actually happens, in order.
It elaborates. A zero-entry unpacked array is legal in most tools. The generate block is taken. Every module inside it is instantiated.
It compiles and simulates. The replay control logic exists and runs. Pointers advance. Nothing indexes anything, because nothing is ever allocated — replay_slot_available is permanently false.
The smoke test passes. With no errors injected, no object is ever retransmitted, so the replay path is never exercised.
And 19.3 §10's admission conjunction now refuses everything. Recall that the Adapter's admission requires replay_slot_available. With zero slots the term is always false, so either the link accepts nothing at all — a total stall at bring-up — or, in a design where the term was omitted, an object is accepted with nobody owning a recoverable copy and the first error loses it permanently.
Four properties.
Both outcomes are bad and they look completely different. A total stall is caught in the first hour; a lost object on the first error is caught in the field. Which one you get depends on whether 19.3's admission discipline was followed — so the quality of the Adapter determines the symptom of a parameter bug.
No individual parameter is out of range. REPLAY_ENABLE is a valid bit. REPLAY_DEPTH of zero is a valid integer and is exactly right when replay is disabled. The illegality is in the pair, and per-parameter documentation cannot express it.
The integrator's reasoning was sound. They wanted the area back and set the depth to zero. Nothing told them that was not how to disable the feature — and "read the integration guide" is not a mechanism, it is a hope.
And this generalises far beyond replay. Zero-depth reassembly with fragmentation enabled, zero classes with per-class arbitration enabled, a CDC FIFO with matched clocks and synchroniser stages set to zero — every optional structure has a "present but empty" configuration, and each one needs the paired check.
12. The Feature Dependency Graph
Enumerate the dependencies once, in one place, and derive both the elaboration checks and the runtime legality function from it.
ILLUSTRATIVE for one architecture. NOT a UCIe-defined dependency set (§3).
REPLAY_ENABLE
requires INTEGRITY_ENABLE -- retransmit what, on whose verdict?
requires REPLAY_DEPTH > 0 -- §11
requires DUP_WINDOW > 0 -- a retransmission can duplicate (19.3 §32)
MULTI_PROTOCOL_ENABLE
requires NUM_CLASSES > 1 -- arbitration needs something to arbitrate
requires ARB_ENABLE -- the Arb/Mux function
FRAGMENTATION_ENABLE
requires REASM_DEPTH > 0 -- fragments must land somewhere
requires REASM_DEPTH >= MAX_CONCURRENT_OBJECTS -- 19.4 §41
ASYNC_PHY_BOUNDARY
requires CDC_STAGES >= 2 -- one stage is not a synchroniser
requires CDC_FIFO_DEPTH > CDC_STAGES + 2 -- 19.4 §47
DEGRADED_WIDTH_ENABLE
requires LANE_REPAIR_ENABLE || WIDTH_NEGOTIATE_ENABLEThree things this graph is for, and only the first is obvious.
It generates the elaboration assertions of §10, mechanically, with no judgement required at the point of writing them.
It generates the runtime legality function of §13, which validates a static-configuration write before it is committed. The same graph, two consumers — which is what stops the two from drifting apart, and drifting apart is what produces a block that rejects at elaboration what it accepts at runtime.
And it tells the verification plan which combinations are illegal, so §35's coverage matrix does not spend cycles trying to cover configurations that must fail — and, more usefully, so §51's invalid-neighbour tests know exactly which neighbours to try.
13. The Configuration Legality Function
// ILLUSTRATIVE. One function, two consumers: the runtime validator and — as an
// INDEPENDENTLY WRITTEN COPY — the verification reference model (§53).
function automatic logic legal_cfg(ip_cfg_t cfg);
logic ok;
ok = 1'b1;
// Structural capability: a request may not exceed what was built.
if (cfg.req_lanes > NUM_LANES) ok = 1'b0;
if (cfg.req_classes > NUM_CLASSES) ok = 1'b0;
// Feature dependencies (§12)
if (cfg.replay_en && !REPLAY_ENABLE) ok = 1'b0; // not built
if (cfg.replay_en && !cfg.integrity_en) ok = 1'b0;
if (cfg.multi_proto && (cfg.req_classes < 2)) ok = 1'b0;
if (cfg.frag_en && !FRAGMENTATION_ENABLE) ok = 1'b0;
// Enumerated legality: not every value in a field's range is supported.
if (!(cfg.req_lanes inside {1, 2, 4, 8, 16, 32})) ok = 1'b0;
return ok;
endfunctionArchitecture. A pure function of a configuration struct, with no side effects and no dependence on current state — which is what makes it usable by the commit FSM of §22, by a software model, and by an assertion.
Why the enumerated check matters more than the range checks. A field wide enough to encode 0 to 63 does not mean 63 values are supported. §34's envelope table is the authority for which values are supported, and this function is that table in executable form. A block that accepts any value the field can hold has claimed a range it never verified.
Why the structural checks come first. cfg.replay_en with REPLAY_ENABLE == 0 is a request for a feature that does not exist in this silicon. That is a different failure from an illegal combination of features that do exist, and the error class reported to software should differ (§39) — one is a software bug, the other is a wrong part number.
The verification rule, and it is absolute.
The testbench must not call this function. 19.5 §56's argument applies exactly: a reference model that computes expected legality by calling the design's own legality function agrees with the design about every configuration, including the ones the function gets wrong. The verification environment needs an independently written model of legality, derived from the envelope table rather than from the RTL — and where the two disagree, one of them is a bug and it is not always the RTL.
14. Generate Blocks, and What Must Not Change
// ILLUSTRATIVE. A structural difference, with a STABLE external boundary.
generate
if (REPLAY_ENABLE) begin : g_replay
ucie_replay_ring #(
.DEPTH (REPLAY_DEPTH)
) u_replay (
.clk, .rst_n,
.alloc_req, .alloc_gnt, .retire_req,
.slot_available (replay_slot_available),
.replay_active (replay_active)
);
end else begin : g_no_replay
// The boundary is IDENTICAL. Only the semantics differ.
assign replay_slot_available = 1'b1; // nothing to reserve
assign replay_active = 1'b0; // never retransmitting
assign alloc_gnt = alloc_req;
end
endgenerateArchitecture. Two implementations, one boundary. The else branch is not empty — it implements the contract for the disabled case.
Why the tie-offs are the interesting part. replay_slot_available = 1'b1 is a statement that admission need not reserve a replay slot when there is no replay, which is exactly right and is not what a naive else branch produces. A branch that leaves the signal undriven produces X; a branch that ties it to zero produces §11's total stall. The disabled case has a correct value and it must be chosen deliberately, per signal.
What must never differ between the branches:
- the set of signals driven — every signal driven in one branch is driven in the other;
- the set of signals consumed — a disabled branch that stops consuming an input leaves that input unloaded, which changes synthesis and can silently delete upstream logic;
- the reset behaviour of anything the branch owns (§26);
- the timing class of each output — a combinational output in one branch and a registered one in the other changes the integrator's timing closure (§32).
Failure. §15.
DV. Elaborate and run the full regression in both branches. A feature-disabled build is not a subset of a feature-enabled build — it is a different design and needs its own regression, which is why §35's matrix has an on/off axis for every optional feature.
15. Wrong Generate — the Signal That Disappears
// WRONG — the boundary changes with the configuration.
generate
if (REPLAY_ENABLE) begin : g_replay
// ... drives replay_status, replay_depth_used, replay_attempts
end
// no else branch at all
endgenerateWhat happens at the integrator. The replay-disabled build has replay_status, replay_depth_used and replay_attempts undriven. Depending on the integration, that is an X into a status register, a synthesis warning nobody reads, or a compile error in the wrapper.
The integrator's fix is the real damage. They wrap the connections in their own ifdef:
// The integrator's wrapper, now feature-specific — this is the actual cost.
`ifdef REPLAY_BUILD
.replay_status (link_replay_status),
`endifFour consequences.
There are now two wrappers to maintain, and the number doubles with every optional feature. Three optional features produce eight wrapper variants, and nobody maintains eight wrappers; they maintain one and let the others rot.
The IP boundary is no longer a contract. The whole value of reusable IP is that the integration is written once. A boundary that changes shape per configuration has to be re-integrated per configuration, which is most of the cost the IP existed to remove.
Verification collateral breaks too. §48's integration assertions bind to a port list. A port list that varies by configuration needs a checker that varies by configuration, and the checker is now as configuration-specific as the wrapper.
And it hides real problems. Once the integrator has an ifdef habit, a genuinely missing connection looks like another feature-conditional one.
The rule. Keep the boundary stable and let the semantics vary. A disabled replay drives
replay_attemptsto zero — which is true, informative, and requires no wrapper change. A tied-off output is cheaper than a conditional port in every dimension that matters: integration effort, verification collateral, synthesis predictability, and the number of builds that have ever been tested.
16. What an Interface Contract Must Define
A port list is not a contract. For every boundary the IP exposes, seven things must be specified, and a missing one is an integration bug waiting for a specific integrator.
| Element | What must be stated | Failure if unstated |
|---|---|---|
| Payload | fields, widths, which are meaningful when | an integrator reads a don't-care field |
| Valid / ready | which side drives each, whether ready is combinational (§31) | a combinational loop, or a timing surprise |
| Metadata | that it accompanies its payload on the same accept event | payload and metadata offset forever (19.4 §16) |
| Stability | payload held while stalled (§48) | data changes under a stalled consumer |
| Reset | which reset applies, synchronous or not, release ordering (§26) | §28 |
| Clock | which domain, and every crossing (§30) | a CDC the integrator did not know existed |
| Error | the categories, and which are recoverable (§39) | §41 |
Two rules about how the contract is written.
Describe semantics, not the current implementation. "This output is asserted while the transmit queue is non-empty" is an implementation statement — it becomes false when the queue is replaced. "This output is asserted while the block holds accepted work that has not yet been delivered" is a contract statement, and it survives the redesign.
And state what is not guaranteed, explicitly. Latency that happens to be three cycles today and is not contractually three cycles must say so. An integrator will otherwise measure it and depend on it — and be right to, because nothing told them not to.
17. A Protocol-Independent Stream Type
// ILLUSTRATIVE. A generic transfer type for the IP's own internal and external
// stream boundaries. NOT an FDI or RDI signal set, and no UCIe field name,
// width or direction is claimed (§3).
typedef struct packed {
logic [DATA_W-1:0] data; // meaningful for bytes indicated by strb
logic [STRB_W-1:0] strb; // which bytes carry meaning this beat
logic [META_W-1:0] meta; // opaque to this boundary; see the meta contract
logic [CLS_W-1:0] cls; // resource class — 19.5 §8's index
logic first; // first beat of an object
logic last; // last beat of an object
logic poison; // this object is known bad; do not act on it
} ip_stream_t;Architecture. One struct carrying everything the boundary needs and nothing about how either side implements it.
Five decisions in that struct, each of which is a lesson.
meta is opaque. The boundary transports it and does not interpret it. That is what makes the same stream type usable for a PCIe-carrying path, a CXL-carrying path and a streaming path — and it is the reason 19.2 §8's normalisation exists on the other side of it.
cls travels with the beat rather than being a separate side-channel. A class carried on a parallel bus can be sampled at a different cycle than the data it describes, which is 19.4 §16's metadata-offset bug reaching the boundary.
first and last are both present, not just last. last alone forces the receiver to infer first from the previous beat's last, which requires state and desynchronises after any lost beat. Both bits make each beat self-describing, and an assertion that first and last frame correctly is one of §48's cheapest checks.
poison exists because the alternative is worse. An object known to be bad must be marked rather than silently dropped, so the consumer can account for it — 19.3 §30's delivery gate depends on being able to distinguish "not delivered" from "delivered and invalid".
And there is no id field. Identity is a semantic concern owned by the protocol engine (19.2 §20); a transport boundary that carries an identity has invited both ends to interpret it, and they will interpret it differently.
18. Wrong Interface — Internal Names as the Public Contract
// WRONG — the public port list is a snapshot of the current implementation.
module ucie_adapter_tx (
input logic stg_fifo_wr_en, // "the staging FIFO's write enable"
input logic [7:0] stg_fifo_wr_ptr, // an internal pointer, exposed
output logic stg_fifo_almost_full, // an internal watermark, exposed
output logic [4:0] replay_ring_head, // another internal pointer
...
);What happens six months later. The staging buffer is replaced — a derived occupancy instead of a maintained counter (19.4 §9), or a different depth, or merged with the skid. The internal pointers change width or stop existing.
Every integration breaks, and it breaks for a change that altered no externally observable behaviour.
Four properties.
The integrator's logic depends on the wrong thing. Somebody's flow control read stg_fifo_almost_full. That signal meant "this FIFO is near its watermark" — an implementation fact. What they needed was "the block will stop accepting soon", a contract fact, and the two coincided until the buffer was redesigned.
It is not fixable by renaming. The problem is not the names, it is that the quantities are internal. Renaming stg_fifo_almost_full to tx_almost_full leaves an internal watermark exposed under a nicer name, and the next redesign still breaks it.
It blocks the redesign, which is the real cost. Once integrations depend on the internal shape, the internal shape cannot be improved — so the IP is frozen not by its contract but by its accidents. This is how a reusable block becomes a legacy block.
And it makes verification collateral non-portable. §48's assertions bind to these ports and encode the same assumptions. The checkers break with the redesign too, so the one artefact that could have caught the integration breakage breaks alongside it.
The rule. The public boundary answers questions; it does not expose registers. Will you accept work? —
ready. Do you hold undelivered work? — abusyor occupancy-class output defined semantically. Why are you not ready? — a stall-reason encoding (19.1 §44), which is a classification rather than a pointer. Every one of those survives an internal redesign; none of them names a structure.
19. Configuration Versioning and Capability Discovery
A reusable block outlives the software that first configured it. The same driver may meet three generations of the IP, and each generation has features the previous one did not.
Three mechanisms, and a block needs all three.
A version identifier, so software can know which contract it is talking to.
A capability indication, so software can know what this instance actually supports — which is not derivable from the version, because two instances of the same version may be built with different structural parameters (§4).
And a defined behaviour for unsupported requests, so software written for a later generation fails cleanly on an earlier one rather than half-configuring it.
20. The Capability Structure
// ILLUSTRATIVE. What THIS INSTANCE can do, derived from the structural
// parameters at elaboration. Read-only to software. IP-specific, not UCIe.
typedef struct packed {
logic [7:0] ip_major; // contract version
logic [7:0] ip_minor;
logic [7:0] max_lanes; // = NUM_LANES
logic [7:0] max_classes; // = NUM_CLASSES
logic replay_supported; // = REPLAY_ENABLE
logic integrity_supported; // = INTEGRITY_ENABLE
logic multi_protocol_supported; // = MULTI_PROTOCOL_ENABLE
logic degraded_width_supported; // = DEGRADED_WIDTH_ENABLE
logic async_phy_boundary; // = ASYNC_PHY_BOUNDARY
logic [7:0] tx_depth_log2; // structural sizing, for software tuning
logic [7:0] rx_depth_log2;
} ip_capabilities_t;Architecture. Every field is a function of an elaboration parameter, wired at build time. There is no logic here — it is the parameter set made readable.
Why this earns its area. Without it, software has three bad options: hardcode the configuration per product (which breaks on the next SKU), probe by attempting operations and observing failures (which is how a driver corrupts a link), or read a build-time file that is not in the silicon (which drifts). Exposing the parameters is the only mechanism that cannot drift, because it is the parameters.
Why the depths are exposed as log2. They are advisory — software tunes watermarks and outstanding limits against them. Exposing them as log2 makes it structurally clear they are sizing hints rather than pointers, which is §18's distinction encoded in the representation.
And what is deliberately absent. No current state, no occupancy, no active configuration. This structure answers "what was built", and mixing in "what is happening now" is exactly the confusion §25 exists to prevent — a software developer who reads max_lanes and assumes it is the active width has been misled by the register layout.
21. Requested, Validated, Active
The pattern this curriculum has now used at six layers, and the reason it belongs in the reuse chapter is that a reusable block must expose all three, not just the last one.
| Field | Written by | Read by | Meaning |
|---|---|---|---|
| requested | software | software, and the validator | what somebody asked for |
| validated | hardware (§13) | software | whether the request is legal for this instance |
| active | hardware, on commit | the datapath, and software | what the block is actually doing |
Three reasons all three must be visible.
Software cannot debug with only active. A configuration write that had no effect is indistinguishable from one that was rejected, which is indistinguishable from one still pending. Three registers make those three states three readings.
The datapath must read only active. 19.1 §37, 19.3 §47 and 19.5 §36 all made this point about their own configuration; here it is a boundary requirement, because an integrator who wires their datapath to a requested value has reintroduced §23's bug outside the IP.
And validated must be separate from active. A legal request that is waiting for quiescence is validated and not yet active. Collapsing them means software cannot tell "illegal" from "waiting" — and those need different responses: fix the request, or wait.
22. The Configuration Commit FSM
// ILLUSTRATIVE. Product/IP architecture, not a UCIe state machine (§3).
typedef enum logic [2:0] {
CFG_IDLE = 3'd0,
CFG_VALIDATE = 3'd1,
CFG_QUIESCE = 3'd2,
CFG_COMMIT = 3'd3,
CFG_RESUME = 3'd4,
CFG_REJECT = 3'd5
} cfg_state_e;
cfg_state_e cfg_state_q, cfg_state_d;
always_comb begin
cfg_state_d = cfg_state_q;
unique case (cfg_state_q)
CFG_IDLE : if (cfg_write_done) cfg_state_d = CFG_VALIDATE;
CFG_VALIDATE : cfg_state_d = legal_cfg(cfg_requested_q) ? CFG_QUIESCE
: CFG_REJECT;
CFG_QUIESCE : if (quiesce_timeout) cfg_state_d = CFG_REJECT;
else if (datapath_idle) cfg_state_d = CFG_COMMIT;
CFG_COMMIT : cfg_state_d = CFG_RESUME; // exactly one cycle
CFG_RESUME : if (datapath_running) cfg_state_d = CFG_IDLE;
CFG_REJECT : if (cfg_status_read) cfg_state_d = CFG_IDLE;
default : cfg_state_d = CFG_REJECT;
endcase
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) cfg_state_q <= CFG_IDLE;
else cfg_state_q <= cfg_state_d;
// The commit is ONE cycle and writes the WHOLE active configuration at once.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cfg_active_q <= CFG_RESET_VALUE;
cfg_epoch_q <= '0;
end else if (cfg_state_q == CFG_COMMIT) begin
cfg_active_q <= cfg_requested_q; // atomic: one register
cfg_epoch_q <= cfg_epoch_q + 1'b1; // and the epoch, same cycle
end
end
assign cfg_validated = (cfg_state_q != CFG_REJECT) && (cfg_state_q != CFG_IDLE);
assign cfg_admit_stop = (cfg_state_q == CFG_QUIESCE) || (cfg_state_q == CFG_COMMIT);Architecture. Validate before quiescing, quiesce before committing, commit in one cycle, then resume. CFG_COMMIT is unconditionally one cycle long, which is what makes atomicity structural rather than reviewed.
State. One FSM register, the active configuration, and an epoch. The epoch advances with the commit in the same cycle — 19.5 §31 made this atomicity a property; here it is the same expression, so it holds by construction.
Cycle behaviour. cfg_admit_stop covers quiesce and commit, so no new work enters while the configuration is changing.
Contract. datapath_idle must mean every structure the configuration affects is empty — not just the ingress queue. 19.5 §36 showed what a partial quiesce does to a capacity reduction: occupied can exceed the new active_capacity and every subsequent subtraction underflows.
Failure. The quiesce_timeout path to CFG_REJECT is essential and easy to omit. Without it, a configuration request during a permanent stall hangs the FSM in CFG_QUIESCE forever, and software sees a write that neither completes nor fails — the worst of the three states in §21.
DV. Every arc. Then a configuration write with traffic in flight, one with an illegal value, one during a link recovery, and one that times out — and confirm the datapath never observes a mixed configuration in any of them.
23. Wrong Config Update — the Partial Commit
// WRONG — each field commits when software writes it.
always_ff @(posedge clk) begin
if (wr_en && (addr == ADDR_WIDTH)) cfg_active_q.req_lanes <= wr_data;
if (wr_en && (addr == ADDR_PROTO)) cfg_active_q.proto_en <= wr_data;
if (wr_en && (addr == ADDR_REPLAY)) cfg_active_q.replay_en <= wr_data;
endWorked. Software performs three writes, three cycles apart, to change from an x16 single-protocol configuration with replay to an x8 dual-protocol configuration without.
| Cycle | Active configuration the datapath sees | Legal? |
|---|---|---|
| t | x16, one protocol, replay on | yes |
| t+1 | x8, one protocol, replay on | yes, but not what was intended |
| t+2 | x8, two protocols, replay on | yes |
| t+3 | x8, two protocols, replay off | yes — the intended configuration |
Every intermediate configuration is individually legal, and the datapath operated in two configurations nobody designed. Objects admitted at t+1 were striped for x8 while the far end was still x16. Objects admitted at t+2 were classified into a second protocol class whose credit state had never been initialised.
Five properties.
Legality checking does not help. Each intermediate passes legal_cfg. The illegality is in the transition, not in any state — which is why the mechanism must be atomicity rather than validation.
The corruption is silent. No assertion fires; the datapath did exactly what its configuration told it. The far end receives objects striped for the wrong width and interprets them as data, so the failure surfaces as a data-integrity problem several layers away.
It is not fixable by writing in a different order. There is no ordering of three independent writes that avoids two intermediate configurations. Some orderings are less harmful, which is worse than none being harmful, because a team that finds a "safe order" has documented a workaround instead of fixing the mechanism.
Software cannot fix it either. A driver that quiesces traffic before writing is relying on a discipline that no register enforces, across an interface, in code owned by somebody else. The hardware must make the guarantee.
And it composes with every configuration-dependent structure in the block. Credit capacity, replay depth, class count, watermarks — each one reads the configuration every cycle, so each one sees the mixture.
24. SVA — the Active Configuration Changes Atomically
// MANDATORY. The active configuration changes only on a commit.
property p_active_cfg_changes_on_commit_only;
@(posedge clk) disable iff (!rst_n)
(cfg_active_q != $past(cfg_active_q)) |-> $past(cfg_state_q == CFG_COMMIT);
endproperty
a_active_cfg_changes_on_commit_only:
assert property (p_active_cfg_changes_on_commit_only);
// MANDATORY — the commit is exactly one cycle, so no multi-cycle window exists.
property p_commit_single_cycle;
@(posedge clk) disable iff (!rst_n)
(cfg_state_q == CFG_COMMIT) |=> (cfg_state_q != CFG_COMMIT);
endproperty
a_commit_single_cycle: assert property (p_commit_single_cycle);
// MANDATORY — the datapath is idle at the commit. Section 22's contract.
property p_commit_only_when_quiesced;
@(posedge clk) disable iff (!rst_n)
(cfg_state_q == CFG_COMMIT) |-> (datapath_idle && (outstanding_count == '0));
endproperty
a_commit_only_when_quiesced: assert property (p_commit_only_when_quiesced);
// MANDATORY — an illegal request never becomes active.
property p_illegal_never_active;
@(posedge clk) disable iff (!rst_n)
!legal_cfg(cfg_requested_q) |-> (cfg_state_q != CFG_COMMIT);
endproperty
a_illegal_never_active: assert property (p_illegal_never_active);
// MANDATORY — a live object's configuration epoch does not change under it.
// The effect check: what atomicity is FOR.
property p_object_sees_one_epoch(int unsigned tag);
@(posedge clk) disable iff (!rst_n)
(obj_accept_fire && (obj_tag == tag))
|-> (cfg_epoch_q == $past(cfg_epoch_q, 0))
throughout (obj_complete_fire && (obj_tag == tag))[->1];
endpropertyArchitecture. Four properties on the mechanism and one on the outcome.
Why the fifth is the one that matters. The first four describe this commit FSM. The fifth describes what the FSM exists to guarantee — that no accepted object ever spans a configuration change — and it holds regardless of how the commit is implemented. It is the property that would catch §23 in a design with no commit FSM at all, which is exactly the design that has the bug.
Why p_commit_only_when_quiesced checks outstanding_count as well as datapath_idle. An empty pipeline is not the same as no outstanding work: objects can be in flight at the peer with responses pending (19.2 §16's semantic table). A configuration committed while operations are outstanding changes the rules those operations will be completed under.
DV. Force a commit attempt at every point in an object's lifetime and confirm it waits. Then break the atomicity deliberately — commit two fields on separate cycles — and confirm the fifth property fires while the first four can be made to pass.
25. Structural Maximum Versus Runtime State
These are different registers with different owners, and conflating them is the most common misreading of a reusable block's interface.
// ILLUSTRATIVE. Three lane-related quantities, and all three are needed.
localparam int NUM_LANES = 32; // structural: what was built
logic [NUM_LANES-1:0] cfg_lane_mask_q; // static config: what is enabled
logic [NUM_LANES-1:0] active_lane_mask_q; // runtime state: what works NOW
// The invariant that binds them, and it is a subset chain.
// active ⊆ configured ⊆ built
assign lane_state_legal = ((active_lane_mask_q & ~cfg_lane_mask_q) == '0);Worked. The block is elaborated for 32 lanes. The product enables 16 of them. A lane fails and repair degrades the link to 8. All three numbers are simultaneously true and none of them is "the width".
Three consequences for the interface.
Software must be able to read all three. A driver that reads only the active mask cannot tell a degraded link from a narrow product; one that reads only the capability cannot tell a healthy link from a failed one.
Performance expectations follow the active mask. Bandwidth estimates, watermark tuning and outstanding limits should all be computed from active_lane_mask_q, and it changes at runtime — so anything derived from it is dynamic too, which is 19.4 §29's headroom becoming a moving target on a degradable link.
And the structural maximum still costs area. The 32-lane datapath exists in a product using 8. That is the correct trade only if the IP is genuinely shared across products that use the range — otherwise it is §5's mistake made with a parameter instead of a register, and the envelope table of §34 is where that decision gets recorded honestly.
The assertion is a subset chain, and it is worth binding at integration: active ⊆ configured ⊆ built. A violation means repair or negotiation has enabled a lane the product disabled, which is a serious escape — the lane may not be bonded at all.
26. The Reset Contract
The most common integration failure of a reusable block is not algorithmic. It is a reset the integrator drove differently than the designer assumed.
A reusable block must document four reset kinds and what each one clears.
| Reset kind | Purpose | Typical scope |
|---|---|---|
| Hard / POR | bring the block to a known state from cold | everything |
| Soft / recovery | re-establish the link without losing accepted work | link-epoch state only |
| Configuration reset | restore configuration defaults | requested and active configuration |
| Diagnostic clear | zero counters and first-fault records | instrumentation only |
And it must document, for each:
- synchronous or asynchronous assertion;
- synchronous release, and to which clock;
- minimum assertion width, in cycles of which clock;
- ordering relative to clocks being stable and to other resets;
- which reset each state element responds to — §27's matrix.
The rule that generates all of this. There is no
reset_all. 19.1 §8's reset hierarchy, 19.2 §44's semantic table surviving recovery, 19.3 §50's reliability state surviving recovery, 19.4 §51's per-buffer policy and 19.5 §28's epoch lifetime are five chapters saying the same thing: a recovery must not clear state whose meaning outlives the link event. A block that offers one reset input has made that distinction unavailable to its integrator.
27. The Reset Matrix
This table is a deliverable. It ships with the IP, it is reviewed at integration, and every row of it is an assertion in §29.
| State | POR | Soft / recovery | Config reset | Diagnostic clear |
|---|---|---|---|---|
| Semantic operation table (19.2 §16) | clear | preserve | preserve | preserve |
| Semantic identity / generation | clear | preserve | preserve | preserve |
| Replay ring contents (19.3 §19) | clear | preserve and re-baseline | preserve | preserve |
| Replay pointers | clear | re-baseline with the peer | preserve | preserve |
| Credit counters (19.5 §13) | clear to zero | re-establish by agreement | clear to zero | preserve |
| Credit epoch (19.5 §28) | clear | advance | preserve | preserve |
| Pending-return accumulator | clear | clear | clear | preserve |
| Active configuration | defaults | preserve | defaults | preserve |
| Requested configuration | defaults | preserve | defaults | preserve |
| Configuration epoch | clear | preserve | advance | preserve |
| Ingress / staging / output buffers (19.4 §51) | clear | preserve | preserve | preserve |
| Reassembly buffer | clear | architecture-defined | preserve | preserve |
| CDC FIFO (PHY-facing) | clear | drain or hold, by domain | preserve | preserve |
| Link training / negotiation state | clear | clear | preserve | preserve |
| Active lane mask | clear | re-derive from training | preserve | preserve |
| First-fault record (19.1 §35) | clear | preserve | preserve | clear |
| Performance counters | clear | preserve | preserve | clear |
Four readings of this table, and the fourth is the point of the whole section.
Column 3 is mostly "preserve". That is the headline: a recovery clears link-epoch state and almost nothing else. Seventeen rows and only five of them clear.
Two rows have clear to zero rather than defaults. Credit counters reset to zero, not to the configured depth — 19.5 §35 is why, and it is the difference between a link that stalls until it is told its capacity and a link that overflows the far end at bring-up.
One row is deliberately unresolved. The reassembly buffer's recovery policy is architecture-defined, because a partial object's fate is a protocol question (19.4 §51). Writing "architecture-defined" in a delivered matrix is honest and useful; leaving the row out is not.
And the diagnostic-clear column is where a subtle bug lives. The credit epoch must be preserved by a diagnostic clear. A counter-clear implementation that resets a register block wholesale — because the counters and the epoch happen to live in the same address range — advances nothing but destroys the epoch, and every in-flight return becomes acceptable again. 19.5 §31's monotonicity assertion catches exactly this, and this table is where the requirement it checks is written down.
28. Wrong Reusable IP — the Hidden Reset Assumption
// WRONG — the block assumes a synchronous reset and says so nowhere.
always_ff @(posedge clk) begin
if (!rst_n) state_q <= S_IDLE; // synchronous reset, undocumented
else state_q <= state_d;
endWhat the integrator does. Their SoC uses an asynchronous reset distributed from a central controller, released without synchronisation to this block's clock — a perfectly normal SoC reset architecture.
What happens. rst_n releases asynchronously relative to clk. The release can occur arbitrarily close to a clock edge. Different flops in the state register capture different values, and a one-hot or gray-coded state register can land in an encoding that is not a legal state.
Four properties.
It is a metastability failure, so it is intermittent, temperature-dependent and voltage-dependent. It appears in one unit out of a thousand, at one corner, and disappears when anybody looks at it.
It appears at bring-up, when nothing else is trusted either. A block that occasionally comes up in an illegal state at power-on is indistinguishable from a hundred other bring-up problems, and the investigation goes to the PHY, the PLL, and the power sequencing before it goes to a reset synchroniser.
The default arm decides how bad it is. 19.5 §34 argued for faulting on the default rather than holding. A design that faults reports an illegal state and stops; one that holds runs indefinitely in a state that is not in the state diagram — and no assertion written against the legal states will describe what it does.
And the fix is trivial once the cause is known. Either the block synchronises its own reset release, or the contract says the integrator must. Both are acceptable; leaving it unstated is not, and stating it in a comment inside the RTL is not stating it either — the integrator reads the interface document, not the always block.
The contract sentence this needs. "
rst_nmay be asserted asynchronously and must be released synchronously toclk, with at least N cycles of assertion afterclkis stable." One sentence, and it eliminates the entire failure class.
29. SVA — the Reset Contract
// MANDATORY. Reset behaviour, checkable at integration.
// Release is synchronous — no reset deassertion within a setup window of a
// clock edge. Bind this at the integration boundary, not inside the block.
property p_reset_release_synchronous;
@(posedge clk)
$rose(rst_n) |-> $stable(rst_n)[*1];
endproperty
// MANDATORY — minimum assertion width, in cycles of this clock.
property p_reset_min_width;
@(posedge clk)
$fell(rst_n) |=> !rst_n[*(RST_MIN_CYCLES-1)];
endproperty
a_reset_min_width: assert property (p_reset_min_width);
// MANDATORY — the state machine is in a legal state whenever reset is released.
property p_state_legal_after_reset;
@(posedge clk) disable iff (!rst_n)
state_q inside {S_IDLE, S_ACTIVE, S_RECOVER, S_FAULT};
endproperty
a_state_legal_after_reset: assert property (p_state_legal_after_reset);
// MANDATORY, ONE PER ROW of Section 27's matrix. The preserve direction is the
// one that finds bugs, because clearing is the accidental default.
property p_soft_reset_preserves_semantic_table;
@(posedge clk) disable iff (!por_n)
soft_reset_fire |=> (sem_table_q == $past(sem_table_q));
endproperty
a_soft_reset_preserves_semantic_table:
assert property (p_soft_reset_preserves_semantic_table);
property p_diag_clear_preserves_epoch;
@(posedge clk) disable iff (!por_n)
diag_clear_fire |=> (cfg_epoch_q == $past(cfg_epoch_q))
&& (credit_epoch_q == $past(credit_epoch_q));
endproperty
a_diag_clear_preserves_epoch: assert property (p_diag_clear_preserves_epoch);
// MANDATORY — the negative form, which fails at INTEGRATION rather than
// waiting for a symptom. No recovery signal reaches any clearing input.
property p_recovery_never_clears_semantics;
@(posedge clk) disable iff (!por_n)
recovery_active |-> !sem_table_clear_en;
endproperty
a_recovery_never_clears_semantics:
assert property (p_recovery_never_clears_semantics);Architecture. Contract checks on the reset signal itself, a legal-state check, one preserve check per matrix row, and a negative connectivity check.
Why the preserve direction is the one to assert. Clearing is what a careless implementation does by default — a wide reset expression, a shared reset net, a register block cleared wholesale. Nothing accidentally preserves state. So the assertions worth the effort are the ones that say "this must not have been cleared", and there should be one per row of §27's matrix.
Why p_recovery_never_clears_semantics is written as connectivity. It asserts that a signal never reaches an input, which fails during integration on a mis-wire rather than during a test on a symptom. 19.4 §52 made the same argument about a global buffer flush. These are the cheapest assertions in any IP block and they catch the errors that are hardest to reproduce.
Note the disable iff (!por_n). These properties are about soft resets, so they must remain active during them — disabling on the reset being tested would make every one of them vacuous, which is the single most common way a reset assertion silently checks nothing.
DV. Drive each reset kind independently with populated state and check every matrix row. Then drive two resets simultaneously, which is the case the matrix does not describe and the integrator will eventually produce.
30. The Clock Contract
A reusable block fails integration at its boundaries far more often than inside its algorithms, and the clock contract is half of that boundary.
What must be stated:
- which clock each port is synchronous to, port by port, with no exceptions and no "usually";
- every clock-domain crossing inside the block, and the mechanism used for each — synchroniser chain, Gray-coded FIFO pointers, handshake;
- which crossings the integrator is responsible for, if any;
- whether two clocks may be the same — a block with an
ASYNC_PHY_BOUNDARYparameter must define behaviour when the parameter is off and the clocks are tied; - frequency ratio assumptions, if any exist. A design that assumes the read clock is never slower than half the write clock has a functional requirement, not a performance preference, and it must be documented and asserted.
Two failure modes worth naming.
An undocumented internal CDC. The integrator ties both clocks together, the crossing becomes trivially safe, everything works — and the block is later used with genuinely asynchronous clocks by somebody who was never told a crossing existed. 19.4 §48's binary-pointer-synchronised-directly bug is what they find.
A pulse crossing a boundary. 19.1 §40 established that a single-cycle pulse crossing a domain may be seen zero, one or two times. A credit return, a release event or an error indication crossing as a bare pulse is 19.5 §24's duplicate-return bug, arriving from the clock architecture rather than from the credit logic.
31. Say Whether ready Is Combinational
// ILLUSTRATIVE. Two boundary styles. BOTH ARE VALID; the contract must say
// which, because the integrator's timing closure depends on it.
// Style A — combinational ready. Lowest latency, and it exports a path.
assign in_ready = downstream_ready && !internal_hazard;
// Style B — registered ready, via a skid buffer (19.4 §19). One cycle of
// latency, and the boundary is timing-clean in both directions.
assign in_ready = !skid_full_q;Why this is a contract item rather than an implementation detail.
Style A exports a combinational path through the block. The integrator's upstream logic now has a path to this block's downstream interface. Two blocks in series, each with a combinational ready, produce a path spanning both — and three produce a path nobody budgeted for. This is 19.2 §38's combinational ready loop reaching the boundary.
Style A can also create a genuine loop. If the integrator wires this block's ready into something that feeds its valid, the loop closes outside the IP, and neither the IP nor the integration is individually wrong. Only the contract can prevent it, by stating that ready is combinational so the integrator knows not to.
Style B costs a cycle and a skid buffer, and buys a boundary that composes. For hardened IP intended for many integrations, Style B is almost always right — the latency is one cycle, and the alternative is a timing negotiation with every integrator.
The requirement is not a style. It is a statement. Say which one, per boundary, in the interface document, and assert it — §48 includes a
ready-timing check for exactly this reason.
32. The Timing Contract
What a reusable block must specify, and what it must not claim.
| Must specify | Must not claim |
|---|---|
| which input-to-output paths are combinational | a fixed maximum frequency |
| which boundaries are registered | a fixed cell-library-independent latency |
| architectural latencies — pipeline depths the protocol depends on | a fixed area |
| how each parameter affects critical-path depth (§33) | that "any parameter value closes timing" |
| the assumed maximum fan-in / fan-out at each boundary | — |
Two distinctions that matter.
An architectural latency is part of the contract; an implementation latency is not. If the far end must receive a response within N cycles for a protocol timer not to expire, that N is architectural and belongs in the contract. If the ingress path happens to be three stages deep today, that is not architectural and must be documented as not guaranteed — otherwise an integrator measures it, depends on it, and a later pipeline stage breaks them (§18's failure in the time domain).
Parameter-to-timing relationships are the part everybody omits. §33 is why they matter: a block that is functionally correct at NUM_CLASSES = 32 and unroutable at that value has a supported functional range and a much narrower supported physical range. The envelope table of §34 must record both.
33. Wrong Parameterisation — Unbounded Arbiter Fan-In
// WRONG-BY-OMISSION — functionally generic, physically unbounded.
always_comb begin
grant = '0;
for (int c = 0; c < NUM_CLASSES; c++)
if (req[c] && (grant == '0)) grant[c] = 1'b1; // priority chain, depth ∝ NUM_CLASSES
endWhat happens as NUM_CLASSES grows. The priority chain is a serial dependency: class c's grant depends on all classes below it. The combinational depth grows linearly with NUM_CLASSES, and at 3 classes it is invisible while at 32 it is the critical path of the whole block.
Four properties.
The RTL is functionally correct at every value. Simulation passes. Assertions pass. Coverage closes. Nothing in the functional domain reports a problem, so the defect is discovered by physical design, late, in a product the RTL team considered finished.
The failure is reported as somebody else's problem. "The arbiter does not close timing at 32 classes" arrives as a physical-design issue, and the available responses at that stage — pipelining the arbiter, retiming, restructuring — all change the block's latency contract (§32), which changes behaviour the far end may depend on.
Reusability made it worse, not better. The block was written generically so that a future product could use 32 classes. The genericity created a claim the block cannot honour, and the product that tries to use it discovers this after committing to it.
And the fix is architectural, not syntactic. A tree-structured arbiter, a two-level arbiter, or a registered grant with a one-cycle latency each solve it — and each changes the contract. So the decision belongs at design time, with the envelope table recording the answer, rather than at closure time.
The rule. Reusable does not mean unbounded. For every parameter, know how the critical path scales with it, and record the physically supported range separately from the functionally supported range. A parameter with no stated upper bound is a promise to close timing at infinity.
34. The Supported Configuration Envelope
A deliverable table, and the one that makes every parameter claim honest.
| Parameter | Legal range | Verified range | Physically closed | Architectural consequence |
|---|---|---|---|---|
NUM_LANES | 1, 2, 4, 8, 16, 32 | all six | to 32 | datapath width; deskew depth |
DATA_W | multiple of NUM_LANES | 64, 128, 256, 512 | to 512 | must divide evenly (§10) |
TX_DEPTH | 1 to 256 | 1, 2, 3, 15, 16, 17, 256 | to 256 | latency absorption (19.4 §21) |
RX_DEPTH | 1 to 256 | 1, 2, 3, 15, 16, 17, 256 | to 256 | credit capacity (19.5 §4) |
NUM_CLASSES | 1 to 8 | 1, 2, 3, 8 | to 8 only (§33) | arbiter depth; per-class state |
REPLAY_DEPTH | 0, or 4 to 128 | 0, 4, 32, 128 | to 128 | reliability window (19.4 §39) |
REPLAY_ENABLE | 0, 1 | both | both | requires INTEGRITY_ENABLE (§12) |
ASYNC_PHY_BOUNDARY | 0, 1 | both | both | adds a CDC FIFO; changes latency |
CDC_STAGES | 2 to 4 | 2, 3 | all | 4 is legal and unverified |
Four things this table does that a parameter list cannot.
It separates legal from verified. CDC_STAGES = 4 is legal and has never been run. Saying so is the difference between a known gap and a surprise — and it lets the integrator decide whether to fund the verification or avoid the value.
It separates verified from physically closed. NUM_CLASSES above 8 is functionally fine and does not close (§33). That is the row that stops a product from designing itself into a corner.
The verified column shows the selection strategy, not a range. Depths are verified at 1, 2, 3, at a power of two and its neighbours, and at the maximum — which is §51's golden-configuration policy made visible. A reader can tell at a glance that the powers-of-two edge cases were considered.
And a blank or absent row is a claim nobody has checked. The table's real value is that omissions are visible. A parameter that does not appear here has no stated support, and that is a far more useful default than an implied "any value works".
35. The Parameter Coverage Matrix
The Cartesian product is not reachable and pretending otherwise is dishonest. Nine parameters with four values each is 262,144 configurations, at hours of regression per configuration.
What to run instead, in priority order:
| Tier | What it covers | Cost |
|---|---|---|
| 1 — nominal | the shipping configuration of every active product | one build per product |
| 2 — extremes per parameter | each parameter at min and max with others nominal | 2 × parameters |
| 3 — pairwise | every pair of parameter values appears in some run | tens of builds |
| 4 — targeted interactions | pairs the architecture says interact (§12's graph) | a handful, chosen |
| 5 — invalid neighbours | configurations that must be rejected (§11) | cheap — they fail at elaboration |
Three notes on why this ordering.
Tier 4 is where the bugs are, and it is not covered by tier 3. Pairwise coverage is mechanical and finds width and edge bugs; the interactions that matter are the ones the dependency graph names — replay depth against duplicate-window size, CDC depth against synchroniser stages, watermark against producer pipeline depth. A hand-chosen tier 4 of ten configurations is worth more than a mechanical tier 3 of eighty.
Tier 5 is nearly free and almost always skipped. An illegal configuration fails at elaboration in seconds. Running the invalid neighbours of every legal boundary is minutes of compute and is the only thing that verifies §10's assertions actually fire — an elaboration check that has never been observed to fail may not be connected.
And every tier must state what it omitted. A regression report that says "parameter coverage: passed" without saying which configurations were not built reads as complete coverage of a space it sampled. Name the untested values — they are the ones in §34's "legal, unverified" column.
36. Reuse by Contract, Not by Shape
Two structures that store data are not the same component. Chapter 19.4 §4 enumerated eight buffer kinds with eight lifetimes; the reuse question is which of them can share an implementation.
The test is the ownership contract, not the storage.
| Structure | Entry created by | Entry destroyed by | Can it be a FIFO? |
|---|---|---|---|
| Ingress / output FIFO | a push | a pop | yes — this is the FIFO |
| Skid buffer | upstream valid with skid empty | downstream takes it | yes, a depth-1 FIFO with a specific ready contract |
| Staging buffer | admission | replay confirming ownership | no — release is not a pop |
| Replay ring | history commit | resolution, out of order | no — entries retire on an external verdict |
| Reassembly buffer | first fragment | object complete | no — indexed, not ordered |
| Ping-pong bank | producer claims a free bank | the consumer releases it | no — ownership, not pointers |
| CDC FIFO | write-domain push | read-domain pop | yes, with Gray pointers — a FIFO with a different pointer discipline |
Three of seven are FIFOs. Four are not, and the four that are not differ in when an entry stops being live — which is precisely the thing a queue abstraction hardcodes.
The rule. Reuse the FIFO for the three that are FIFOs, and write the other four separately. Two structures share an implementation when they share an ownership contract, not when they share a storage element. A replay ring and an output FIFO both hold objects in memory and have nothing else in common.
37. Wrong Abstraction — the Universal Buffer
// WRONG — one module for every storage structure in the block.
module ucie_universal_buffer #(
parameter int DEPTH = 16,
parameter int WIDTH = 64,
parameter bit MODE_FIFO = 1'b1,
parameter bit MODE_REPLAY = 1'b0,
parameter bit MODE_CDC = 1'b0,
parameter bit MODE_SKID = 1'b0,
parameter bit MODE_PINGPONG = 1'b0,
parameter bit MODE_REASSEMBLY = 1'b0,
parameter bit OUT_OF_ORDER_FREE = 1'b0,
parameter bit GRAY_POINTERS = 1'b0,
parameter bit EXTERNAL_RETIRE = 1'b0,
parameter bit OWNERSHIP_STATE = 1'b0,
parameter bit INDEXED_WRITE = 1'b0,
parameter int NUM_BANKS = 1,
parameter int SYNC_STAGES = 0,
// ... and it keeps going
) ( /* a port list that is the union of seven interfaces */ );What has actually been built. Seven components, in one file, sharing a namespace, with a port list that is the union of seven interfaces — and a legal-configuration space that is the product of thirteen booleans, of which perhaps six combinations are meaningful.
Five properties, and the fourth is the one that makes this unrecoverable.
It is component explosion, hidden. The seven components still exist; they have merely been made harder to see, harder to review and impossible to verify independently.
Most of the parameter space is meaningless and unchecked. MODE_FIFO with OWNERSHIP_STATE and GRAY_POINTERS? It elaborates. §10's legality checking would need a dependency graph larger than the module, and nobody writes it, so the module accepts nonsense — which is §1's definition of the dangerous block.
Every instance carries the union's assertions. A plain FIFO instance now has ownership-state assertions bound to it that are vacuous, so the assertion pass rate becomes uninformative. Vacuous assertions are worse than absent ones, because they appear in the coverage report as passing checks.
And no instance can be changed safely. Improving the replay path means editing a module that seven structures instantiate. The blast radius of every change is the whole block, so changes stop happening — which is exactly the outcome the abstraction was built to avoid.
The seventh property is social and it is the reason this pattern keeps appearing. The universal buffer is written by somebody who noticed genuine duplication — seven modules with similar storage — and generalised it. The duplication was real; the generalisation was along the wrong axis. What the seven share is a memory array and a pointer or two, which is the cheap part. What they do not share is the ownership contract, which is the expensive part and the part that has bugs.
38. Specialise When Lifetimes Differ, Share the Storage Primitive
The correct factoring keeps the shared part shared and the different parts separate.
// ILLUSTRATIVE. A storage primitive with NO policy: it is a memory with
// a write port and a read port. Every structure instantiates it.
module ucie_storage_array #(
parameter int DEPTH = 16,
parameter int WIDTH = 64
) (
input logic clk,
input logic wr_en,
input logic [$clog2(DEPTH)-1:0] wr_idx,
input logic [WIDTH-1:0] wr_data,
input logic [$clog2(DEPTH)-1:0] rd_idx,
output logic [WIDTH-1:0] rd_data
);
// ... a memory. No pointers, no occupancy, no ownership, no lifetime.
endmodule
// Each structure owns its own control, because each has a different contract.
// ucie_fifo — wide pointers, derived occupancy, pop frees (19.4 §7)
// ucie_replay_ring — three pointers, retirement on resolution (19.3 §20)
// ucie_reassembly — indexed writes, completion bitmap (19.3 §35)
// ucie_pingpong — four-state ownership per bank (19.4 §34)Architecture. One primitive with no policy, four control modules with four contracts.
Why the split falls exactly here. The storage array has no lifetime concept at all — it does not know what an entry means, when it becomes live, or when it stops. That is the entire content of the difference between the four structures, so it is what must not be shared.
What this buys. The memory implementation — inference style, banking, retiming, whether it maps to flops or a compiled RAM — is chosen once and improves everywhere. The physical-design lever is shared; the semantics are not.
And what it costs, honestly. Four modules instead of one, and four sets of pointer logic that look superficially similar. That similarity is the trap — it is what tempts the merge in §37 — and the answer is that four small correct modules with four small correct assertion sets are cheaper to own than one module whose legal configuration space nobody can enumerate.
39. The Error Interface
A single error bit is not an interface. The integrator's only possible response to it is to give up.
// ILLUSTRATIVE. Generic IP error categories. NOT a UCIe-defined error
// register or encoding (§3) — the categories are chosen so that each one
// implies a DIFFERENT response.
typedef enum logic [2:0] {
ERR_NONE = 3'd0,
ERR_RECOVERABLE = 3'd1, // handled internally; informational
ERR_PROTOCOL = 3'd2, // the peer or the client violated a contract
ERR_RESOURCE = 3'd3, // exhaustion: no slot, no credit, no entry
ERR_CONFIG = 3'd4, // an illegal or unsupported configuration (§13)
ERR_INTERNAL = 3'd5, // an internal invariant failed — a design bug
ERR_FATAL = 3'd6 // the link cannot continue
} ip_err_class_e;The categories are chosen by response, and that is the design rule.
| Class | The integrator's response |
|---|---|
ERR_RECOVERABLE | count it; watch the rate |
ERR_PROTOCOL | investigate the peer or the client — not this block |
ERR_RESOURCE | resize, throttle, or accept the backpressure |
ERR_CONFIG | fix the software or the part choice |
ERR_INTERNAL | escalate to the IP vendor — this is a bug in the block |
ERR_FATAL | reset the link |
The test for whether a category earns its place: does it imply a different action? If two classes lead to the same response, merge them. If one class leads to three different responses depending on context, split it.
ERR_INTERNALis the category most often missing, and it is the most valuable one in the list — it is the block saying "my own invariant failed; do not debug your integration", which can save weeks.
40. First-Error Capture
// ILLUSTRATIVE. Sticky first-error context. Captured once, held until an
// explicit diagnostic clear (§27) — never overwritten by later errors.
typedef struct packed {
logic valid;
ip_err_class_e err_class;
logic [7:0] block_id; // which sub-block raised it
logic [15:0] err_code; // block-specific detail
logic [EPOCH_W-1:0] cfg_epoch; // which configuration was active
logic [EPOCH_W-1:0] link_epoch; // which link agreement was in force
logic [TAG_W-1:0] object_tag; // the object involved, if any
logic [31:0] state_snapshot; // the sub-block's state at capture
logic [63:0] timestamp;
} ip_first_error_t;
always_ff @(posedge clk or negedge por_n) begin
if (!por_n) first_err_q.valid <= 1'b0;
else if (diag_clear_fire) first_err_q.valid <= 1'b0;
else if (err_fire && !first_err_q.valid) begin
first_err_q.valid <= 1'b1;
first_err_q.err_class <= err_class_in;
first_err_q.block_id <= block_id_in;
// ... the rest of the context, all captured in the SAME cycle
end
endArchitecture. Sticky-first, not sticky-last, with the full context captured atomically.
Why first rather than last. 19.1 §35 and 13.3 §12 both established this: errors cascade, and the last error is almost always a consequence. A credit invariant fails, the link recovers, the recovery times out, the block goes fatal. A last-error register reports "fatal, recovery timeout" and the actual cause is three events earlier.
Why the epochs are in the record. A configuration epoch and a link epoch turn "an error happened" into "an error happened under this configuration, under this link agreement" — which is the difference between a reproducible investigation and a guess. This is the field most often omitted and most often needed, because the first question in any error investigation is "what was the state of the world".
Why it survives a soft reset. §27's matrix preserves it through recovery and clears it only on a diagnostic clear. A recovery that clears the first-error record has destroyed the evidence for the thing that caused the recovery.
And why state_snapshot is opaque. It is block-specific detail, documented per block ID, not decoded at the boundary. That keeps the boundary stable (§18) while still exposing the internal context a vendor needs — the integrator forwards the value, the vendor decodes it.
41. Wrong Error Integration — Only fatal Escapes
// WRONG — the whole error interface reduced to one bit.
assign ip_error = err_fire; // no class, no context, no first-captureWorked. A credit invariant fails inside the block (19.5 §14's credit_fault). The block enters its fault state. The link stops. The integrator sees one bit go high.
What the integrator can determine from that bit: nothing.
Five specific things that are now unavailable.
Whether the block or the peer is at fault. ERR_PROTOCOL versus ERR_INTERNAL is the difference between debugging the far die and debugging this one — and with one bit, both teams debug simultaneously and neither finds it.
Whether it is a configuration mistake. ERR_CONFIG says "software asked for something this part cannot do", which is fixed in an afternoon. Without the class it looks like a hardware failure, and hardware failures get boards swapped and dies re-binned before anybody re-reads the driver.
Whether it is recoverable. A recoverable error that was handled internally and a fatal one that stopped the link are the same bit. The rate becomes unmeasurable, so a link degrading slowly looks identical to a link that failed once.
When it first happened. With no first-capture, the bit reflects the current condition. By the time software reads it, the original error may have been superseded by three cascaded consequences.
And it makes the IP look unreliable. Every problem — a driver bug, a peer violation, a marginal board — presents as "the UCIe block failed". The vendor receives escalations for defects that were never in the block, and cannot disprove them because the block reported nothing.
The rule. An error output must carry enough to route the investigation to the right team. That is the minimum bar, and it is the bar
ERR_INTERNALexists to meet.
42. The Performance Interface
// ILLUSTRATIVE. Counters chosen because each answers a question in the debug
// taxonomy (§61). Free-running, read-and-clear by software, cleared ONLY by
// a diagnostic clear (§27).
logic [63:0] obj_accepted_q; // work taken in
logic [63:0] obj_delivered_q; // work completed — the two must converge
logic [63:0] replay_attempts_q; // reliability pressure
logic [63:0] zero_credit_cycles_q; // flow-control pressure (19.5 §62)
logic [63:0] recovery_cycles_q; // link-health pressure
logic [63:0] stall_cycles_q [NUM_STALL_REASONS]; // 19.1 §44's classifier
logic [OCC_W-1:0] tx_high_water_q; // sizing evidence
logic [OCC_W-1:0] rx_high_water_q;Architecture. Eight quantities, each tied to a decision.
The selection rule, stated as a test. For every counter: name the question it answers and the action its value would change. obj_accepted_q minus obj_delivered_q answers "is work being lost or held?". The stall histogram answers "which resource is the bottleneck?". The high-water marks answer "is this buffer the right size?" — which is a question for the next design review, not an error (19.4 §54).
What must not be exposed, and this is the harder half.
Not every FSM transition. A counter per state transition is dozens of registers that answer no question and freeze the FSM encoding into the software interface — the state machine can no longer be redesigned without breaking a driver. That is §18's failure in the observability domain.
Not internal pointers. §18 directly.
Not one counter per assertion. Assertions are a verification artefact; a silicon counter for each one is verification collateral leaking into the product interface.
The boundary. Expose rates and pressures, not mechanisms. A rate survives an internal redesign; a mechanism counter does not, and every mechanism counter is a future compatibility constraint.
43. The Observability Contract
A reusable block is incomplete if an integrator cannot answer five questions from its interface alone.
| Question | What must exist | Where it came from |
|---|---|---|
| Why are you not ready? | a stall-reason classification, one-hot, causally prioritised | 19.1 §44 |
| Why are you in recovery? | the first-fault record, preserved through the recovery | §40 |
| Which configuration is active? | requested / validated / active, all three readable | §21 |
| What resource is exhausted? | per-resource stall counters, not one aggregate | 19.5 §63 |
| What failed first? | sticky-first error with epochs and object context | §40 |
Two properties of this list.
Every one of them is a question an integrator asks at 2am, and every one of them is unanswerable from a block that exposes only ready, error and a status word. The cost of answering them is a few dozen registers; the cost of not answering them is that every integration problem becomes a vendor escalation with a waveform attached.
And the stall classifier is the highest-value item. "Not ready" has at least six causes in the blocks of Chapters 19.1 to 19.5 — no credit, no replay slot, no staging space, the link is recovering, the configuration is committing, downstream backpressure. They look identical from outside and have completely different fixes, and 19.4 §39 showed what it costs to conflate just two of them: a replay-depth problem and a staging-depth problem are indistinguishable in one aggregate bin and need opposite responses.
44. Parameter-Safe Arithmetic
// ILLUSTRATIVE. Every intermediate is explicitly widened before it is used.
// The extra bit is not defensive — it is the only place an out-of-range
// intermediate can be represented long enough to be checked.
logic [OCC_W:0] committed_ext;
assign committed_ext = {1'b0, occupied_q} + {1'b0, reserved_q};
assign over_committed = (committed_ext > {1'b0, OCC_W'(DEPTH)});
assign headroom = over_committed ? '0
: OCC_W'(committed_ext - {1'b0, OCC_W'(DEPTH)});Three rules, and each one has a bug behind it.
Widen before you add. SystemVerilog sizes an expression by its context and its operands. A sum of two OCC_W-bit values in an OCC_W-bit context wraps silently, and the wrapped value is legal.
Range-check before you truncate. 19.5 §15 is the flagship instance: once truncated, an out-of-range result has become an in-range lie.
Cast explicitly, using the parameter-derived width. OCC_W'(DEPTH) rather than a bare DEPTH makes the width follow the parameter. A literal that is correct at one depth and wrong at another is §45.
45. Wrong Generic Arithmetic — the SKU That Grew
The flagship reusable-IP bug. It is correct in the configuration it was written in, incorrect in a later one, and nothing about the change draws attention to it.
// WRONG — the intermediate width comes from the literal, not the parameter.
// Written and verified with DEPTH = 16. Reused with DEPTH = 256.
logic [4:0] total; // "5 bits is plenty for depth 16"
assign total = occupied_q + reserved_q; // 5-bit sum
assign full = (total >= DEPTH);In the original SKU, DEPTH = 16. occupied_q and reserved_q are each at most 16, their sum at most 32, and a 5-bit register represents 0 to 31. The sum can reach 32 and wrap to 0 — but in this SKU occupied + reserved never exceeds 16 by construction, so it never does. The design is correct, by an argument nobody wrote down.
In the next SKU, DEPTH = 256. The parameter is overridden. occupied_q and reserved_q are now 9 bits each. total is still declared [4:0] — it is a hardcoded width, not a derived one.
| Quantity | DEPTH = 16 | DEPTH = 256 |
|---|---|---|
occupied_q max | 16 | 256 |
reserved_q max | 16 | 256 |
| true sum max | 32 | 512 |
total capacity | 31 | 31 |
| result | correct | wraps at every value above 31 |
With occupied_q = 40 and reserved_q = 0, total is 8. full is false. The buffer accepts entries it does not have.
Six properties, and this is why it is the flagship.
The original SKU is genuinely correct, so there is no bug to find in the code that was written and reviewed. The defect is created by the reuse, at a moment when nobody is reviewing the arithmetic — they are overriding a parameter.
Nothing at elaboration complains. The declaration is legal. Synthesis truncates silently, possibly with a warning in a log containing thousands of warnings.
The smoke test passes. Small transfers keep the occupancy below 31, and every value below 31 is correct. The bug requires the buffer to be more than an eighth full, which a directed test may never reach.
It fails in the overflow direction. full reads false when the buffer is full, so entries are overwritten. Data corruption, not a stall.
The larger SKU is usually the higher-performance one, so the bug appears exactly in the configuration that will run hardest and be shipped to the most demanding customer.
And the fix is trivial and unfindable. logic [OCC_W:0] total; — one line. Finding it requires knowing that a width somewhere is a literal, and there is no symptom that points at a width.
The rule that prevents the whole class. No literal widths anywhere in a parameterised block. Every width is a
localparamderived from a parameter (§9). A grep for a bare[N:0]declaration in reusable RTL should return only the derived-widthlocalparamdefinitions themselves — and that grep is a five-minute review step that finds this bug before it exists.
46. The Zero-and-One Edge Cases
$clog2 and generate loops both break at the smallest legal configuration, and the smallest legal configuration is a real product choice rather than a corner case.
| Parameter at 1 | What breaks | The guard |
|---|---|---|
DEPTH = 1 | $clog2(1) = 0 — a zero-width pointer | (DEPTH <= 1) ? 1 : $clog2(DEPTH) |
NUM_CLASSES = 1 | $clog2(1) = 0 — a zero-width class index, everywhere at once | (N <= 1) ? 1 : $clog2(N) |
NUM_LANES = 1 | per-lane loops with one iteration; deskew logic with nothing to deskew | verify the degenerate path explicitly |
NUM_CLASSES = 1 | the arbiter has one requester — does it grant? | an arbiter that assumes N ≥ 2 hangs |
REPLAY_DEPTH = 0 | §11 | the paired feature check (§10) |
CDC_STAGES = 0 | not a synchroniser at all | a minimum of 2 (§12) |
Three notes.
The class-index case is the worst because it is simultaneous. Every $clog2(NUM_CLASSES) in the design becomes zero-width in the same elaboration. Dozens of signals, ports and array indices are affected at once, and the resulting error messages point at all of them and explain none.
A single-requester arbiter is a genuine functional bug, not just a width bug. An arbiter written as "grant the first requester at or after the rotate pointer" works with one requester; one written with an explicit "if only one requester, no arbitration needed" fast path and a general path may take neither when NUM_CLASSES is 1. This is worth a directed test rather than an inspection.
And these must be verified, not just guarded. §34's envelope table lists depth 1 in the verified column for exactly this reason: a guard that has never been elaborated is a guess.
47. SVA — Parameter-Derived Invariants
// MANDATORY. Bounds that follow from the parameters, checked at runtime.
// These are the assertions that must remain true across the whole envelope.
property p_occupancy_within_depth;
@(posedge clk) disable iff (!rst_n)
(occupancy_q <= OCC_W'(DEPTH));
endproperty
a_occupancy_within_depth: assert property (p_occupancy_within_depth);
// MANDATORY — a runtime index never exceeds the structural bound.
property p_class_index_valid;
@(posedge clk) disable iff (!rst_n)
obj_valid |-> (obj_class < CLS_W'(NUM_CLASSES));
endproperty
a_class_index_valid: assert property (p_class_index_valid);
// MANDATORY — the subset chain of Section 25: active ⊆ configured ⊆ built.
property p_lane_subset_chain;
@(posedge clk) disable iff (!rst_n)
((active_lane_mask_q & ~cfg_lane_mask_q) == '0)
&& ((cfg_lane_mask_q & ~NUM_LANES_MASK) == '0);
endproperty
a_lane_subset_chain: assert property (p_lane_subset_chain);
// MANDATORY — a disabled feature is never exercised. One per optional feature,
// and these are the assertions that make Section 14's tie-offs meaningful.
property p_no_replay_when_disabled;
@(posedge clk) disable iff (!rst_n)
(!REPLAY_ENABLE) |-> (!replay_active && (replay_attempts_q == '0));
endproperty
a_no_replay_when_disabled: assert property (p_no_replay_when_disabled);
// MANDATORY — an unsupported configuration never reaches the active register.
property p_active_cfg_always_legal;
@(posedge clk) disable iff (!rst_n)
legal_cfg_independent(cfg_active_q); // the MODEL's function, not the DUT's
endproperty
a_active_cfg_always_legal: assert property (p_active_cfg_always_legal);Architecture. Bounds, index validity, the subset chain, feature gating, and configuration legality.
Why p_active_cfg_always_legal calls an independent function. §13's rule, enforced: the assertion must not call the design's own legal_cfg, because then it proves only that the design agrees with itself. legal_cfg_independent is the verification environment's separately written model, derived from §34's envelope table. If the two disagree, the assertion fires and one of them is wrong — which is exactly the information wanted.
Why the feature-gating properties matter more than they look. §14's tie-offs are the implementation of a disabled feature; these properties are the statement that the feature is genuinely inert. A disabled-replay build in which replay_active occasionally pulses has a generate branch driving a signal it should not, and nothing else would notice.
DV. Bind all of these in every configuration of §35's matrix. The interesting result is which ones become vacuous in which configurations — a property that is vacuous everywhere is checking nothing and should be rewritten.
48. Integration Assertions — Verification That Ships With the IP
The IP's boundary contract, written as bindable properties and delivered alongside the RTL. An integrator binds them and finds out immediately whether their integration honours the contract.
// ILLUSTRATIVE. Boundary contract checks, intended to be BOUND by the
// integrator without modifying the DUT.
// 1. Payload is stable while stalled — the most-violated boundary rule.
property p_payload_stable_under_stall;
@(posedge clk) disable iff (!rst_n)
(in_valid && !in_ready) |=> (in_valid && $stable(in_payload));
endproperty
// 2. No unknowns in CONTRACT-REQUIRED fields at the accept event. Note it does
// NOT check the whole payload — Section 49 explains why that matters.
property p_no_x_on_accept;
@(posedge clk) disable iff (!rst_n)
(in_valid && in_ready) |-> (!$isunknown({in_payload.cls,
in_payload.first,
in_payload.last,
in_payload.strb}));
endproperty
// 3. Object framing: first and last frame correctly, no interleaving.
property p_framing_well_formed;
@(posedge clk) disable iff (!rst_n)
(in_accept && in_payload.first)
|=> !(in_accept && in_payload.first)[*0:$] ##1 (in_accept && in_payload.last);
endproperty
// 4. Reset sequencing — the integrator's half of Section 26's contract.
property p_no_traffic_before_reset_release;
@(posedge clk)
(!rst_n) |-> (!in_valid);
endproperty
// 5. No transaction accepted for a disabled feature.
property p_no_accept_when_feature_disabled;
@(posedge clk) disable iff (!rst_n)
(in_valid && in_ready) |-> cfg_active_q.proto_en[in_payload.cls];
endproperty
// 6. A response belongs to a live request — the integrator's client contract.
property p_response_matches_live_request;
@(posedge clk) disable iff (!rst_n)
rsp_valid |-> outstanding_q[rsp_tag];
endpropertySix properties, and they divide into two kinds.
Properties 1 to 4 check the integrator's behaviour. They fire when the surrounding logic violates the contract — unstable payloads, X on meaningful fields, malformed framing, traffic before reset release. These are the ones that save the vendor from escalations, because they identify the integration as the cause in the integrator's own simulation, before anybody files a bug.
Properties 5 and 6 check the interaction. They can fire from either side, and that is fine — the point is that the violation is detected at the boundary where both sides are visible, rather than three blocks downstream.
Why property 2 is deliberately narrow. §49.
Why this collateral is worth building. An integrator without it discovers a contract violation as a functional failure inside the IP, files it against the IP, and the vendor spends a week proving the stimulus was illegal. With it, the integrator's own regression says "your payload changed while my ready was low" on day one. The cost is a few dozen lines that the vendor writes once.
49. Wrong X-Checking — Every Bit, Always
// WRONG — asserts that the entire payload is known at every accept.
property p_no_x_anywhere;
@(posedge clk) disable iff (!rst_n)
(in_valid && in_ready) |-> !$isunknown(in_payload);
endpropertyWhy this is wrong rather than merely strict. The strb field exists precisely so that some data bytes are meaningless on a given beat. Unused metadata bits may be intentionally undriven. X on a don't-care bit is legal and, in a gate-level or power-aware simulation, expected.
What happens. The assertion fires constantly on legal stimulus. Three responses follow, and all three are bad:
It is waived, and the waiver is written broadly enough to also waive the real X that matters.
It is disabled, and the boundary loses X-checking entirely.
Or the design is changed to drive don't-care bits to zero — which costs area and power, adds toggling on a die-to-die interface where toggling is expensive, and removes the one signal that would have shown an uninitialised path.
The rule. Check meaningful bits at meaningful events. The meaningful bits are the ones the contract says the receiver interprets; the meaningful event is the accept. A protocol check that fires on legal stimulus does not make verification stricter — it makes the assertion report unreadable, and an unreadable report is where real failures go to be ignored. This generalises well beyond X-checking, and Chapter 20.1 makes it a principle of the verification module.
50. The IP Verification Package
What world-class IP delivers alongside the RTL. This is a list of artefacts, not a repository build.
| Artefact | What it is | What it prevents |
|---|---|---|
| Interface monitors | passive observers of each boundary, emitting transactions | every integrator writing a slightly different monitor |
| Bindable contract checkers | §48's properties, packaged for bind | the integrator re-deriving the contract from the RTL |
| Configuration legality model | an independent implementation of §13 | a testbench that inherits the design's misconceptions |
| Transaction reference model | expected behaviour derived from the contract | a scoreboard that mirrors the DUT (19.5 §56) |
| Coverage model | §35's parameter matrix plus functional coverage | "we ran it" being mistaken for "we covered it" |
| Smoke sequences | a short, self-checking bring-up test per configuration | integration failures discovered at system level |
| Integration guide | the reset matrix, clock contract, timing contract, envelope | §28, §30, §33 |
Three notes.
The legality model is deliberately independent and is the artefact most likely to be skipped. Shipping the design's own function as "the model" is worse than shipping nothing, because it looks like verification collateral and validates nothing.
The smoke sequences must be per configuration, not per block. A smoke test that only runs the nominal configuration is a smoke test for one product.
And the integration guide is the artefact that actually gets read. Everything in §26, §30, §32 and §34 exists to go in it. An IP delivery whose integration guide is a port list has delivered RTL and called it IP.
51. Golden Configuration Tests
For every structural parameter, run a fixed set of values chosen because each one breaks a different assumption.
| Value | What it catches |
|---|---|
| 1 | $clog2(1) = 0; single-requester arbitration; degenerate loops (§46) |
| 2 | the smallest non-degenerate case; the smallest ping-pong |
| 3 | non-power-of-two — pointer wrap that assumes a mask (19.4 §13) |
| a power of two | the width edge — $clog2(DEPTH) versus $clog2(DEPTH+1) (19.5 §11) |
| power of two + 1 | pointer wrap and the width edge together |
| the maximum supported | §45's intermediate-width bug; §33's fan-in |
| an invalid neighbour | that §10's elaboration assertions actually fire |
Two observations.
Three is the most productive single value in the table. A depth of 3 breaks any pointer that wraps by masking, any occupancy that assumes a power of two, and any address decode that assumes alignment — and it costs one extra regression run.
And the invalid neighbour is the row nobody runs. It takes seconds, it fails at elaboration, and it is the only evidence that the legality checks are connected to anything. An elaboration assertion that has never fired is indistinguishable from one that was accidentally deleted.
52. Parameter Mutation Testing
Take a passing regression, change one parameter, and run it again unchanged.
What this tests is not the design. It is the testbench.
A verification environment that has only ever run one configuration accumulates assumptions: an array sized to the default class count, a loop bound written as a literal, a coverage bin list enumerating three classes, an expected-latency constant measured once. None of these is visible while the configuration never changes.
The procedure:
- take the nominal regression, passing;
- change exactly one structural parameter to another value from §34's verified column;
- rerun with no other change;
- anything that fails is either a real design bug or a testbench assumption — and both are findings.
The result that matters most is the one that looks like a false alarm. A testbench failure is not noise; it is proof that the environment cannot verify the configuration space the design claims to support, which means every "passing" result for the non-default configurations was meaningless. §53 is what that looks like.
53. Wrong Testbench — Hardcoded Defaults
// WRONG — the environment assumes the default configuration.
class ucie_scoreboard;
int expected_credit[3]; // NUM_CLASSES is 3 "by default"
int outstanding[64]; // "the maximum is 64"
...
endclassThe design is elaborated with NUM_CLASSES = 4. The scoreboard array is size 3.
What happens. An object of class 3 arrives. The scoreboard indexes out of range. Depending on the language and the tool, that is an immediate error, a silent wrap to index 0, or a null-handle dereference. The verification environment fails before the design has a chance to.
Four properties.
It is reported as a testbench problem and de-prioritised. "The scoreboard crashed" gets a quick fix — often widening the array — and nobody asks what else in the environment was sized against the default. The next parameter change finds the next one.
The silent-wrap variant is much worse. Class 3's credits accumulate in class 0's slot. The scoreboard now checks a fiction and reports it as passing, so a four-class configuration is "verified" by an environment that models three.
Coverage lies in the same way. A coverage model with three class bins reports full class coverage in a four-class design.
And the root cause is the same as §8's. If the testbench imports the same parameter package as the design, the mutation of §52 changes both, and the assumption is never exposed. The environment must take its configuration from the elaborated design's capability structure (§20) or from an explicit test-level parameter — never from a shared default it can silently agree with.
The rule. Reusable RTL requires reusable verification. A parameterised design verified by a fixed-size environment has a verified configuration space of exactly one point, whatever the envelope table claims.
54. Version Compatibility
A reusable block evolves. Its public contract must evolve compatibly or be explicitly versioned.
Three rules, in order of how often they are broken.
Never change the meaning of an existing configuration field. If bit 3 meant "enable replay" in version 1.0 and means "enable replay with the extended window" in 1.1, every existing driver silently requests something different. This is the rule most often broken, because the change looks small and self-evidently better.
Add capability bits rather than reinterpreting existing ones. Software that does not know about a new feature leaves its bit at zero and gets the old behaviour. The old behaviour must therefore remain reachable, which is the actual constraint the rule implies.
And version the contract, not the implementation. An internal redesign that preserves every boundary behaviour is not a new contract version, and bumping the version for it forces integrators to re-qualify for nothing. Conversely, a one-line change that alters a boundary's timing class is a contract change, however small it looks in the diff.
55. Backward Compatibility Testing
Load a configuration that was legal in the previous version into the new one. There are exactly two acceptable outcomes:
It works identically — every boundary behaviour observably unchanged; or
it is explicitly rejected with ERR_CONFIG (§39) and a version indication that tells software why.
The unacceptable outcome is the third one: it is accepted and behaves differently.
Three reasons this test is worth its cost.
It is cheap. The previous version's regression configurations already exist. Running them against the new build is a rerun, not new work.
It catches the §54 violation directly. A reinterpreted configuration bit produces "accepted, behaves differently" — the exact third outcome — and nothing else in a normal regression looks for it, because the new build's own tests use the new meaning.
And it is the only test that protects deployed software. Every other test verifies the block against its current specification. This one verifies it against the specification somebody else already wrote code for, which is the one that costs money when it breaks.
56. Flagship Trace 1 — A Configuration Commit Under Traffic
Illustrative. Software changes from x16 / one protocol / replay-on to x8 / two protocols / replay-off, with traffic in flight.
| Cycle | cfg_state_q | Requested | Active | cfg_epoch | Admission | Outstanding | Datapath sees |
|---|---|---|---|---|---|---|---|
| 100 | CFG_IDLE | — | x16, 1p, replay | 7 | open | 5 | x16, 1p, replay |
| 101 | CFG_IDLE | written (3 registers) | x16, 1p, replay | 7 | open | 5 | x16, 1p, replay |
| 104 | CFG_VALIDATE | x8, 2p, no replay | x16, 1p, replay | 7 | open | 5 | x16, 1p, replay |
| 105 | CFG_QUIESCE | x8, 2p, no replay | x16, 1p, replay | 7 | stopped | 5 | x16, 1p, replay |
| 120 | CFG_QUIESCE | x8, 2p, no replay | x16, 1p, replay | 7 | stopped | 2 | x16, 1p, replay |
| 148 | CFG_QUIESCE | x8, 2p, no replay | x16, 1p, replay | 7 | stopped | 0 | x16, 1p, replay |
| 149 | CFG_COMMIT | x8, 2p, no replay | x16, 1p, replay | 7 | stopped | 0 | x16, 1p, replay |
| 150 | CFG_RESUME | x8, 2p, no replay | x8, 2p, no replay | 8 | stopped | 0 | x8, 2p, no replay |
| 151 | CFG_IDLE | x8, 2p, no replay | x8, 2p, no replay | 8 | open | 0 | x8, 2p, no replay |
Four readings.
The three register writes at cycle 101 change nothing the datapath can see. They land in cfg_requested_q. That is the entire content of §23's fix, and it is visible as the unchanged "Datapath sees" column between cycles 101 and 149.
Quiescence took 44 cycles — the time for five outstanding operations to complete. That is not overhead to optimise away: committing at cycle 105 with five operations outstanding would change the rules those operations complete under, which is §24's p_commit_only_when_quiesced.
The commit is one cycle and the epoch moves with it. Cycle 149 is CFG_COMMIT; at cycle 150 both the configuration and the epoch have moved. No cycle exists in which one has changed and the other has not.
And admission reopens only at cycle 151, one cycle after the commit, so the first newly admitted object sees a fully settled configuration under epoch 8. There is no object anywhere in this trace whose lifetime spans two epochs, which is what §24's fifth property asserts.
57. Flagship Trace 2 — Structural Capacity Versus Runtime Width
Illustrative. NUM_LANES = 32, product enables 16, a lane fails, repair degrades to 8, a later retrain restores 12.
| Event | Structural (NUM_LANES) | Configured mask | Active mask | What the datapath uses |
|---|---|---|---|---|
| elaboration | 32 | — | — | 32 lanes of hardware exist |
| product configuration | 32 | 16 | — | 16 lanes enabled |
| training complete | 32 | 16 | 16 | x16 |
| lane 5 fails | 32 | 16 | 16 | x16 — error raised |
| repair / renegotiation | 32 | 16 | 8 | x8 |
| later retrain | 32 | 16 | 12 | x12 |
| POR | 32 | defaults | cleared | re-derived from training (§27) |
Four readings.
The first column never changes. Thirty-two lanes of datapath, deskew and repair logic exist in the silicon throughout, in a product that uses at most 16 of them. That is the cost of the structural parameter, paid once at synthesis — and §34's envelope table is where the decision to pay it should have been recorded.
The second column changes only by configuration commit, never by hardware. The third changes only by hardware, never by software. Two columns, two owners, and §25's subset chain binds them: active ⊆ configured ⊆ built holds at every row.
The x12 row is the one that catches designs out. A design that assumes degraded widths are powers of two — because the initial widths are — breaks on a repair that yields 12. Whether 12 is reachable is architecture-defined, and a block that supports arbitrary masks must not have a power-of-two assumption anywhere in its striping arithmetic.
And everything derived from the active width is now dynamic. Bandwidth estimates, watermarks and headroom (19.4 §29) were computed from a width that changed three times in this trace. A headroom constant sized for x16 is wrong at x8 — in the safe direction, as it happens, which is why it survives — and a throughput expectation sized for x16 is wrong at x8 in the direction that generates a bug report.
58. Flagship Trace 3 — A Feature-Disabled Build
Illustrative. REPLAY_ENABLE = 0, with §14's stable boundary and correct tie-offs.
| Cycle | Event | replay_slot_available | replay_active | Admission | Result |
|---|---|---|---|---|---|
| 0 | idle | 1 (tied) | 0 (tied) | open | — |
| 10 | object admitted | 1 | 0 | open | accepted — no slot reserved |
| 11 | object transmitted | 1 | 0 | open | — |
| 12 | integrity error at the peer | 1 | 0 | open | — |
| 13 | peer signals a bad object | 1 | 0 | open | ERR_PROTOCOL raised |
| 14 | — | 1 | 0 | open | no retransmission — by design |
| 15 | the client is told | 1 | 0 | open | the semantic operation fails upward |
Four readings.
The boundary is identical to the replay-enabled build. replay_slot_available and replay_active both exist and are both driven. No wrapper change, no ifdef, no configuration-specific integration — §15's failure avoided.
The tie-off values are correct, not merely defined. replay_slot_available = 1 means admission does not reserve a slot, which is right when there are no slots (19.3 §10's conjunction still holds, with that term always true). A tie to zero would stall the link permanently — which is §11's outcome reached from a tie-off instead of a depth.
The error is reported, not swallowed. The object fails and the client is told. A reliability-free configuration is not an error-free configuration; it is one where errors are escalated rather than repaired, and the difference must be visible at the boundary or the integrator will assume errors cannot happen.
And §47's p_no_replay_when_disabled is what makes this trace verifiable. Without it, a generate branch that occasionally pulsed replay_active in a replay-disabled build would go unnoticed — and it would tell the integrator's logic that a retransmission is in progress in a design that cannot retransmit.
59. The Reusable-IP Scoreboard
FOUR MODELS, because a reusable block can be wrong in four independent ways.
Every value is derived from OBSERVATION or from the CAPABILITY STRUCTURE —
never from the design's own configuration logic (§13, §53).
CONFIGURATION MODEL
structural_capability read from ip_capabilities_t at time zero
requested_cfg from observed register writes
validated_cfg from an INDEPENDENT legality model
active_cfg from observed commit events
cfg_epoch incremented on observed commits
CHECK: active_cfg is always legal
CHECK: active_cfg changes only at an observed commit
CHECK: active_cfg ⊆ structural_capability
TRANSACTION MODEL
accepted objects observed entering, by boundary
delivered objects observed leaving
outstanding accepted − delivered
cfg_epoch_at_accept per object
CHECK: every accepted object is eventually delivered or explicitly failed
CHECK: no object's cfg_epoch changes during its lifetime (§24)
CHECK: no object accepted for a disabled feature (§47)
RESOURCE MODEL
queue_occupancy derived from observed pushes and pops
credit_state 19.5 §55's model, per class
replay_occupancy derived from observed commits and resolutions
high_water per structure
CHECK: every occupancy ≤ its parameter-derived bound
CHECK: high_water reported as SIZING EVIDENCE, not as an error
HEALTH MODEL
reset_events by kind (§27)
state_before / state_after for every row of the reset matrix
recovery_events entries, exits, durations
first_error class, context, epochs
CHECK: one assertion PER ROW of the reset matrix (§29)
CHECK: first_error is sticky-first and survives recovery (§40)
CHECK: error class is actionable — no ERR_FATAL for a config mistakeWhy four models rather than one.
They fail independently, and the failure identifies the layer. A configuration-model failure means the commit machinery is wrong. A transaction-model failure means work was lost. A resource-model failure means a structure overflowed or a bound is wrong. A health-model failure means state was cleared that should have survived. Merging them into one scoreboard loses the ability to say which, which is the same argument 19.5 §55 makes about credit models.
The health model is the one specific to reuse. No other chapter needs it: it exists because §27's matrix is a deliverable, and a deliverable table needs a model that checks every row. Most reusable-IP scoreboards omit it entirely, which is why §28-class bugs reach integration.
And the configuration model must derive validated_cfg independently. §13's rule, once more, because this is the third place it can be violated and the easiest one to violate accidentally — the design's function is right there, exported, and calling it is one line.
60. Coverage
// ILLUSTRATIVE. Configuration coverage is different from functional coverage:
// most of these bins are sampled ONCE PER BUILD, not once per transaction.
covergroup cg_ip_config;
// --- Structural: sampled at elaboration, one bin per build ---
cp_lanes: coverpoint NUM_LANES {
bins one = {1}; bins small = {2, 4}; bins mid = {8, 16}; bins max = {32};
}
cp_depth: coverpoint TX_DEPTH {
bins one = {1}; // §46
bins two = {2};
bins non_pow2 = {3, 5, 7, 17}; // §51 — the productive row
bins pow2 = {4, 8, 16, 32, 64};
bins maximum = {256}; // §45's flagship bug lives here
}
cp_classes: coverpoint NUM_CLASSES {
bins one = {1}; // §46 — zero-width class index
bins few = {2, 3}; bins many = {8}; // §33 — the fan-in limit
}
cp_features: coverpoint {REPLAY_ENABLE, MULTI_PROTOCOL_ENABLE,
FRAGMENTATION_ENABLE, ASYNC_PHY_BOUNDARY} {
bins all_off = {4'b0000};
bins all_on = {4'b1111};
bins others[] = default; // §35's pairwise tier
}
// --- Configuration lifecycle: sampled per commit attempt ---
cp_cfg_outcome: coverpoint cfg_state_q {
bins committed = (CFG_VALIDATE => CFG_QUIESCE => CFG_COMMIT);
bins rejected = (CFG_VALIDATE => CFG_REJECT);
bins timed_out = (CFG_QUIESCE => CFG_REJECT); // §22's easily-omitted arc
}
cp_cfg_under_traffic: coverpoint outstanding_count iff (cfg_write_done) {
bins idle = {0};
bins busy = {[1:$]}; // §56 — the case that matters
}
// --- Reset coverage: one bin per matrix row per reset kind (§27) ---
cp_reset_kind: coverpoint reset_kind {
bins por = {RST_POR}; bins soft = {RST_SOFT};
bins cfg = {RST_CFG}; bins diag = {RST_DIAG};
}
cp_reset_with_state: coverpoint state_populated iff (reset_fire) {
bins empty = {1'b0};
bins occupied = {1'b1}; // the ONLY interesting case
}
// --- Runtime versus structural (§25, §57) ---
cp_active_width: coverpoint $countones(active_lane_mask_q) {
bins full = {NUM_LANES};
bins degraded = {[1:NUM_LANES-1]};
bins odd = {3, 5, 6, 7, 12}; // §57 — non-power-of-two widths
}
// --- Errors: every class must be produced by some test (§39) ---
cp_err_class: coverpoint first_err_q.err_class {
bins recoverable = {ERR_RECOVERABLE}; bins protocol = {ERR_PROTOCOL};
bins resource = {ERR_RESOURCE}; bins config = {ERR_CONFIG};
bins internal = {ERR_INTERNAL}; bins fatal = {ERR_FATAL};
}
// --- Crosses ---
x_reset_kind_state: cross cp_reset_kind, cp_reset_with_state;
x_features_depth: cross cp_features, cp_depth;
x_cfg_under_traffic: cross cp_cfg_outcome, cp_cfg_under_traffic;
endgroupFour notes on what these bins are for.
cp_depth.maximum is §45's detector. The intermediate-width bug only appears at large depths. If the maximum depth is never built, the flagship bug is unverified — and it is unverified in exactly the SKU it will ship in.
cp_reset_with_state.occupied is the only interesting reset bin. A reset applied to an empty block preserves nothing and clears nothing observable. Every row of §27's matrix is only testable when the state is populated, so a reset regression that resets an idle design has covered nothing.
cp_cfg_under_traffic.busy is §56's trace. A configuration change on an idle block never exercises quiescence, which means §23's failure mode is untested.
And cp_err_class must be fully covered by deliberate injection. Every class needs a test that produces it — including ERR_INTERNAL, which requires forcing an internal invariant to fail. An error class that no test produces is an error class whose reporting path has never been exercised, and it will be exercised for the first time in the field, at the worst moment.
61. Debug Taxonomy
Seven symptoms that are specific to reusable IP, each with a first place to look.
The default SKU works; a larger SKU fails. A width or fan-in assumption that scales with a parameter. Look for literal widths first (§45), then for a critical path that grows with a parameter (§33). The corruption direction — accepting when full — points at an arithmetic wrap rather than a logic error.
A feature-disabled build fails to compile, elaborate, or simulate. The generate contract. Look for signals driven in one branch and not the other (§15), then for the tie-off values being wrong rather than missing (§58).
Only a non-power-of-two depth fails. A pointer or address assumption that masks rather than compares. Depth 3 is the fastest reproducer (§51), and the bug is almost always in wrap arithmetic or a full/empty comparison.
Integration fails after an internal redesign that changed no behaviour. The public contract exposed an implementation detail (§18). Look at what the integrator's logic reads, not at what changed inside.
Recovery loses work in one SKU and not another. The reset matrix differs accidentally between builds — usually a generate branch whose reset expression is not identical to its sibling's (§14). Check §29's preserve assertions in the failing configuration specifically; they are frequently bound only in the nominal build.
Assertions fail only when a parameter is 1. A zero-width $clog2 or a degenerate loop (§46). The give-away is that many unrelated assertions fail together, because one width expression collapsed everywhere at once.
Verification fails before the design does. The testbench assumed a default (§53). This is a finding, not noise — it means every prior "pass" for non-default configurations was meaningless, and the fix is to source the environment's configuration from the capability structure rather than from a shared parameter package (§8).
62. Common Misconceptions
"Reusable RTL means adding parameters." A parameter is a claim that the block works across a range. A claim nobody checked is a defect with a delivery date, and §34's envelope table is what makes claims honest.
"If it compiles, the configuration is supported." §11 compiles, elaborates, simulates, passes a smoke test, and loses a transport object on the first error. Compilation checks syntax; legality checks architecture, and only §10 and §13 do that.
"Every parameter should be runtime configurable." Then synthesis instantiates the maximum hardware for every product, the multiplexer depth scales with the unused maximum, and most reachable configurations are physically impossible. §5.
"Every value should be a parameter — it's cheaper." Then arbitration weights and timeout values are frozen at elaboration, when the information needed to choose them does not yet exist, and every tuning request is a silicon release. §6.
"Every product should use one universal top module." Then the top module's legal configuration space is the product of a dozen booleans, of which a handful are meaningful and none are enumerated. §37.
"One giant generic buffer increases reuse." It is seven components in one file with a shared namespace and a union port list. They share a memory array; they do not share an ownership contract, and the contract is where the bugs are. §36.
"Testing the default configuration is enough." It verifies exactly one point in the space the envelope table claims to support — and it hides every testbench assumption, so the other points are not merely untested but untestable. §52.
"Reusable IP should expose every internal state so integrators can debug." Then every internal register becomes a compatibility constraint and the block cannot be redesigned. Expose rates and pressures, not mechanisms. §42.
"Reset is an integration detail." It is the most common integration failure and the hardest to reproduce, because the failures are metastable, intermittent and corner-dependent. §28.
"Timing is physical design's problem." A block whose arbiter depth scales linearly with a parameter has a functional range wider than its physical range, and the fix at closure time changes the latency contract. §33.
"Parameter values do not affect architecture." NUM_CLASSES = 1 changes whether arbitration exists. REPLAY_DEPTH = 0 changes whether an ownership handoff exists. Several parameters select between architectures, not between sizes.
"Generate blocks automatically produce clean interfaces." They produce whatever the branches drive. A missing else branch produces a boundary that changes shape with the configuration, and the integrator's wrapper absorbs the damage. §15.
"Verification collateral can be product-specific." Then every integrator re-derives the contract from the RTL and gets it subtly wrong, and the vendor debugs integrations for a living. §50.
"Backward compatibility only matters to software." It is the only test that verifies the block against the specification somebody already wrote code for — and "accepted, behaves differently" is a failure no other test looks for. §55.
"A generic scoreboard can call the design's legality function." Then it agrees with the design about every configuration, including the ones the function gets wrong. §13, §53, and 19.5 §56.
63. Understanding Check
64. Module 19 in One Page, and What Comes Next
Six chapters built an implementable UCIe link.
19.1 — Link Architecture set the top level: the block partition, the state-ownership table, the reset hierarchy, and the rule that a recovery must not clear state whose meaning outlives the link event.
19.2 — Protocol Engines established that a protocol engine converts a semantic obligation into transportable objects without transferring ownership of the obligation — so allocation happens at client acceptance, identity carries a generation, multi-part completion needs a bitmap rather than a counter, and the semantic table survives a recovery.
19.3 — Adapter Design established that the Adapter owns transport reliability and not higher-level semantics — reservation before ownership transfer, staging and replay as separate lifetimes, integrity aligned to its own data, a duplicate window rather than a last-sequence register, and retirement on resolution.
19.4 — UCIe Buffering established that a buffer stores obligations rather than bytes — seven kinds with seven lifetimes and four sizing rules, derived occupancy, one accept event for payload and metadata, headroom computed from the backpressure round trip, and a consumer that releases the ping-pong bank.
19.5 — Flow-Control Logic established that a credit is a claim on storage this side cannot see — three capacity numbers plus a fourth with no register, signed arithmetic before truncation, consumption on a commitment rather than a valid or a grant, a per-entry return bit and an epoch, and a batching threshold that deadlocks a working link unless something else can flush it.
And this chapter established that reusable IP is a contract, not a parameter list — four configuration classes that must not mix, a legal-configuration space that is enumerated and checked at elaboration, a boundary that survives an internal redesign, a reset matrix delivered as a table, reuse by ownership contract rather than by storage shape, and a verification package that ships with the RTL.
One thread runs through all six. Every chapter's worst bug was a design in which every block was locally correct and one relationship was wrong: an obligation released by a transport signal, a local event given a global scope, a counter used where a set was needed, state advanced on a grant rather than a transfer, a number correct for one parameter and wrong for the next. Local correctness is not the property that matters; the relationships between blocks are.
Which is exactly why the next module asks the opposite question.
Everything in Module 19 was designed by reasoning about what the implementation should do. Module 20 asks how to prove, independently of that implementation, that the link obeys its contracts — how to build monitors that do not inherit the design's misconceptions, reference models that are not mirrors, and a layered decomposition that says which layer first diverged rather than merely that the output was wrong. The scoreboard that agrees with the design about the very thing that is wrong has appeared in three chapters now. The next module is about not building it.
- 20.1 — Protocol Verification — per-layer protocol-rule checks.
Browse the full path on the UCIe tutorials index.