UCIe · Module 20
UVM Architecture for UCIe
Assembling a UVM environment for a layered, bidirectional, multi-protocol UCIe endpoint — agents chosen by the DUT boundary, four transaction layers instead of one mega item, sequence layering that composes scenarios across interfaces, a reset coordinator that distributes meaning rather than a wire, a drain condition that stops a test passing with obligations live, and observed coverage rather than intended coverage.
Chapters 20.1 to 20.5 produced everything a UCIe verification plan needs: contracts, a link-level composition, temporal properties, obligation models and a coverage model. All five assumed something would run them. This chapter builds that something — and its main job is to wire responsibilities together without letting the framework's mechanics obscure the protocol reasoning those five chapters established.
1. The One-Sentence Model
A UVM environment is a wiring architecture for responsibilities. It exists to make stimulus ownership explicit, observation passive, reference models independent, reset and recovery coordinated, multi-interface activity composable, and test intent visible — and it fails when the framework's mechanics become the thing the reader has to understand instead of the protocol.
That framing decides every choice below. A uvm_config_db entry, a factory override and an analysis FIFO are all correct in some place and wrong in most places; what settles each case is whether it makes a responsibility clearer or hides one.
2. What This Chapter Owns
| Question | Where it is answered |
|---|---|
| Boundary contracts, the five verification planes, model independence, safety versus liveness | 20.1 — Protocol Verification |
| The link lifecycle, reference link model, fault-injection points, recovery scenarios | 20.2 — Link Verification |
| Temporal properties, triggers, scoping, vacuity, the assertion inventory | 20.3 — UCIe Assertions |
| Obligation models, correlation, epochs, first divergence, drain checks | 20.4 — UCIe Scoreboards |
| The coverage model and its collectors | 20.5 — UCIe Functional Coverage |
| Conformance, interoperability and official compliance | 20.7 — UCIe Compliance Testing |
This chapter does not re-teach UVM. It assumes agents, sequencers, drivers, monitors, subscribers, the factory and the phasing exist and are understood. What it teaches is the seven decisions that are specific to a layered bidirectional protocol and that a generic UVM tutorial cannot reach:
Topology driven by the DUT boundary (§6–§10), because "one UCIe agent" is the wrong answer three different ways depending on what is being verified.
Four transaction layers (§11–§16) rather than one item with eighty optional fields — the same three-level identity hierarchy 20.1 §22 established, plus link events.
item_done() is not semantic completion (§23–§25) — the flagship UVM mistake for a protocol with retries and recovery, and it produces sequences that work against a toy driver and fail against a real one.
A drain condition rather than a delay (§26–§30), because a test that drops its objection with thirty obligations live exits PASS.
A reset coordinator that distributes meaning (§31–§35). If every component clears its queues on the reset wire, a recovery wired to that wire hides 20.4 §31's design bug — and this chapter's centerpiece is the table that prevents it.
Observed rather than intended coverage (§51–§53), because a sequence that asked for a retry and a DUT that never retried are the same thing to stimulus-side sampling.
And a peer that is adversarial (§57–§59), because an accommodating peer produces a green regression and a link that fails against the first real one.
3. Sourcing
4. An Environment Is a Wiring Architecture
Seven responsibilities, and the environment's whole job is to keep them separate.
| Responsibility | Owned by | Must not |
|---|---|---|
| generate stimulus | sequences, via sequencers and drivers | observe or check |
| drive a boundary | a driver, at exactly one boundary | decide what to send |
| observe | a monitor, passively | check, sample coverage, or log |
| hold expectations | reference models (20.4) | read design internals |
| relate observations | the scoreboard's join | manufacture correlation |
| measure situations | coverage subscribers (20.5) | sample intent |
| coordinate lifecycle | the test and the reset coordinator | be duplicated per component |
Three rules that fall out of the table.
Each row has exactly one owner. A monitor that also checks (§38), a driver that also samples coverage (§53), or a sequence that also injects a fault by force (§22) has taken a second responsibility — and the second one is always the one that becomes wrong invisibly.
Row 7 is the one most environments get wrong by distributing it. Every component implementing its own reset behaviour is not decentralisation; it is seven independent decisions where one policy was needed (§33).
And the framework is not on this list. The factory, the config database and TLM ports are mechanisms for wiring these responsibilities. When a reader has to understand a factory override chain to know which driver runs, the mechanism has become a responsibility — §49.
5. The Environment
Four things to read, and two absences.
The virtual sequence reaches sequencers, never drivers (§22). Three arrows leave it and all three land on agents.
Monitors publish to two independent consumers. Models and coverage subscribers both receive the observation stream and neither knows about the other — which is what lets either be disabled without touching the other (20.5 §8).
Assertions bind to the design, not through the environment. They are collateral that an integrator can bind with no environment at all (19.6 §48), and drawing them inside the environment would suggest otherwise.
The reset coordinator feeds the models, not the components' reset inputs. It distributes a semantic reset kind (§34); it does not gate anybody's logic.
Absence one: no arrow from the DUT into a model. Every model input is a monitor observation (20.4 §6).
Absence two: no arrow from an agent to a model. An agent knows what it asked for; a model must know what happened (§52).
6. The DUT Boundary Decides the Topology
There is no "the UCIe environment". The topology follows from what is inside the DUT, and getting this wrong produces either a driver on an internal boundary or a missing stimulus source.
The rule:
An active agent belongs at every boundary the environment owns. A passive monitor belongs at every boundary the environment observes. No boundary gets both a driver and a monitor-only role, and no internal boundary gets a driver at all.
Three questions that determine the topology:
Which boundaries are outside the DUT? Those are candidates for active agents.
Which of those does the environment need to control rather than merely observe? A boundary whose behaviour is entirely determined by the DUT needs no driver.
And which boundaries are inside the DUT? Those get passive monitors only — and they are exactly the boundaries whose observation makes 20.1 §7's layer-divergence analysis possible.
7. Three DUT Scopes, Three Environments
| DUT scope | Active agents | Passive monitors | Notes |
|---|---|---|---|
| A whole link endpoint | protocol client; peer / far end; management; fault | protocol boundary, Adapter boundaries, PHY-facing, management | the topology §5 draws |
| The Adapter alone | both of its boundaries — protocol-facing and PHY-facing; fault | its internal stage boundaries, if observable | two active agents, one on each side |
| The Protocol Layer alone | client side; Adapter-facing responder; fault | internal engine boundaries | the Adapter side becomes a responder |
| The Physical Layer alone | Adapter-facing; the link partner's PHY; fault | training and calibration boundaries | a different discipline; 8.3 and 8.4 own it |
Three observations.
The Adapter-alone scope has active agents on both sides. Its protocol-facing boundary receives objects and its PHY-facing boundary must be driven by something that behaves like a physical layer. Neither side is "the stimulus side", which is why a topology assuming one upstream agent and one downstream monitor does not fit.
Raw Mode changes the endpoint topology, not just the checks. With the Adapter bypassed (§3), the protocol boundary connects toward RDI-like abstraction directly — so the Adapter monitor has little to observe and the reliability responsibilities move to the protocol side (20.1 §51). The environment is the same shape with different weights, which is the argument for building all four monitors even in an Adapter-managed configuration.
And the fault agent appears in every row. Fault injection is a boundary-contract activity (20.2 §43) and it exists at every scope — which is why it is a component rather than a habit scattered through drivers (§60).
8. Agent Decomposition
// ILLUSTRATIVE. One agent per boundary the environment owns. Each has its own
// config, its own sequencer, and its own driver — and its monitor is the
// boundary's observer regardless of whether the agent is active.
class ucie_protocol_agent extends uvm_agent;
`uvm_component_utils(ucie_protocol_agent)
ucie_protocol_agent_cfg cfg;
ucie_protocol_sequencer seqr; // built only when active
ucie_protocol_driver drv; // built only when active
ucie_protocol_monitor mon; // ALWAYS built (§9)
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(ucie_protocol_agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal(get_type_name(), "agent cfg not set")
mon = ucie_protocol_monitor::type_id::create("mon", this);
if (cfg.is_active == UVM_ACTIVE) begin
seqr = ucie_protocol_sequencer::type_id::create("seqr", this);
drv = ucie_protocol_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
mon.vif = cfg.vif;
if (cfg.is_active == UVM_ACTIVE) begin
drv.vif = cfg.vif;
drv.seq_item_port.connect(seqr.seq_item_export);
end
endfunction
endclassThree notes.
The monitor is built unconditionally. A passive agent is only a monitor, and an active agent still needs one — because the driver's intent and the boundary's behaviour are different things (§52).
The config arrives by uvm_config_db and the virtual interface arrives in the config (§46). Not two separate lookups, and not a get() inside the driver's run_phase.
And is_active is per agent, not per environment. An Adapter-scope environment has two active agents; an endpoint-scope environment has one active protocol agent and a monitor on the Adapter boundary. A single environment-wide active/passive switch cannot express either.
9. Active Versus Passive
The rule, and it has no exceptions:
A driver belongs only where the environment owns the stimulus. An internal DUT boundary gets a monitor and nothing else.
Why an internal boundary must never be driven, in three parts.
It destroys the layer-divergence analysis. 20.1 §7's whole value is knowing which layer first diverged. A driver on the Adapter boundary means the objects there came from the environment rather than from the protocol engine — so the protocol engine's contribution is no longer observable and the divergence table has a hole where the answer was.
It creates scenarios the design cannot reach. An object injected at an internal boundary may be one the upstream layer would never produce. The resulting failure is not a bug report (20.2 §43's rule about not forcing internal registers, applied to transactions).
And it hides the upstream layer entirely. If the Adapter is driven directly, the protocol engine is unexercised — and a regression can pass with the protocol engine essentially untested while every Adapter check is green.
The legitimate exception, and it is a different activity: a block-level environment whose DUT is the Adapter drives that boundary because it is now an external boundary (§7). The boundary did not change; the DUT scope did.
10. Wrong Agent — the One That Controls Everything
// WRONG — one agent, one driver, every boundary.
class ucie_agent_bad extends uvm_agent;
ucie_driver_bad drv; // drives protocol, peer, management AND faults
ucie_monitor_bad mon; // observes everything, publishes one stream
endclass
class ucie_driver_bad extends uvm_driver #(ucie_mega_item);
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
if (req.is_protocol) drive_protocol(req);
else if (req.is_peer) drive_peer(req);
else if (req.is_mgmt) drive_mgmt(req);
else if (req.is_fault) inject_fault(req);
seq_item_port.item_done();
end
endtask
endclassFive consequences.
Concurrency across boundaries becomes impossible. One driver processes one item at a time, so protocol traffic while the peer applies backpressure while a fault is injected — 20.5 §30's concurrency cases — cannot be expressed. The most valuable scenarios in the plan are unreachable by construction.
The if forest grows with every feature and most fields are invalid in most branches (§12).
Active/passive is all-or-nothing. An Adapter-scope reuse needs a driver on one boundary and a monitor on another; this agent has one switch.
Reset means one thing. §33's failure, guaranteed: one driver, one reset behaviour, and the four boundaries need different ones.
And the single monitor publishes one stream, so 20.1 §7's four-boundary divergence analysis is impossible — the environment cannot say which layer diverged because it never distinguished them.
11. Four Transaction Layers
20.1 §22's three-level identity hierarchy, plus link events. Four classes, four lifetimes, four owners.
| Class | Represents | Direction | Lifetime |
|---|---|---|---|
ucie_semantic_item | a client operation to be driven | stimulus — a sequence item | until its obligation is terminal |
ucie_object_obs | an observed transport object | observation — monitor output | until resolution |
ucie_attempt_obs | an observed physical attempt | observation | one attempt |
ucie_link_event | a lifecycle, configuration or fault event | observation | instantaneous |
Three rules.
Stimulus items and observations are different classes. A uvm_sequence_item carries randomisable constraints and is requested; an observation carries what happened. Using one class for both is how §52's intended-versus-observed confusion starts — the scoreboard cannot tell whether it is looking at a request or a result.
Observations are immutable once published. A subscriber that modifies an observation corrupts every other subscriber's copy. They should be treated as read-only, and where the language permits, made so.
And link events are not data items. Folding a recovery entry into the object transaction means every object carries a is_recovery field that is false almost always — which is §12, and it also means a recovery with no traffic in flight generates no event at all.
12. Wrong Mega Transaction
// WRONG — one class for everything.
class ucie_mega_item extends uvm_sequence_item;
rand bit is_pcie, is_cxl, is_streaming;
rand bit is_request, is_response, is_completion;
rand bit is_retry, is_first_attempt;
rand bit is_training, is_recovery, is_config_commit;
rand bit is_credit_consume, is_credit_return;
rand bit is_fault, is_peer_action;
rand int cls, obj_id, sem_id, attempt_num, epoch;
rand int width, rate, lane_mask;
rand bit [1023:0] payload;
rand int fault_kind, fault_delay, fault_target;
// ... and it keeps growing
endclassFive consequences, and the fifth is the one that outlives the project.
Most fields are invalid in most contexts. A recovery event has no payload, no class and no attempt number; a credit return has no semantic identity. So every consumer must know which fields are meaningful for which combination, and that knowledge lives nowhere.
Constraints become unwritable. A constraint block must exclude every invalid combination of sixteen booleans — and the combinations it fails to exclude are generated, producing items that mean nothing.
The driver and the scoreboard both grow if forests on the same discriminators, in two places, drifting apart.
Semantic boundaries disappear. 20.1 §22's three levels are three sets of fields in one class — so the ratio checks that need "one operation, two objects, three attempts" have nothing to count, because a single class cannot represent a one-to-many relationship.
And the class becomes unrefactorable. Every component depends on it, so splitting it later touches everything. The mega item is the single decision hardest to reverse in a UVM environment, which is why it is worth getting right on day one.
13. The Semantic Sequence Item
// ILLUSTRATIVE. STIMULUS. What a client asks the DUT to do. No wire format is
// claimed (§3) — the fields are the abstraction the environment drives at.
class ucie_semantic_item extends uvm_sequence_item;
`uvm_object_utils(ucie_semantic_item)
// What to do
rand op_kind_e op; // READ, WRITE, MESSAGE, ...
rand int unsigned cls; // resource class
rand bit [63:0] addr;
rand int unsigned length;
rand bit [7:0] data[];
// How to drive it — stimulus shaping, not protocol content
rand int unsigned pre_delay; // cycles before offering
rand int unsigned stall_pattern; // how the client backpressures
// Filled in by the DRIVER after acceptance, for the sequence's use (§25)
int unsigned accepted_tag; // the monitor tag, if observable
bit accepted;
constraint c_class { cls < NUM_CLASSES; }
constraint c_length { length inside {[1:MAX_LEN]}; data.size() == length; }
constraint c_delay { pre_delay inside {[0:32]}; }
function void do_copy(uvm_object rhs); /* ... */ endfunction
function string convert2string(); /* ... */ endfunction
endclassThree notes.
The item carries what to do and how to drive it, and nothing about what should happen. No expected response, no expected latency, no expected attempt count. Those belong to the reference model (20.4 §17) — an item carrying an expectation is a predictor embedded in stimulus, and §56 is why that is fatal.
stall_pattern is a first-class field, not an afterthought. 20.5 §21's stability and skid coverage requires a client that backpressures, and a client that never stalls leaves 20.3 §11's stability property with a one-cycle window.
And accepted_tag is written by the driver, after the fact. It is how a sequence that needs semantic completion finds its obligation (§25) — and it is the only field flowing backwards, which is deliberate and worth commenting in real code.
14. The Transport Observation
// ILLUSTRATIVE. OBSERVATION. What a monitor saw at the Adapter boundary.
// Immutable once published (§11).
class ucie_object_obs extends uvm_object;
`uvm_object_utils(ucie_object_obs)
longint cycle; // in THIS boundary's clock domain (20.4 §46)
int unsigned boundary_id;
int unsigned mon_tag; // the join key — verification-only (20.4 §8)
int unsigned obj_id_observed; // as seen; may be reused
int unsigned sem_tag; // the obligation this belongs to, if inferable
int unsigned cls;
int unsigned part_index; // for a fragmented operation
int unsigned cfg_epoch, link_epoch;
bit [7:0] payload_token[]; // may be abstracted for formal (20.3 §61)
endclassTwo notes.
cycle is per boundary domain, and the join is on the tag. 20.4 §46: comparing raw cycles across domains compares incomparable quantities, so correlation uses mon_tag and time is diagnostic context.
And sem_tag is "if inferable". 20.4 §23's honest caveat: where the association between an object and its operation is not directly observable, the monitor infers it — and the inference needs its own sanity checks, reported in the environment category rather than as a design failure.
15. The Link Event
// ILLUSTRATIVE. OBSERVATION. A lifecycle, configuration, resource or fault
// event. Separate from data because most of these carry no data at all (§11).
class ucie_link_event extends uvm_object;
`uvm_object_utils(ucie_link_event)
longint cycle;
evt_kind_e kind; // 20.4 §7's enumeration
int unsigned boundary_id;
// Lifecycle
ref_phase_e phase_from, phase_to;
int unsigned link_epoch;
// Configuration
int unsigned cfg_epoch;
cfg_snapshot_t requested, agreed, active;
// Resource
int unsigned cls, units;
int unsigned credit_epoch;
// Fault / diagnostic
fault_family_e fault_family; // 20.5 §24's taxonomy
int unsigned reset_kind; // §34
bit first_fault_valid;
endclassOne note that matters. The four groups of fields are disjoint by kind, which looks like §12's mega item and is not — because kind is a small closed enumeration whose meaning is fully determined, and every consumer switches on it exhaustively (20.4 §7's argument for one event type). The mega item's problem was sixteen independent booleans producing an unbounded combination space, not a tagged union with one discriminator.
16. Why Four Classes and Not One
| Mega item (§12) | Four layers (§11) | |
|---|---|---|
| Fields valid at any moment | a small minority | all of them |
| Constraints | must exclude a combinatorial space | local and writable |
| Represents one-to-many | cannot | naturally — sem_tag on several objects |
| Concurrency across boundaries | one driver, serialised (§10) | independent agents |
| Reset behaviour | one policy | per layer (§35) |
| Refactorable later | no — everything depends on it | yes, per class |
| Distinguishes intent from outcome | no — §52's failure | yes — stimulus versus observation |
The last row is the one that decides it. A single class used for both what a sequence asked for and what a monitor saw makes the two indistinguishable in the scoreboard and in the coverage model. §52's intended-versus-observed lesson is unreachable in an environment with one transaction class, because there is nothing to compare.
17. Sequence Layering
Three levels, and each has a different scope of authority.
| Level | Does | Knows about |
|---|---|---|
| Primitive | one basic action | one sequencer |
| Scenario | one protocol scenario | one sequencer, composing primitives |
| Virtual | coordinates several agents | several sequencers, via a virtual sequencer |
Two rules.
A primitive never composes and a virtual never drives. A primitive that internally sequences four actions is a scenario; a virtual sequence that calls a driver method has bypassed the sequencer (§22).
And this layering is where UVM earns its place for UCIe. 20.5 §30's concurrency cases — a retry during full-duplex traffic, a recovery with obligations live, a configuration commit under load — are all multi-agent, multi-boundary, time-coordinated scenarios, and a flat sequence library cannot express any of them.
18. Primitive Sequences
// ILLUSTRATIVE. One action each. Small, composable, individually reusable.
class ucie_send_one_req extends uvm_sequence #(ucie_semantic_item);
`uvm_object_utils(ucie_send_one_req)
rand op_kind_e op;
rand int unsigned cls;
rand bit [63:0] addr;
task body();
ucie_semantic_item it = ucie_semantic_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { it.op == local::op;
it.cls == local::cls;
it.addr == local::addr; })
`uvm_error(get_type_name(), "randomize failed")
finish_item(it);
// NOTE: finish_item returns when the DRIVER accepted it — NOT when the
// semantic operation completed. §23.
endtask
endclass
class ucie_client_stall extends uvm_sequence #(ucie_semantic_item);
rand int unsigned cycles;
task body(); /* hold the client's ready low for `cycles` */ endtask
endclass
class ucie_inject_one_fault extends uvm_sequence #(ucie_fault_item);
rand fault_family_e family;
rand int unsigned point; // 20.2 §43's seven injection points
task body(); /* one injection, at one point */ endtask
endclass
class ucie_request_recovery extends uvm_sequence #(ucie_mgmt_item);
task body(); /* one management-side recovery request */ endtask
endclass
class ucie_peer_hold_response extends uvm_sequence #(ucie_peer_item);
rand int unsigned cycles;
task body(); /* the far end withholds a response */ endtask
endclassTwo notes.
Five primitives, five sequencers. Each targets exactly one agent, so each is reusable in any scenario that needs that action — which is the whole reason for the level.
And the comment on finish_item is not decoration. It is the flagship mistake (§23), and putting the warning at the point of use is how it stops being made.
19. Scenario Sequences
// ILLUSTRATIVE. One protocol scenario, on one sequencer, composing primitives.
class ucie_burst_then_quiet extends uvm_sequence #(ucie_semantic_item);
`uvm_object_utils(ucie_burst_then_quiet)
rand int unsigned burst_len;
rand int unsigned quiet_cycles;
constraint c { burst_len inside {[8:64]}; quiet_cycles inside {[64:512]}; }
task body();
ucie_send_one_req req;
// The burst.
repeat (burst_len) begin
req = ucie_send_one_req::type_id::create("req");
if (!req.randomize()) `uvm_error(get_type_name(), "randomize failed")
req.start(m_sequencer, this);
end
// THE QUIET PERIOD — this is why the scenario exists. 19.5 §42's batching
// deadlock needs a burst followed by silence, and no constraint set that
// keeps traffic flowing can produce it (20.5 §54).
repeat (quiet_cycles) @(posedge cfg.vif.clk);
endtask
endclassTwo notes.
The quiet period is the content of this scenario, not a gap between tests. 20.5 §54: a burst followed by silence is unreachable at any seed count from a constraint set that keeps traffic flowing — and it is the precondition for 19.5 §42's deadlock and for 20.5 §23's partial-flush bin.
And the scenario stays on one sequencer. It shapes traffic; it does not coordinate the peer or the fault injector. The moment it needs to, it becomes a virtual sequence (§20).
20. Virtual Sequences
// ILLUSTRATIVE. Coordinates several agents through their SEQUENCERS.
// This is [20.2 §36]'s flagship scenario, expressed as stimulus.
class ucie_recovery_with_live_work extends uvm_sequence;
`uvm_object_utils(ucie_recovery_with_live_work)
`uvm_declare_p_sequencer(ucie_virtual_sequencer)
task body();
ucie_burst_then_quiet traffic;
ucie_peer_hold_response hold;
ucie_inject_one_fault fault;
// 1. Start traffic and let obligations accumulate.
traffic = ucie_burst_then_quiet::type_id::create("traffic");
if (!traffic.randomize() with { burst_len == 12; quiet_cycles == 0; })
`uvm_error(get_type_name(), "randomize failed")
fork traffic.start(p_sequencer.protocol_seqr); join_none
// 2. Make the peer withhold responses, so obligations STAY outstanding.
hold = ucie_peer_hold_response::type_id::create("hold");
if (!hold.randomize() with { cycles == 400; })
`uvm_error(get_type_name(), "randomize failed")
fork hold.start(p_sequencer.peer_seqr); join_none
// 3. WAIT FOR THE OBSERVED CONDITION, not for a delay. The scenario
// requires obligations live at the moment of injection — 20.5 §26's
// cp_outstanding.many bin — and only the model knows when that holds.
wait (p_sequencer.models.sem.outstanding_count() >= 3);
// 4. Inject, at the moment the context is right.
fault = ucie_inject_one_fault::type_id::create("fault");
if (!fault.randomize() with { family == F_INTEGRITY;
point == INJ_TRANSMIT_PATH; })
`uvm_error(get_type_name(), "randomize failed")
fault.start(p_sequencer.fault_seqr);
// 5. Let the recovery run and the obligations resolve. The DRAIN is the
// test's job (§28), not this sequence's.
endtask
endclassFour notes.
Step 3 is what makes this a real scenario rather than a hopeful one. Injecting after a fixed delay produces cp_outstanding in whatever bucket it happens to be; waiting on the model's count produces the many bin deliberately. 20.5 §55's coverage-guided loop, in code.
It reads the model, not the design. models.sem.outstanding_count() is the environment's independently maintained count (20.4 §10) — which is also the count the coverage bin will record, so the scenario and the measurement agree by construction.
It starts sequences on sequencers and never touches a driver (§22).
And it does not drain. The sequence sets up a situation and ends; the test decides when the simulation may finish (§26, §28) — and conflating the two is §27.
21. The Virtual Sequencer
// ILLUSTRATIVE. A handle holder. It coordinates nothing itself.
class ucie_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils(ucie_virtual_sequencer)
ucie_protocol_sequencer protocol_seqr;
ucie_peer_sequencer peer_seqr;
ucie_mgmt_sequencer mgmt_seqr;
ucie_fault_sequencer fault_seqr;
// Read-only model handles, so a virtual sequence can synchronise on an
// OBSERVED condition (§20 step 3) rather than on a delay.
ucie_model_bundle models;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclassTwo notes.
Sequencer handles are assigned in the environment's connect_phase, not looked up by hierarchy path. A virtual sequencer that finds its sub-sequencers by path cannot be instantiated twice — and a multi-link environment (18.4) needs exactly that.
And the model handles are read-only by convention. A virtual sequence may wait on the model's state; it must never write it. The model's state comes from monitors (20.4 §17), and a sequence that writes it has manufactured an observation.
22. Wrong Virtual Sequence — Reaching Into the Design
// WRONG — the sequence forces DUT internals to create a corner case.
task body();
// "Make the replay ring nearly full so we can test the boundary."
force dut.adapter.replay_alloc_ptr = dut.adapter.replay_retire_ptr + DEPTH - 1;
#100ns;
release dut.adapter.replay_alloc_ptr;
// ... then send traffic and expect the near-full behaviour.
endtaskFour consequences.
The state may be unreachable in reality. A pointer pair forced to a value the allocation logic would never produce means the design is now in a state no interface sequence could create — so a failure from here is not a bug report (20.2 §43).
The reference models do not know. The scoreboard's replay occupancy came from observed commits and resolutions (20.4 §18); the forced pointers did not generate any observation, so the model and the design now disagree for a reason that is the environment's fault. Expect a cascade of false failures.
It breaks on every refactor. A hierarchical force is the most fragile possible coupling (19.6 §18) — and it is in a sequence, which is supposed to be the most reusable layer.
And the corner it was trying to reach is reachable legitimately. A near-full replay ring is produced by issuing objects while withholding resolutions — which is ucie_peer_hold_response plus a burst, i.e. two primitives the environment already has. The force was a shortcut around writing a scenario, and the scenario is what the plan needed anyway.
Faults and corner states enter through verification hooks at contract boundaries (20.2 §43), never through hierarchical force on design state.
23. item_done() Is Not Semantic Completion
The flagship UVM mistake for a protocol with retries and recovery.
Four different "done" events, and they can be thousands of cycles apart:
| Event | Means | Who signals it |
|---|---|---|
finish_item() returns | the driver accepted the item | the driver's item_done() |
| the interface transfer completes | the boundary handshake happened | the interface |
| the object resolves | reliability is satisfied (20.4 §18) | the transport model |
| the obligation is terminal | the semantic operation completed or failed | the semantic model |
The distances between them, worked:
| From | To | Typical distance |
|---|---|---|
finish_item | interface transfer | 0 to a few cycles |
| interface transfer | resolution | a round trip, plus retries |
| resolution | terminal | a completion path |
finish_item | terminal | round trip + retries + possibly a full recovery |
And the reason the mistake survives: with a zero-latency responder in a unit testbench, all four coincide. The sequence works. It then fails against a real peer, a real retry, or a real recovery — and the failure looks like a protocol bug because the sequence's expectations are wrong rather than the design's behaviour.
24. Wrong Sequence — Assuming Completion
// WRONG — assumes the response has arrived when finish_item returns.
task body();
ucie_semantic_item wr = ucie_semantic_item::type_id::create("wr");
start_item(wr);
if (!wr.randomize() with { op == OP_WRITE; addr == 'h1000; })
`uvm_error(get_type_name(), "randomize failed")
finish_item(wr);
// "The write is done." IT IS NOT.
ucie_semantic_item rd = ucie_semantic_item::type_id::create("rd");
start_item(rd);
if (!rd.randomize() with { op == OP_READ; addr == 'h1000; })
`uvm_error(get_type_name(), "randomize failed")
finish_item(rd);
// Expects to read what the write wrote.
endtaskWhat happens. The driver accepted the write and returned. The write is in the transmit queue, or in flight, or being retried. The read is issued immediately and may arrive at the far end first, or the write may be retried after the read landed.
Four properties.
It is a stimulus bug that presents as a design bug. The read returns stale data; the scoreboard reports a data mismatch; and the design did nothing wrong — nothing ordered those two operations.
It passes with a zero-latency responder (§23), so it survives unit testing and fails in the full environment.
It gets "fixed" with a delay, which is §30's failure in miniature: #500ns between the write and the read works until a retry makes it 600.
And the correct fix depends on what the sequence actually meant. If it meant "read after the write completes", it must wait for the obligation to be terminal (§25). If it meant "the protocol should order these", then the ordering is a protocol property and belongs in an assertion (20.3 §22) — and the sequence should issue them concurrently and let the checker decide.
25. Waiting for Semantic Completion
// RIGHT — the sequence waits for the OBSERVED terminal state of its own
// obligation. The tag comes back from the driver (§13), and the wait is on
// the model, which is fed by monitors (20.4 §14).
task body();
ucie_semantic_item wr = ucie_semantic_item::type_id::create("wr");
start_item(wr);
if (!wr.randomize() with { op == OP_WRITE; addr == 'h1000; })
`uvm_error(get_type_name(), "randomize failed")
finish_item(wr); // the DRIVER accepted it
// The driver recorded the monitor tag on the item after acceptance.
if (!wr.accepted)
`uvm_error(get_type_name(), "item was not accepted")
// Wait for the OBLIGATION to be terminal — not for a delay, not for
// item_done, not for the interface transfer. §23's fourth row.
wait (p_sequencer.models.sem.is_terminal(wr.accepted_tag));
// And check WHICH terminal state, because a failure is also terminal.
if (p_sequencer.models.sem.state_of(wr.accepted_tag) != OB_COMPLETED) begin
`uvm_info(get_type_name(),
$sformatf("write %0d did not complete: %s", wr.accepted_tag,
p_sequencer.models.sem.state_of(wr.accepted_tag).name()),
UVM_LOW)
return; // do not proceed to the dependent read
end
// Only now is the ordering the sequence intended actually established.
// ... issue the read
endtaskThree notes.
The wait is on the model, not on a driver callback. The model's terminal state is derived from observed events (20.4 §14) — so the sequence and the scoreboard agree about what "completed" means, which they would not if the driver decided.
The terminal state is checked, not merely terminality. 20.3 §33's three-valued outcome: a failed operation is terminal too, and a sequence that proceeds to a dependent read after a failed write is testing something it did not intend.
And this pattern needs the tag to be observable. Where the monitor cannot attribute the accepted item to a tag (20.4 §23), the sequence must degrade to waiting on a count — "outstanding returns to zero" — and say so. A weaker synchronisation that is honest beats a precise one that is guessing.
26. Objection Discipline
The rule:
The test owns the lifecycle. A sequence raises an objection only if the simulation must not end while that sequence is mid-flight — and even then, ending the sequence is not the same as ending the test.
Three-part division:
| Component | Objection responsibility |
|---|---|
| the test | raises for the whole run; drops after the drain condition (§28) |
| a virtual sequence | may raise while it is setting up a situation, so it is not cut off |
| a primitive sequence | normally none — it is short and its parent is already objecting |
Two consequences.
Raising an objection in every sequence is the anti-pattern. It produces an objection count nobody can reason about, and the drop that actually ends the run is whichever sequence happened to finish last — which is not a decision anybody made.
And the objection must not be dropped when the stimulus finishes. §27.
27. Wrong Test Ending
// WRONG — the objection is dropped when the stimulus sequence ends.
task run_phase(uvm_phase phase);
phase.raise_objection(this);
vseq = ucie_recovery_with_live_work::type_id::create("vseq");
vseq.start(env.vseqr);
phase.drop_objection(this); // stimulus is done — but the DUT is not
endtaskWorked. The virtual sequence injected a fault and returned. At that moment: a recovery is in progress, thirty obligations are outstanding, twelve transport objects are unresolved, credit is being re-established, and eight monitor events are still to be published.
The objection drops. run_phase ends. The simulation exits. And unless the end-of-test checks run and are strict (20.4 §55), it exits PASS.
Four properties.
Everything the test set up is discarded, unverified. The scenario's entire purpose was what happens after the fault — and the run ends before any of it is observed.
It looks correct. Raise, start, drop is the canonical UVM shape, and it is right for a test whose stimulus and whose effects coincide. For a protocol with retries and recovery they do not.
20.4 §56's empty-model trap makes it worse. If the end-of-test check is "no obligations outstanding", and a wrong reset handler cleared them (§33), the check passes on a run that verified nothing.
And the failure is silent and permanent. No warning, no error — and every subsequent run of that test has the same hole, so the recovery scenarios in the regression are all measuring the fault injection and none of the recovery.
28. The Drain Architecture
// RIGHT — stimulus ends, then the environment DRAINS, then the objection drops.
task run_phase(uvm_phase phase);
phase.raise_objection(this, "test running");
// 1. Stimulus.
vseq = ucie_recovery_with_live_work::type_id::create("vseq");
vseq.start(env.vseqr);
// 2. Stop NEW stimulus, but allow everything already in flight to finish:
// responses, retries, credit returns, recovery, monitor publication.
env.stop_new_stimulus();
// 3. Wait for the drain CONDITION (§29), with a bound so a hang is a
// failure rather than a timeout with no diagnosis.
fork
begin : drain
env.wait_for_drain();
`uvm_info(get_type_name(), "drain complete", UVM_LOW)
end
begin : watchdog
#(DRAIN_TIMEOUT);
env.report_drain_failure(); // names WHICH condition did not hold
`uvm_error(get_type_name(), "drain did not complete")
end
join_any
disable fork;
// 4. End-of-test checks (20.4 §55) run in check_phase, on a drained model.
phase.drop_objection(this, "test complete");
endtaskThree notes.
Step 2 is a stimulus stop, not a reset. New requests stop; the environment keeps monitoring, the models keep updating, and the DUT keeps working. A "stop" that gates the monitors loses exactly the events the drain is waiting for.
Step 3's watchdog reports which condition failed, not merely that the drain timed out. A drain timeout with no diagnosis is 20.2 §49's single global timeout — and the whole point of a structured drain is that it can name the layer.
And the end-of-test checks run in check_phase, after the drain. Running them inside run_phase before the drain reports failures for work that was legitimately still in flight.
29. The Drain Condition
// ILLUSTRATIVE. The condition, stated fully. Each clause is a DIFFERENT model
// (20.4 §5), and the report names which one did not settle.
task ucie_env::wait_for_drain();
forever begin
@(drain_check_event); // periodic, plus on every model change
if (drain_conditions_met()) return;
end
endtask
function bit ucie_env::drain_conditions_met();
drain_status = '{default: 0};
// 1. Semantic: every obligation is terminal, OR classified on a fatal end.
drain_status.semantic = (models.sem.outstanding_count() == 0)
|| (models.link.phase == RM_FAILED
&& models.sem.unclassified_count() == 0);
// 2. Transport: no object left unresolved.
drain_status.transport = (models.trans.unresolved_count() == 0);
// 3. Link: not mid-recovery and not mid-configuration-commit.
drain_status.link = (models.link.phase != RM_RECOVERY)
&& !models.link.cfg_commit_in_progress;
// 4. Resource: conservation closes and nothing is stranded (20.4 §25).
drain_status.resource = models.res.conservation_closes()
&& (models.res.stranded_count() == 0);
// 5. Monitors: no observation still queued anywhere.
drain_status.monitors = (pending_observations() == 0);
// 6. Faults: injection has ceased and every injected fault was accounted for.
drain_status.faults = !fault_agent.active()
&& fault_agent.all_injections_accounted();
return drain_status.semantic && drain_status.transport && drain_status.link
&& drain_status.resource && drain_status.monitors && drain_status.faults;
endfunctionFour notes.
Clause 1's second half is what lets a fatal run drain. 20.2 §41: obligations outstanding at a fatal end are correct if each is classified. A drain condition demanding zero outstanding cannot express a correct fatal run and hangs until the watchdog fires.
Clause 4 is the one that catches the silent failure. Conservation closing and nothing stranded (20.4 §25) — a run can reach zero outstanding obligations while holding a leaked credit claim and a stranded replay entry, and only this clause notices.
Clause 5 exists because of TLM. Observations in an analysis FIFO have been published and not consumed. Dropping the objection with events queued means the models never see them, so the end-of-test checks run on an incomplete model.
And drain_status is a struct, so the failure report names the clause. "Drain failed: transport did not settle — 3 objects unresolved" is actionable; "drain timeout" is not.
30. Wrong Drain — a Fixed Delay
// WRONG — a delay in place of a condition.
vseq.start(env.vseqr);
#10us; // "long enough for everything to settle"
phase.drop_objection(this);Four properties.
It is right until it is not. Ten microseconds covers a clean run and does not cover a run with two recoveries, a retry storm and a degraded renegotiation — which is exactly the run the scenario was written to produce.
When it is too short, the failure is silent (§27). When it is too long, every test in the regression pays the delay, so somebody eventually shortens it — reintroducing the silent version.
It cannot express the fatal case. A run that ends in a fatal state never settles in the ordinary sense; a fixed delay either cuts it off before the classification is emitted or waits pointlessly.
And the number has no derivation, so it cannot be reviewed, cannot be scaled with the configuration, and will be adjusted by whoever is debugging next — 20.2 §16's fixed-watchdog failure, in the test's lifecycle rather than in a property.
A drain is a condition with a bounded wait, not a delay. The bound exists so a hang fails loudly; the condition exists so a correct run is not cut off.
31. The Reset and Recovery Coordinator
Reset in a UCIe environment is not "the driver drives reset and everyone restarts". It is a semantic event with several scopes, and the environment must distribute the meaning rather than the wire.
Seven things a reset must coordinate, and no component can do this alone:
| Concern | Must happen |
|---|---|
| stimulus | sequences pause before the reset, resume after it — not mid-item |
| driver state | the driver abandons or completes its in-flight item, deterministically |
| sequencer queue | outstanding items are either flushed or preserved, per reset kind |
| monitor epochs | monitors advance or retain their epoch view |
| model policy | per model, per reset kind — 20.4 §30's table |
| coverage epoch | collectors know a boundary occurred, so cross state is not stitched across it |
| scoreboard retention | the decision that matters — §33 |
The coordinator's job is to convert one observed event into seven correctly-scoped notifications. It does not gate anybody's logic; it tells each component what happened, in terms the component can act on.
32. Reset Is a Meaning, Not a Wire
// ILLUSTRATIVE. The coordinator observes the management boundary and publishes
// a SEMANTIC reset kind. Note what it does NOT do: it does not touch anyone's
// state, and it does not forward a wire.
class ucie_reset_coordinator extends uvm_component;
`uvm_component_utils(ucie_reset_coordinator)
uvm_analysis_port #(ucie_reset_event) rst_ap;
ucie_env_cfg cfg;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
rst_ap = new("rst_ap", this);
endfunction
// Fed by the management monitor's observations — never by a raw signal.
function void write_link_event(ucie_link_event e);
ucie_reset_event r;
case (e.kind)
EVT_HARD_RESET: r = make_reset(RESET_HARD, e);
EVT_RECOVERY_ENTER: r = make_reset(RESET_RECOVERY, e);
EVT_CFG_RESET: r = make_reset(RESET_CONFIG, e);
EVT_DIAG_CLEAR: r = make_reset(RESET_DIAG, e);
default: return; // not a reset-class event
endcase
rst_ap.write(r); // one event, many correctly-scoped consumers
endfunction
endclassThree properties.
It consumes observations, not signals. The management monitor decided that a recovery was entered; the coordinator classifies rather than samples. A coordinator watching rst_n directly cannot distinguish a recovery from a cold reset — which is precisely the distinction the whole section exists for.
It publishes and does nothing else. Each consumer applies its own policy (§35). A coordinator that clears the models itself has centralised the policy as well as the notification, and the seven policies are genuinely different.
And RESET_DIAG is in the enumeration. 19.6 §27's subtlest matrix row is a diagnostic counter-clear that must not touch the epochs — and if the environment has no notion of a diagnostic clear, it cannot check that row.
33. Wrong UVM Reset Handling
// WRONG — every component implements its own reset behaviour, on the wire.
// This appears in the driver, the monitor, the scoreboard and the coverage
// collector, written independently, all reasonable in isolation.
task ucie_protocol_monitor::run_phase(uvm_phase phase);
forever begin
@(negedge vif.rst_n);
pending_obs.delete(); // "clean slate"
...
end
endtask
function void ucie_scoreboard::reset_handler();
if (!vif.rst_n) begin
sem_model.clear(); // "clean slate"
trans_model.clear();
res_model.clear();
end
endfunctionWorked, on the design bug this is supposed to catch. The DUT's recovery path drives the same reset net as the global reset — 20.4 §31's exact defect — so a recoverable link event clears the design's semantic table.
| Cycle | Design | Monitor | Scoreboard | Verdict |
|---|---|---|---|---|
| 900 | 4 outstanding | 2 obs queued | 4 outstanding | — |
| 901 | recovery; reset net asserts | — | — | — |
| 902 | table cleared — the bug | queue deleted | models cleared | — |
| 950 | recovery completes | — | — | — |
| 951 | 0 outstanding | — | 0 outstanding | agree — PASS |
| ∞ | 4 operations never complete | — | nothing expects them | never reported |
Five properties, and the fifth is why this is the chapter's centerpiece.
Four independent decisions, each locally reasonable. Nobody wrote a bad line. The monitor clearing its queue on reset is correct for a cold reset; so is the scoreboard clearing its models. The defect is that neither distinguished the reset kind.
The failure is an absence. Four obligations ceased to exist everywhere at once, and absences are only detectable by something that remembers.
The drain condition does not save it. §29 clause 1 checks that outstanding reaches zero — and it did, by deletion. So the drain completes and the end-of-test checks pass.
It composes with 20.3 §40's scoping anti-pattern. If the preservation assertion is also disabled during recovery, then the design's bug, the environment's clearing, and the checker's disabling all align — and the regression is green on a link that silently loses work.
And the fifth: the fix is architectural, not local. Patching each component's if (!rst_n) to check a kind means four components each deciding what a recovery means, and they will drift. The environment needs one policy table (§35), distributed by one coordinator (§32), applied per component by a policy the component receives rather than invents.
34. The Reset Event
// ILLUSTRATIVE. The semantic reset kinds, matching the design's reset matrix
// (19.6 §27) rather than the design's reset nets. NOT a UCIe encoding (§3).
typedef enum {
RESET_HARD, // power-on / cold: everything
RESET_RECOVERY, // a recoverable link event: LINK-EPOCH state only
RESET_CONFIG, // configuration registers return to defaults
RESET_DIAG // counters and fault records clear; EPOCHS DO NOT
} reset_kind_e;
class ucie_reset_event extends uvm_object;
`uvm_object_utils(ucie_reset_event)
reset_kind_e kind;
longint cycle;
int unsigned link_epoch_before, link_epoch_after;
int unsigned cfg_epoch_before, cfg_epoch_after;
// The CONTEXT at the reset — what was live when it happened. This is what
// makes 20.5 §26's coverage bins and 20.4 §33's matrix checks possible.
int unsigned outstanding_at_reset;
int unsigned unresolved_objects_at_reset;
bit first_fault_valid_at_reset;
endclassTwo notes.
The four kinds mirror the design's reset matrix, not its reset nets. That is the whole design of the abstraction: the environment checks the matrix (20.4 §33), so the environment's vocabulary must be the matrix's.
And the context fields make the event measurable. 20.5 §26's cp_outstanding bins and 20.5 §53's reset-with-work coverage both sample from here — and a reset event carrying no context can only produce the "a reset happened" bin, which is the one that proves nothing.
35. The Component Reset-Policy Table
The chapter's centerpiece. Every component, every reset kind, one answer — and the answers differ.
| Component | RESET_HARD | RESET_RECOVERY | RESET_CONFIG | RESET_DIAG |
|---|---|---|---|---|
| Protocol driver | abandon in-flight; idle the interface | finish or abandon deterministically; then hold | hold | no action |
| Peer driver | abandon; idle | hold; resume when operational | hold | no action |
| Fault agent | stop injecting; clear the plan | stop injecting; keep the injection log | keep the log | clear counters only |
| Protocol sequencer | flush the item queue | preserve the queue — the items are still wanted | preserve | no action |
| Protocol monitor | clear partial observations | publish or discard partials deterministically; advance epoch view | advance cfg_epoch view | no action |
| Adapter monitor | clear partials | advance epoch view | advance | no action |
| Link/mgmt monitor | clear | this is the event source | — | — |
| Semantic model | report abandoned, then clear | UNTOUCHED — annotate only | untouched | untouched |
| Transport model | report, then clear | mark recovery-spanned; retain history state | untouched | untouched |
| Resource model | clear | void the agreement; clear pending returns | clear credit state | clear counters; NOT the epochs |
| Link/config model | clear | phase → recovery; cfg_agreed = 0 | reset requested + active | untouched |
| Coverage collectors | reset per-run context | sample the reset event, then advance context | sample | no action |
| Scoreboard join | clear tag map | untouched | untouched | untouched |
Five readings.
Column 3 is mostly "untouched" or "annotate". That is the headline, and it is 20.4 §30's table extended to the components that surround the models. Thirteen rows and only four do anything substantial on a recovery.
The semantic-model row is the one line that makes §33's bug detectable. Annotate only — and §33's four components each independently chose clear.
The RESET_DIAG column has one non-trivial cell, and it is the resource model's: clear counters, not epochs. 19.6 §27's subtle row — a diagnostic read-and-clear that also zeroes the credit epoch makes every stale in-flight return acceptable again. The environment must not make the mistake it is checking for.
The sequencer row is counter-intuitive and correct. On a recovery the queued items are still wanted — the client did not withdraw its requests because the link hiccuped. Flushing them turns a recovery into a lost-stimulus event and reduces the scenario's coverage silently.
And the coverage row says sample, then advance. 20.5 §53's merge argument: the reset event's context must be sampled before the context is reset, or the bin recording "a reset with work outstanding" is always empty.
36. Analysis Architecture
Monitors publish immutable observations. Everything else subscribes.
// ILLUSTRATIVE. A monitor with ONE responsibility. Note what is absent.
class ucie_protocol_monitor extends uvm_monitor;
`uvm_component_utils(ucie_protocol_monitor)
virtual ucie_protocol_if vif;
uvm_analysis_port #(ucie_semantic_obs) sem_ap;
uvm_analysis_port #(ucie_link_event) evt_ap;
int unsigned next_tag;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
sem_ap = new("sem_ap", this);
evt_ap = new("evt_ap", this);
endfunction
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb); // clocking block — §63
if (vif.mon_cb.rst_n && vif.mon_cb.valid && vif.mon_cb.ready) begin
ucie_semantic_obs o = ucie_semantic_obs::type_id::create("o");
o.cycle = cycle_count;
o.mon_tag = next_tag++; // the monitor owns the tag
o.sem_id = vif.mon_cb.sem_id; // as OBSERVED
o.cls = vif.mon_cb.cls;
o.addr = vif.mon_cb.addr;
sem_ap.write(o); // publish; do nothing else
end
end
endtask
endclassWhat is deliberately absent, and each absence is a rule.
No check(), no comparison, no expectation. The monitor has no opinion about correctness (§38).
No cov.sample(). Coverage is a subscriber (§51).
No $display beyond uvm_info at high verbosity. A logger is a subscriber too.
No scoreboard handle. §38.
And no state beyond the tag counter and whatever partial-observation assembly the boundary requires — because state in a monitor is state that needs a reset policy (§35), and the less of it there is the fewer decisions there are.
37. Analysis Ports
One port per observation type, not one per consumer.
| Port | Publishes | Subscribers |
|---|---|---|
sem_ap | semantic-boundary observations | semantic model, traffic coverage, logger |
obj_ap | transport-object observations | transport model, reliability coverage |
att_ap | physical attempts | transport model, reliability coverage |
evt_ap | link, config, resource and fault events | link model, resource model, all coverage, reset coordinator |
Two rules.
A new subscriber requires no monitor change. That is the entire point of the pattern, and it is what makes a coverage collector addable without touching the observation path.
And the reset coordinator is a subscriber on evt_ap (§32). It is not privileged and it does not tap a signal — it consumes the same observation stream everything else does, which is what keeps its classification consistent with the models' view.
38. Wrong Monitor Coupling
// WRONG — the monitor calls its consumers directly.
task ucie_monitor_bad::run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (accept_detected()) begin
ucie_obs o = build_obs();
sb.check(o); // the monitor now checks
cov.sample(o); // the monitor now samples coverage
logger.print(o); // the monitor now logs
if (o.cls > NUM_CLASSES) // the monitor now has an opinion
`uvm_error(get_type_name(), "bad class")
end
end
endtaskFive consequences.
The monitor cannot be reused. A passive-only build, a coverage-only build, or a second instance for a second link all need the scoreboard, the coverage collector and the logger to exist — so the monitor drags the whole environment with it.
The subscribers cannot be tested independently. 20.4 §42's environment self-checks require feeding a model a synthetic event stream. With direct calls, the only way to exercise the model is through a monitor and a design.
Ordering becomes implicit. The scoreboard sees the observation before the coverage collector, always, because that is the order of the lines — and if the collector needs model state that the scoreboard's update produces, the dependency is invisible and fragile.
Disabling one disables none. 20.5 §8's per-collector switch is unimplementable when the call is hardcoded.
And the last line is the worst. A monitor that reports an error has become a checker — an unreviewed one, in the component whose entire value is having no opinions. That check belongs in an assertion (20.3 §17) or in a model, where it can be found.
39. Analysis FIFOs, and Where They Belong
uvm_tlm_analysis_fifo decouples publication from consumption. Use it where the consumer needs to pull, not everywhere.
| Situation | Use a FIFO? |
|---|---|
| a model that updates synchronously on each observation | no — a direct write() is simpler and has no queue to drain |
| a coverage collector that samples per event | no |
| a scoreboard that must compare two streams and one may lag | yes — one FIFO per stream |
a component that processes in its own run_phase at its own rate | yes |
| a logger that formats expensively | yes, so formatting does not stall the monitor |
Two rules.
Every FIFO is a drain obligation. §29 clause 5: observations sitting in a FIFO have been published and not consumed, so the drain condition must include them or the end-of-test checks run on an incomplete model.
And a FIFO where a direct write would do adds latency to the model's view. A virtual sequence waiting on models.sem.outstanding_count() (§20) sees a count that lags reality by the FIFO's depth — which turns a precise synchronisation into an approximate one for no benefit.
40. Multiple Monitor Streams
Four boundaries publish four streams, and the scoreboard consumes all of them. 20.1 §7's four-monitor argument is why; this is how they arrive.
// ILLUSTRATIVE. The environment's connect_phase, wiring four streams to their
// consumers. Note that each stream keeps its own type (§11).
function void ucie_env::connect_phase(uvm_phase phase);
// Semantic boundary → semantic model, traffic coverage
proto_agent.mon.sem_ap.connect(sb.sem_fifo.analysis_export);
proto_agent.mon.sem_ap.connect(traffic_cov.analysis_export);
// Adapter boundary → transport model, reliability coverage
adapter_mon.obj_ap.connect(sb.obj_fifo.analysis_export);
adapter_mon.obj_ap.connect(reliability_cov.analysis_export);
// Link boundary → transport model (attempts), reliability coverage
link_mon.att_ap.connect(sb.att_fifo.analysis_export);
link_mon.att_ap.connect(reliability_cov.analysis_export);
// Management boundary → link model, resource model, ALL coverage, and the
// reset coordinator (§37)
mgmt_mon.evt_ap.connect(sb.evt_fifo.analysis_export);
mgmt_mon.evt_ap.connect(link_cov.analysis_export);
mgmt_mon.evt_ap.connect(config_cov.analysis_export);
mgmt_mon.evt_ap.connect(resource_cov.analysis_export);
mgmt_mon.evt_ap.connect(recovery_cov.analysis_export);
mgmt_mon.evt_ap.connect(rst_coord.analysis_export);
// The reset coordinator's OUTPUT reaches everything with a policy (§35)
rst_coord.rst_ap.connect(sb.rst_export);
rst_coord.rst_ap.connect(traffic_cov.rst_export);
rst_coord.rst_ap.connect(proto_agent.drv.rst_export);
// ... one per row of §35's table
endfunctionThree notes.
Four streams, four types, no union. 20.4 §7's single event type is for the event stream specifically; observations of data at different layers stay distinct because their fields and lifetimes differ (§16).
The scoreboard uses FIFOs and the coverage collectors do not (§39). The scoreboard compares streams that can lag relative to one another; a collector samples and returns.
And the reset coordinator appears twice — once as a subscriber to evt_ap, once as a publisher to everything with a policy. That two-sided position is what makes it a coordinator rather than a component with special access.
41. Correlation Belongs to the Scoreboard
An agent knows what it drove. A monitor knows what it saw. Neither may decide what belongs to what.
20.4 §22's join owns correlation, and the environment's job is only to deliver the observations it needs.
Two failures the environment can introduce, and both look helpful:
A driver that stamps its own correlation. The protocol driver knows it just sent operation X, so it labels the item and the scoreboard trusts the label. But the driver's notion of which object carries which operation is the stimulus view — and if the design fragmented the operation differently than the driver assumed, the label is wrong and the scoreboard inherits it.
A monitor that infers across boundaries. The Adapter monitor sees an object and guesses which semantic operation it belongs to by looking at the protocol monitor's recent history. That inference is legitimate and it belongs in the join, with its own anomaly checks (20.4 §22) — because when it is wrong, the report must say "an environment inference failed" rather than "the design produced an unattributable object".
The environment transports observations; the scoreboard owns relationships. A correlation manufactured upstream of the join cannot be distinguished from an observation, so its errors are reported as the design's.
42. Configuration Architecture — Two Kinds
Two config objects, two lifetimes, two owners. Mixing them produces §45.
| Structural config | Scenario config | |
|---|---|---|
| Contains | virtual interfaces, active/passive, which agents exist, supported feature set, capacities | traffic mix, fault policy, run length, injection rates, target scenario |
| Set by | the test's environment-construction code | the test, or the virtual sequence |
| Read at | build_phase / connect_phase | sequence start, and during the run |
| Changes during the run | never | may |
| Analogue in the design | 19.6 §4's structural parameters | 19.6 §4's product policy |
Two rules.
Structural config is immutable after build_phase. A component that re-reads it during the run has made the environment's topology dynamic, which is unreviewable — the same argument 19.6 §5 makes about runtime-selectable lane counts.
And scenario config belongs to sequences, preferably as explicit parameters rather than as global database entries (§45). A sequence whose behaviour depends on a config_db entry set by a test three levels away is a sequence nobody can read.
43. The Structural Config
// ILLUSTRATIVE. Structural: what exists, and what it is connected to.
class ucie_env_cfg extends uvm_object;
`uvm_object_utils(ucie_env_cfg)
// Which agents exist — the topology decision of §6/§7.
bit have_protocol_agent = 1;
bit have_peer_agent = 1;
bit have_mgmt_agent = 1;
bit have_fault_agent = 1;
uvm_active_passive_enum protocol_is_active = UVM_ACTIVE;
uvm_active_passive_enum peer_is_active = UVM_ACTIVE;
// Per-agent configs, each holding its own vif (§46).
ucie_protocol_agent_cfg protocol_cfg;
ucie_peer_agent_cfg peer_cfg;
ucie_mgmt_agent_cfg mgmt_cfg;
ucie_fault_agent_cfg fault_cfg;
// The DUT's CAPABILITIES, read from the design's capability structure
// (19.6 §20) — not a testbench constant, so a parameter sweep works.
ucie_capabilities_t capability;
// Which checks and collectors apply in this configuration (20.1 §51).
bit reliability_in_path;
bit adapter_in_path;
endclassThree notes.
have_*_agent makes the topology a configuration rather than a code fork. §7's four DUT scopes become four config objects, and the environment's build_phase is the same code in all four.
capability comes from the design's capability structure, which is 19.6 §20's read-only registers — the elaborated parameters made observable. A testbench constant here is 19.6 §53's hardcoded-default failure, and it reappears the first time somebody sweeps a parameter.
And reliability_in_path / adapter_in_path gate whole check and collector sets (20.1 §51, 20.5 §40). In Raw Mode the Adapter is bypassed, so its monitor has little to observe and its checks would fire falsely — and the gate belongs in structural config because the mode is fixed for the run.
44. The Scenario Config
// ILLUSTRATIVE. Scenario: what this run should DO. Owned by the test and
// passed to sequences EXPLICITLY where possible (§45).
class ucie_scenario_cfg extends uvm_object;
`uvm_object_utils(ucie_scenario_cfg)
// Traffic shaping
rand int unsigned num_operations;
rand int unsigned class_weights[]; // per-class traffic mix
rand int unsigned burst_len_min, burst_len_max;
rand int unsigned quiet_period_min, quiet_period_max; // §19's silence
// Peer behaviour — §58's adversarial ranges
rand int unsigned peer_latency_min, peer_latency_max;
rand int unsigned peer_backpressure_pct;
rand bit peer_may_reorder;
// Fault policy — §60
rand int unsigned num_faults;
rand fault_family_e permitted_families[];
rand int unsigned permitted_moments[]; // 20.5 §25's moment enumeration
constraint c_sane {
burst_len_min <= burst_len_max;
peer_latency_min <= peer_latency_max;
peer_backpressure_pct inside {[0:100]};
}
endclassTwo notes.
Every field shapes stimulus, none shapes checking. A scoreboard_mode or disable_check_X field here would let a test weaken its own verification — and a test that can turn off a checker will, the first time that checker is inconvenient. Checker enablement belongs in structural config, set once, reviewed.
And permitted_moments is what makes 20.5 §25's fault-timing coverage reachable. Three of the seven moments require the injector to synchronise on an observed condition rather than fire on a timer; the config names which moments this run may use, and the fault sequence does the synchronisation (§61).
45. Wrong Config Architecture
// WRONG — every knob in the global database, no ownership.
task ucie_test_bad::build_phase(uvm_phase phase);
uvm_config_db #(int)::set(null, "*", "traffic_ratio", 70);
uvm_config_db #(int)::set(null, "*", "retry_count", 3);
uvm_config_db #(int)::set(null, "*", "target_width", 16);
uvm_config_db #(int)::set(null, "*", "timeout", 100000);
uvm_config_db #(int)::set(null, "*", "driver_delay", 5);
uvm_config_db #(int)::set(null, "*", "scoreboard_mode", 2);
uvm_config_db #(int)::set(null, "*", "peer_latency", 10);
uvm_config_db #(int)::set(null, "*", "fault_rate", 5);
endtaskFive consequences.
Nobody owns anything. A wildcard set reaches every component that happens to get that string, and which components those are is not visible from either end.
A typo is silent. "trafic_ratio" sets a key nothing reads; the get fails and falls back to a default. The test runs, differently than intended, with no diagnostic.
The test's intent becomes invisible. Reading the test tells you eight integers were set globally; it does not tell you what scenario the test is exercising, which is the one thing a test should communicate.
scoreboard_mode is a checker knob in a scenario setter (§44) — so this test can, and eventually will, weaken its own checking.
And it defeats reuse. A second link instance in the same simulation receives the same wildcard values (§68). There is no way to configure two links differently, because the configuration was never scoped to a component.
The corrected form: two config objects (§43, §44), set once at a specific path, retrieved once at build_phase, with scenario parameters passed to sequences explicitly — seq.randomize() with { burst_len == cfg.burst_len_max; } reads as intent, and a wildcard integer does not.
46. Virtual Interface Injection
// RIGHT — the vif travels INSIDE the config object, set once at a specific path.
// Top-level:
initial begin
ucie_env_cfg cfg = ucie_env_cfg::type_id::create("cfg");
cfg.protocol_cfg = ucie_protocol_agent_cfg::type_id::create("protocol_cfg");
cfg.protocol_cfg.vif = dut_if.protocol; // the actual interface
cfg.protocol_cfg.is_active = UVM_ACTIVE;
cfg.peer_cfg = ucie_peer_agent_cfg::type_id::create("peer_cfg");
cfg.peer_cfg.vif = dut_if.peer;
uvm_config_db #(ucie_env_cfg)::set(null, "uvm_test_top.env", "cfg", cfg);
run_test();
end
// Agent build_phase: ONE get, then use the handle.
function void ucie_protocol_agent::build_phase(uvm_phase phase);
if (!uvm_config_db #(ucie_protocol_agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal(get_type_name(), "protocol agent cfg not set")
// ... and cfg.vif is used in connect_phase (§8)
endfunctionThree rules.
One get per component, at build_phase, with a uvm_fatal on failure. A silent fallback to a default is §45's typo failure.
The path is specific, not "*". "uvm_test_top.env" names one component; a wildcard names everything and scopes nothing.
And uvm_config_db::get() never appears inside a run_phase or a task. 19.6 §18's argument about implementation coupling applies: a lookup in the middle of a driver's loop makes the driver's dependencies invisible and its cost per transaction real.
47. Agent Configuration
// ILLUSTRATIVE. Per-agent config. No fake UCIe signal set is defined (§3) —
// `ucie_protocol_if` is a symbolic verification-only abstraction.
class ucie_protocol_agent_cfg extends uvm_object;
`uvm_object_utils(ucie_protocol_agent_cfg)
virtual ucie_protocol_if vif;
uvm_active_passive_enum is_active = UVM_ACTIVE;
// Structural limits this agent must respect, from the capability structure.
int unsigned max_outstanding;
int unsigned num_classes;
// Driver contract parameters (§64) — how it behaves, not what it sends.
bit hold_payload_under_stall = 1; // must be 1
int unsigned max_stall_cycles;
endclassOne note that matters. hold_payload_under_stall exists and must be 1 in every legitimate configuration — it is there so a deliberate negative test can set it to 0 and confirm 20.3 §11's stability assertion fires. That is 20.3 §65's negative-testing discipline: a property never observed to fail may be unbound, vacuous or wrong, and the environment needs a way to violate it on purpose.
48. Factory Use
The factory earns its place in exactly three situations for this environment.
| Situation | Why the factory |
|---|---|
| swapping the peer model (§57) | four levels of peer, same interface — the test chooses |
| swapping the fault policy | a directed injector versus a randomised one |
| specialising a sequence item | a protocol-specific item deriving from the semantic base |
And where it does not: everything else. A factory override for a driver, a monitor, a model or a collector is an override nobody expects, and §49 is what accumulates.
// ILLUSTRATIVE. A justified override: this test wants an adversarial peer.
class ucie_test_adversarial_peer extends ucie_base_test;
`uvm_component_utils(ucie_test_adversarial_peer)
function void build_phase(uvm_phase phase);
// ONE override, named in the test, visible to anybody reading it.
ucie_peer_model::type_id::set_type_override(ucie_adversarial_peer::get_type());
super.build_phase(phase);
endfunction
endclassThe property that makes this acceptable: the override is in the test, at the top, singular, and named after what it does. A reader of the test knows which peer runs.
49. Wrong Factory Overuse
// WRONG — overrides layered across the test hierarchy.
// In the base test:
ucie_protocol_driver::type_id::set_type_override(ucie_driver_v2::get_type());
// In a mid-level test:
ucie_driver_v2::type_id::set_type_override(ucie_driver_v2_fast::get_type());
// In the leaf test:
ucie_protocol_driver::type_id::set_inst_override(
ucie_driver_debug::get_type(), "uvm_test_top.env.proto_agent.drv");
// And in a package, executed at elaboration:
ucie_semantic_item::type_id::set_type_override(ucie_item_ext::get_type());Four consequences.
Nobody can say which driver runs. The answer depends on the override order, the instance path, and whether a package-level override executed first. Determining it requires running the simulation and reading the factory print.
A debug driver reaches production runs. An instance override in a leaf test that somebody copied as a template is now in twelve tests, and the fast driver they thought they were using is not the one running.
The item type changes under the sequences. A package-level item override means every sequence is producing a derived type that its constraints were not written for — and the resulting randomisation failures look like constraint bugs.
And it makes the environment unreviewable. This chapter's whole premise (§1) is that the environment should make responsibilities clear. An override chain is a mechanism that hides which component holds a responsibility, which is the failure mode the chapter exists to prevent.
One override, in the test, named after its purpose. If a second is needed, ask whether the thing being swapped should have been a configuration field instead.
50. Scoreboard Integration
20.4 built the models. This chapter wires them and adds nothing.
// ILLUSTRATIVE. The scoreboard component is a HOST for 20.4's four models plus
// the join. It does not reimplement any of them.
class ucie_scoreboard extends uvm_component;
`uvm_component_utils(ucie_scoreboard)
// 20.4 §5's four models, plus the optional data model.
ucie_semantic_model sem;
ucie_transport_model trans;
ucie_resource_model res;
ucie_link_model link;
ucie_data_model data; // optional — 20.4 §28
ucie_join join_layer; // 20.4 §22
ucie_reporter reporter; // 20.4 §37's first-divergence engine
// Four input streams (§40), each with its own FIFO because they can lag
// relative to one another (§39).
uvm_tlm_analysis_fifo #(ucie_semantic_obs) sem_fifo;
uvm_tlm_analysis_fifo #(ucie_object_obs) obj_fifo;
uvm_tlm_analysis_fifo #(ucie_attempt_obs) att_fifo;
uvm_tlm_analysis_fifo #(ucie_link_event) evt_fifo;
task run_phase(uvm_phase phase);
fork
forever begin ucie_semantic_obs o; sem_fifo.get(o); sem.on_obs(o);
join_layer.on_sem(o); end
forever begin ucie_object_obs o; obj_fifo.get(o); trans.on_obs(o);
join_layer.on_obj(o); end
forever begin ucie_attempt_obs o; att_fifo.get(o); trans.on_att(o);
join_layer.on_att(o); end
forever begin ucie_link_event e; evt_fifo.get(e); link.on_evt(e);
res.on_evt(e); join_layer.on_evt(e); end
// Periodic join checks (20.4 §25's stranded check, §40's cross-model).
forever begin #(JOIN_CHECK_PERIOD); join_layer.check_cross_model();
join_layer.check_stranded(); end
join_none
endtask
function void check_phase(uvm_phase phase);
join_layer.check_join_health(); // 20.4 §22 — ENVIRONMENT category first
check_end_of_test(); // 20.4 §55
reporter.report_first_divergence(); // 20.4 §37
endfunction
endclassThree notes.
Five parallel consumers, one per stream plus the periodic checks. 20.4 §25's stranded check must run periodically rather than only at end of test — at end of test the finding is "some resources were stranded"; at the cycle it happens the finding names the obligation.
check_phase runs the environment-health check first (20.4 §39's report ordering). A first divergence computed by an environment whose join is broken is a divergence in the environment.
And the drain (§29) must complete before check_phase, which UVM's phasing gives for free — run_phase ends, then check_phase runs — provided the objection was dropped for the right reason (§27).
51. Coverage Integration
20.5's six collectors become subscribers, and the wiring rule is one line.
// ILLUSTRATIVE. Collectors subscribe to MONITOR streams. They never subscribe
// to a driver, a sequencer or a sequence.
class ucie_recovery_cov_sub extends uvm_subscriber #(ucie_link_event);
`uvm_component_utils(ucie_recovery_cov_sub)
ucie_recovery_cov cov; // 20.5 §38's collector
ucie_model_bundle models; // read-only, for derived bins
function void build_phase(uvm_phase phase);
super.build_phase(phase);
cov = new();
if (!uvm_config_db #(ucie_model_bundle)::get(this, "", "models", models))
`uvm_fatal(get_type_name(), "model bundle not configured")
cov.bind_models(models);
endfunction
function void write(ucie_link_event t);
cov.on_event(t); // one call; no checking, no logging
endfunction
endclassAnd the connection, from §40: every collector's analysis_export connects to a monitor's analysis port. No collector is connected to anything on the stimulus side — which is §52.
52. Intended Versus Observed Coverage
The flagship coverage-integration lesson, and it is worth a section because sampling in a driver is the natural and wrong instinct.
Worked. A test's fault sequence injects a corruption expecting the design to retry. The design has a bug: it drops the object instead of retrying.
| Where coverage is sampled | cp_attempts_per_object | What the report says |
|---|---|---|
| in the fault sequence (intended) | two — the sequence intended a retry | "retry coverage: hit" |
| in the driver (intended) | two — the injection was driven | "retry coverage: hit" |
| in the monitor (observed) | one — only one attempt ever occurred | "retry coverage: NOT hit" |
Only the third is a statement about the design.
Four properties.
Stimulus-side coverage measures the testbench. It records what the environment asked for — which is a useful thing to know and is not coverage of the DUT. A regression whose retry bins are filled from the stimulus side has recorded that retries were requested and knows nothing about whether any occurred.
It hides exactly the bug it was pointed at. The scoreboard may still catch the dropped object — as an obligation that never terminates — but the coverage report says the retry path was exercised, so the retry checks are considered covered and the investigation of the coverage gap that would have pointed here never happens.
And it survives review, because sampling in the driver looks efficient: the driver already has the transaction, and the monitor's observation is "the same thing". It is not the same thing — the difference between them is the design.
The rule, in one line:
Coverage of the DUT is sampled from observations. Coverage of the stimulus, if wanted at all, is a separate and separately labelled model.
53. Wrong Coverage — Sampled in the Driver
// WRONG — the driver samples coverage on what it drove.
task ucie_protocol_driver::run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
drive_item(req);
traffic_cov.sample_class(req.cls); // what we ASKED for
traffic_cov.sample_burst(req.length);
seq_item_port.item_done();
end
endtaskFour consequences beyond §52's headline.
Rejected items are counted as accepted. If admission refuses the item — no credit, wrong class, a disabled protocol — the driver sampled it anyway. So cp_class records classes the design never accepted.
It cannot sample derived bins. 20.5 §16's cp_commit_spans_operation and 20.5 §26's recovery-context bins are computed from model state. A driver has no model handle and should not have one.
It couples the driver to the coverage model (§38's failure in a different component), so neither can be reused or disabled independently.
And it makes the reset policy wrong. §35's coverage row says sample the reset event, then advance context. A driver-side sampler has no reset event — it has a wire — so the reset-with-work bins are unreachable.
54. Reference-Model Independence
The environment's role: deliver observations and configuration to the models, and nothing else.
| A model may receive | A model must never receive |
|---|---|
| monitor observations | a design-internal signal |
| the capability structure (19.6 §20) | a design package function (19.6 §13) |
| semantic reset events (§34) | a raw reset wire |
| structural configuration | a driver's intent |
| other models' state via the join only | another model's state directly (20.4 §4) |
Two notes.
The last row is the one the environment can violate accidentally. Giving every model a handle to every other model is convenient at build_phase and it makes 20.4 §4's fifth cost inevitable — the transport model reads the semantic model's count instead of counting objects itself, and then they cannot disagree.
And "a driver's intent" includes the sequence item. A model that receives the item the sequence generated knows what was requested; it must know what was observed (§52).
55. The Predictor
Where the environment needs an expected value rather than an expected relationship, the predictor is a separate component from the scoreboard.
| Component | Answers |
|---|---|
| predictor | what should happen? — given an observed request and the architecture |
| scoreboard / models | did it happen? — matching, counting, correlating |
// ILLUSTRATIVE. A predictor consumes OBSERVED requests and produces expected
// responses from an INDEPENDENT model of the architecture (§56).
class ucie_predictor extends uvm_subscriber #(ucie_semantic_obs);
`uvm_component_utils(ucie_predictor)
uvm_analysis_port #(ucie_expected_rsp) exp_ap;
ucie_mem_model mem; // the predictor's own state
function void write(ucie_semantic_obs o);
ucie_expected_rsp e = ucie_expected_rsp::type_id::create("e");
e.mon_tag = o.mon_tag;
case (o.op)
OP_READ: begin e.kind = RSP_DATA; e.data = mem.read(o.addr, o.length); end
OP_WRITE: begin e.kind = RSP_ACK; mem.write(o.addr, o.data); end
default: e.kind = RSP_UNKNOWN; // §56 — an honest "I do not know"
endcase
exp_ap.write(e);
endfunction
endclassThree notes.
It consumes observed requests, not sequence items. The design accepted something; the predictor predicts a response to what was accepted, which may differ from what was offered.
RSP_UNKNOWN is a legitimate output. 20.4 §30's rule: a model must be able to say it does not know. A predictor that guesses generates false failures, which cost more credibility than missed bugs cost coverage.
And it holds its own memory state, which is 20.4 §28's data model — including the set-valued expectations of 20.4 §29 for the lost-response ambiguity.
56. Wrong Predictor — the Design's Own Function
// WRONG — the predictor calls the design's package function.
function ucie_expected_rsp predict(ucie_semantic_obs o);
// "The RTL already has this logic; reuse it."
return dut_pkg::compute_expected_response(o);
endfunctionWorked. compute_expected_response has a defect in how it handles an unaligned access spanning a boundary. The design produces the wrong response; the predictor predicts the wrong response; they match; the scoreboard reports a pass.
Four properties.
It is 19.6 §13's rule violated for the fourth time in this curriculum — after the legality function, the scoreboard trigger and the checker state. The pattern is always the same: the verification artefact borrows the design's answer to the question it is supposed to be asking.
It is the most tempting instance, because the function exists, is tested, and is exactly the computation needed. The temptation is the tell.
No failure is possible. Not "unlikely" — impossible. The comparison is f(x) == f(x).
And the correct predictor should be written differently on purpose. 20.2 §20's argument: a model written in the same shape as the design tends to make the same off-by-one, because the off-by-one lives in the shape. A predictor derived from the architecture document rather than from the RTL is structurally unlike it, and that is the point.
57. The Peer Model — Four Levels
A UCIe link environment needs something on the other side. Four levels, and each is right for a different phase of the project.
| Level | Behaviour | Good for | Blind to |
|---|---|---|---|
| 1 — simple responder | fixed latency, always ready, in-order | bring-up of the environment itself | everything in §59 |
| 2 — protocol-aware | correct protocol behaviour, fixed timing | early functional tests | timing, backpressure, reordering |
| 3 — adversarial | legal variation in latency, backpressure, ordering, configuration, faults | the bulk of verification — §58 | genuine implementation quirks |
| 4 — a second instance / VIP | a real implementation on the far side | interoperability-shaped testing (20.7) | — |
Two notes.
Level 3 is where the environment should live, and getting there is the single highest-value improvement to a UCIe testbench after the models themselves.
Level 4 is a different activity, and 20.7 owns it. Driving a real implementation against a real implementation is interoperability-shaped work; it is not a substitute for level 3, because a real peer exercises whatever that peer happens to do rather than the legal range.
58. The Peer Must Be Adversarial
A friendly peer produces a green regression and a link that fails against the first real one.
What a level-3 peer varies, and every one of these is legal behaviour:
| Dimension | Range |
|---|---|
| response latency | minimum to the architectural maximum, randomised per response |
| backpressure | from never to frequently, in bursts as well as uniformly |
| ordering | any order the protocol permits within an ordering group (20.3 §22) |
| credit return timing | immediate to batched to near the flush timeout (19.5 §43) |
| configuration choices | every capability set the architecture allows, including ones that force degradation |
| retry behaviour | acknowledging, negative-acknowledging, and silent, where legal |
| recovery requests | initiating a recovery from the far side, not only responding to one |
// ILLUSTRATIVE. The peer's behaviour is randomised per response, within legal
// ranges taken from the scenario config (§44).
class ucie_adversarial_peer extends ucie_peer_model;
`uvm_component_utils(ucie_adversarial_peer)
ucie_scenario_cfg scfg;
task respond(ucie_object_obs o);
int unsigned delay;
// Latency varies per response — NOT a fixed constant.
if (!std::randomize(delay) with { delay inside {[scfg.peer_latency_min:
scfg.peer_latency_max]}; })
`uvm_error(get_type_name(), "randomize failed")
repeat (delay) @(posedge vif.clk);
// Backpressure, in bursts as well as uniformly.
if ($urandom_range(99) < scfg.peer_backpressure_pct)
apply_backpressure($urandom_range(1, 32));
// Reordering, where the ordering group permits it.
if (scfg.peer_may_reorder && group_permits_reorder(o))
enqueue_out_of_order(o);
else
send_response(o);
endtask
endclassTwo notes.
Every range comes from the scenario config, so a test can narrow them deliberately — a bring-up test uses level-1 behaviour by setting all ranges to their minimum, without a different peer class.
And the last row of the table is the one people omit: the peer initiating a recovery. 8.5 §14's peer-restart case and 20.2 §26's training restart both require the far side to act on its own. A peer that only ever reacts leaves every far-side-initiated path untested.
59. Wrong Peer Model — the Accommodating Peer
// WRONG — the peer that makes every test pass.
class ucie_peer_friendly extends ucie_peer_model;
task respond(ucie_object_obs o);
// Always ready.
vif.ready <= 1'b1;
// Zero latency.
send_response(o); // same cycle
// Always in order, always the same configuration, never a fault,
// never initiates a recovery, always acknowledges.
endtask
endclassSix things this peer never tests, each corresponding to a real design mechanism:
| Never exercised | The mechanism left unverified |
|---|---|
| backpressure | the skid buffer (19.2 §39); stability under stall (20.3 §11) |
| variable latency | the outstanding table's depth; 20.5 §21's occupancy range |
| reordering | response matching (20.3 §21); ordering groups |
| batched credit return | the flush timer (19.5 §43); the partial-batch bin |
| a differing capability set | negotiation, degradation, and the refusal path (20.2 §24) |
| a far-side recovery | peer-restart handling; the training-restart arc |
And the fifth property, which is the reason this matters commercially. The entire premise of a die-to-die standard is that the two dies come from different teams, different companies, different generations (§3). A link verified only against a peer written by the same team, to the same assumptions, has been verified against its own assumptions — and the first real interoperability attempt is where those assumptions are discovered.
60. The Fault-Injection Component
Fault injection is a component with a contract, not a habit scattered through drivers.
// ILLUSTRATIVE. One agent, seven injection points (20.2 §43), and every
// injection carries its own expectation.
class ucie_fault_agent extends uvm_agent;
`uvm_component_utils(ucie_fault_agent)
ucie_fault_sequencer seqr;
ucie_fault_driver drv;
ucie_fault_log log; // 20.2 §43's injection record
// The driver applies faults through VERIFICATION HOOKS at contract
// boundaries — never by forcing design state (§22).
endclass
class ucie_fault_item extends uvm_sequence_item;
`uvm_object_utils(ucie_fault_item)
rand fault_family_e family; // 20.5 §24's taxonomy
rand int unsigned point; // which of the seven boundaries
rand int unsigned duration;
// WHEN — and three of these require synchronising on an observed condition
// rather than firing on a timer (20.5 §25).
rand fault_moment_e moment;
// The EXPECTATION, carried with the injection. 20.1 §44's rule: every
// negative test asserts a positive outcome.
rand int unsigned expected_detector;
rand int unsigned expected_recovery;
endclassThree notes.
The expectation travels with the injection. 20.1 §44: "we injected a fault and nothing crashed" is not a result. Naming the expected detector and the expected recovery in the item turns each injection into a checkable statement, and 20.2 §46's first-fault correlation compares against it.
moment is a field, not a delay. §61.
And it is a full agent with a sequencer, so a virtual sequence can coordinate injections with traffic and peer behaviour (§20) — which is what 20.2 §44's stage-3 fault combinations require.
61. Fault Sequences
// ILLUSTRATIVE. Synchronising on an OBSERVED condition, which is the only way
// to reach three of 20.5 §25's seven moments deliberately.
class ucie_inject_at_moment extends uvm_sequence #(ucie_fault_item);
`uvm_object_utils(ucie_inject_at_moment)
`uvm_declare_p_sequencer(ucie_fault_sequencer)
rand fault_family_e family;
rand fault_moment_e moment;
task body();
ucie_fault_item it;
// Wait for the MOMENT — from the models, not from a timer.
case (moment)
T_FIRST_OBJECT:
wait (p_sequencer.models.trans.total_objects() == 1);
T_AT_MAX_OCCUPANCY:
wait (p_sequencer.models.res.occupancy(STRUCT_RX)
>= p_sequencer.models.res.active_capacity[0] - 1);
T_BEFORE_RESOLUTION:
wait (p_sequencer.models.trans.unresolved_count() > 0);
T_DURING_CFG_COMMIT:
wait (p_sequencer.models.link.cfg_commit_in_progress);
T_DURING_RECOVERY:
wait (p_sequencer.models.link.phase == RM_RECOVERY);
T_STEADY_STATE:
wait (p_sequencer.models.link.phase == RM_OPERATIONAL);
default: ; // T_DURING_TRAINING needs no wait
endcase
it = ucie_fault_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { it.family == local::family;
it.moment == local::moment; })
`uvm_error(get_type_name(), "randomize failed")
finish_item(it);
p_sequencer.log.record(it, $time); // for §46's correlation
endtask
endclassTwo notes.
Every wait is on model state, which is derived from monitor observations. A wait on a design signal would make the fault sequence a white-box component and couple it to the implementation.
And T_AT_MAX_OCCUPANCY is the moment that cannot be reached any other way. 20.5 §25 lists it as one of three needing synchronisation; injecting on a timer produces whatever occupancy happens to be current, so the interaction between a fault and a full structure is never tested.
62. Reactive Sequences, Carefully
A sequence that reacts to observed behaviour is powerful and can deadlock the simulation.
The hazard: a zero-time circular dependency. A sequence waits on a monitor event; the monitor's event is produced by a transfer; the transfer requires the driver to have an item; the driver's item comes from the sequence that is waiting. Nothing advances and no time passes, so no timeout fires.
Three rules that prevent it.
Wait on model state or on a uvm_event, and always across a clock edge. A wait on a variable that only changes in the same time step as the thing being waited for is the circular case; advancing a clock edge breaks it.
Never make the driver wait for a monitor of its own boundary. The driver drives; the monitor observes. A driver that waits for its own transfer to be observed before proceeding has serialised itself against its own observer.
And put the reactive wait in a coordinator rather than in a primitive sequence. §20's virtual sequence waits and then starts primitives; the primitives never wait on anything. That keeps the reactive logic in one place where its dependencies are visible.
63. Race-Free Monitoring
A monitor sampling in the active region sees values mid-settle and produces intermittent, seed-dependent observations — which then present as design bugs.
// ILLUSTRATIVE. The interface provides a monitor clocking block; the monitor
// samples ONLY through it. Symbolic abstraction — no UCIe signal set (§3).
interface ucie_protocol_if (input logic clk, input logic rst_n);
logic valid, ready;
logic [CLS_W-1:0] cls;
logic [63:0] addr;
logic [DATA_W-1:0] data;
// Monitor samples: input skew, so values are stable when read.
clocking mon_cb @(posedge clk);
default input #1step;
input rst_n, valid, ready, cls, addr, data;
endclocking
// Driver drives: output skew, so drives land after the edge.
clocking drv_cb @(posedge clk);
default output #1;
output valid, cls, addr, data;
input ready, rst_n;
endclocking
modport MON (clocking mon_cb);
modport DRV (clocking drv_cb);
endinterfaceThree notes.
#1step samples in the preponed region, which is the same region SVA samples in (20.3 §7). That alignment is what makes an assertion failure and a monitor observation describe the same cycle — and without it, the two disagree by one cycle and every correlation is off.
Separate clocking blocks for monitor and driver, with different skews. One shared block makes the driver's output skew apply to the monitor's inputs, which reintroduces the race it was meant to remove.
And the modports make the discipline enforceable. A monitor given the MON modport cannot drive; a driver given DRV cannot sample outside its block. That is 19.6 §16's contract argument applied to the testbench interface.
64. The Driver Contract
A driver has obligations, and three of them are protocol obligations rather than UVM ones.
| Obligation | Why |
|---|---|
| hold the payload stable while stalled | 20.3 §11's contract — a driver that violates it fires the DUT's own boundary assertion |
| hold the whole bundle stable, not just the data | 20.3 §12's narrow-stability failure, from the driving side |
| respect the reset policy | §35's driver row — deterministic behaviour on each reset kind |
call item_done() when the interface transfer completes | not when the semantic operation completes (§23) |
| record the monitor tag on the item, if observable | so a sequence can wait for semantic completion (§25) |
| never generate protocol content the sequence did not ask for | the driver drives; the sequence decides |
// ILLUSTRATIVE. The stability obligation, driven correctly.
task ucie_protocol_driver::drive_item(ucie_semantic_item it);
// Present the WHOLE bundle at once, then hold it until accepted.
vif.drv_cb.valid <= 1'b1;
vif.drv_cb.cls <= it.cls;
vif.drv_cb.addr <= it.addr;
vif.drv_cb.data <= it.data[0];
// Hold everything stable until ready — NOT just the data (20.3 §12).
do @(vif.drv_cb); while (!vif.drv_cb.ready);
vif.drv_cb.valid <= 1'b0;
it.accepted = 1'b1;
endtaskOne note that is easy to miss. The do ... while loop drives nothing inside it — which is what makes the bundle stable. A loop that recomputes any field per iteration, for any reason, violates the contract and fires the DUT's own assertion, and the resulting failure is reported against the design.
65. Environment Assembly
// ILLUSTRATIVE SKETCH ONLY — no file is created in this repository (§3).
// The env builds what the config says exists (§43), and nothing more.
class ucie_env extends uvm_env;
`uvm_component_utils(ucie_env)
ucie_env_cfg cfg;
// Agents — conditional on the topology (§6, §7)
ucie_protocol_agent proto_agent;
ucie_peer_agent peer_agent;
ucie_mgmt_agent mgmt_agent;
ucie_fault_agent fault_agent;
// Monitors on internal boundaries — passive only (§9)
ucie_adapter_monitor adapter_mon;
ucie_link_monitor link_mon;
// Coordination and checking
ucie_reset_coordinator rst_coord;
ucie_scoreboard sb;
ucie_predictor pred;
// Coverage — one subscriber per collector (§51)
ucie_link_cov_sub link_cov;
ucie_config_cov_sub config_cov;
ucie_resource_cov_sub resource_cov;
ucie_reliability_cov_sub reliability_cov;
ucie_recovery_cov_sub recovery_cov;
ucie_traffic_cov_sub traffic_cov;
// Virtual sequencer
ucie_virtual_sequencer vseqr;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(ucie_env_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal(get_type_name(), "env cfg not set")
// Push per-agent configs down BEFORE building the agents (§46).
if (cfg.have_protocol_agent) begin
uvm_config_db #(ucie_protocol_agent_cfg)::set(this, "proto_agent", "cfg",
cfg.protocol_cfg);
proto_agent = ucie_protocol_agent::type_id::create("proto_agent", this);
end
if (cfg.have_peer_agent) begin
uvm_config_db #(ucie_peer_agent_cfg)::set(this, "peer_agent", "cfg",
cfg.peer_cfg);
peer_agent = ucie_peer_agent::type_id::create("peer_agent", this);
end
// ... mgmt and fault agents likewise
adapter_mon = ucie_adapter_monitor::type_id::create("adapter_mon", this);
link_mon = ucie_link_monitor::type_id::create("link_mon", this);
rst_coord = ucie_reset_coordinator::type_id::create("rst_coord", this);
sb = ucie_scoreboard::type_id::create("sb", this);
pred = ucie_predictor::type_id::create("pred", this);
vseqr = ucie_virtual_sequencer::type_id::create("vseqr", this);
// Coverage collectors, each conditional on what applies (§43, 20.5 §40).
link_cov = ucie_link_cov_sub::type_id::create("link_cov", this);
config_cov = ucie_config_cov_sub::type_id::create("config_cov", this);
resource_cov = ucie_resource_cov_sub::type_id::create("resource_cov", this);
recovery_cov = ucie_recovery_cov_sub::type_id::create("recovery_cov", this);
traffic_cov = ucie_traffic_cov_sub::type_id::create("traffic_cov", this);
if (cfg.reliability_in_path)
reliability_cov = ucie_reliability_cov_sub::type_id::create("reliability_cov",
this);
// Model handles for the collectors and the virtual sequencer (§51, §21).
uvm_config_db #(ucie_model_bundle)::set(this, "*", "models", sb.models());
endfunction
function void connect_phase(uvm_phase phase);
// §40's stream wiring, plus the sequencer handles.
vseqr.protocol_seqr = cfg.have_protocol_agent ? proto_agent.seqr : null;
vseqr.peer_seqr = cfg.have_peer_agent ? peer_agent.seqr : null;
vseqr.mgmt_seqr = cfg.have_mgmt_agent ? mgmt_agent.seqr : null;
vseqr.fault_seqr = cfg.have_fault_agent ? fault_agent.seqr : null;
vseqr.models = sb.models();
// ... the analysis connections of §40
endfunction
endclassThree notes.
The env builds what the config says exists, so §7's four DUT scopes are four config objects and one env class.
Reliability coverage is conditional on the mode (20.5 §40) — a Raw Mode run should not report a permanent shortfall on bins that cannot exist.
And the model bundle is pushed to "*" deliberately, which is the one wildcard this chapter endorses: it is a set of read-only handles, published once, at a scope confined to this environment's subtree. That is different from §45's global scenario knobs in every respect that matters — the values are handles rather than policy, they are immutable, and the scope is one env.
66. Phasing Discipline
Five rules, and each prevents a specific class of intermittent failure.
Build top-down, connect bottom-up, and do nothing else in either. No wait, no clock, no interface access in build_phase — a config lookup that fails there must uvm_fatal, not retry.
run_phase is the only place time passes. A component that needs to act at time zero acts on its first clock edge, not in start_of_simulation.
Objections belong to the test (§26), and the drain is a run_phase activity that precedes check_phase (§28).
check_phase runs after everything has drained, and it runs the environment-health checks before the design checks (20.4 §39).
And report_phase formats; it does not decide. A pass/fail decision made in report_phase from accumulated counters is a decision that could have been made in check_phase where it is visible as a check.
67. Environment Self-Checks
20.4 §42 listed five. The environment is where they are wired.
| Self-check | Where it runs |
|---|---|
| monitor unit tests — one event per stalled handshake, one per multi-beat object | standalone, with a synthetic interface driver |
| model invariants — counts match record states; indices agree with primaries | periodically in the scoreboard's run_phase |
| join health — tag with no work, work with no tag, tag collision | check_phase, first (20.4 §22) |
| minimum activity — per-boundary event counts non-zero | check_phase (20.4 §56) |
| seeded-defect regression — which check catches what | a separate regression, periodically |
Two notes.
The per-boundary event count is the cheapest and catches the worst failure. 20.4 §56: a run in which three monitors published and the fourth did not looks completely healthy in aggregate, and the fourth monitor's plane is entirely unverified. A virtual interface never assigned produces exactly this and emits no warning.
And the seeded-defect run's output is a mapping, not a pass rate. Knowing that "a recovery clearing the semantic table" is caught by §35's matrix check and by nothing else tells you exactly what is lost when that check is disabled for performance — and every environment eventually disables something for performance.
68. Reuse Across DUT Scopes
One environment class, four topologies (§7), and three things make the reuse real.
Agents are independently active or passive (§8). An Adapter-scope run makes two agents active; an endpoint-scope run makes one.
Monitors are unconditional. A boundary that is internal in one scope is external in another; the monitor is the same either way, and only the presence of a driver alongside it changes.
And the models are scope-independent because they consume observations. The semantic model does not know whether the semantic boundary is driven by an agent or by the DUT's own protocol engine — it consumes what the monitor saw. That is the deepest payoff of 20.4's independence rule: the model written for the endpoint environment works unchanged in the Adapter environment.
What genuinely must change per scope: the config object, the peer's abstraction level, and which checks apply (20.1 §51). Nothing else.
69. Debug — Which Component
Seven symptoms specific to a UVM environment for a layered protocol.
A sequence's expectations are wrong and the design is fine. The item_done confusion (§23). Check whether the sequence waited for a driver return or for a terminal obligation — and whether it was tested against a zero-latency responder.
The test passes with work outstanding. §27. Check the objection drop and the drain condition (§29), then check whether the end-of-test check is "zero outstanding" on a model something cleared (§33).
Obligations vanish at a recovery, and the design and the environment agree. §33. Check the reset policy table (§35) — specifically whether any component clears on the wire rather than on the kind.
Coverage reports a path exercised that the design never took. §52. Check whether the collector subscribes to a monitor or to a driver.
A scoreboard failure that is really an environment failure. 20.4 §22's join anomalies. Check the environment-health section first (20.4 §39) — a tag collision merges two histories and the checks run on neither.
Intermittent, seed-dependent monitor observations. §63. Check the clocking blocks — active-region sampling produces exactly this, and it presents as a protocol bug.
Nobody can say which driver is running. §49. Print the factory and count the overrides; the fix is architectural rather than another override.
70. Debug Checklist
An environment issue is suspected, or a green run is under suspicion. In order:
- Did every monitor publish events, per boundary (20.4 §56)?
- Are there join anomalies — tag without work, work without tag, tag collision (20.4 §22)?
- Do the model self-checks pass (§67)?
- Did the run meet its minimum-activity thresholds?
- Was the objection dropped after the drain condition, or after the stimulus (§27)?
- Did the drain complete, and if not, which clause failed (§29)?
- Were any observations still queued in an analysis FIFO at the drop (§39)?
- Does any component clear state on a reset wire rather than on a reset kind (§33)?
- Is every row of §35's table implemented, and does the semantic-model row say "annotate"?
- Does the resource model's
RESET_DIAGcell preserve the epochs (19.6 §27)? - Did any sequence assume completion at
finish_item(§24)? - Does any sequence wait on a design signal rather than on model state (§61)?
- Does any sequence force design state (§22)?
- Is any coverage sampled in a driver or a sequence (§53)?
- Does any monitor check, sample coverage, or log (§38)?
- Does any monitor sample outside a clocking block (§63)?
- Does the driver hold the whole bundle stable under stall (§64)?
- Does the driver call
item_done()at the interface transfer, not at semantic completion? - Does any model read a design-internal signal or call a design function (§54, §56)?
- Does the predictor use the design's own computation (§56)?
- Does any model read another model's state outside the join (20.4 §4)?
- Is the peer adversarial, or accommodating (§59)?
- Does the peer ever initiate a recovery, reorder, or offer a different capability set?
- Does every injected fault carry an expected detector and an expected recovery (§60)?
- Was the fault injected at an observed moment, or on a timer (§61)?
- Is scenario configuration in a config object, or in global
config_dbentries (§45)? - Does any component
get()from the config database outsidebuild_phase(§46)? - How many factory overrides are active, and can anybody name the running driver (§49)?
- Are the agents' active/passive settings correct for this DUT scope (§7)?
- Is any driver on an internal DUT boundary (§9)?
71. Common Misconceptions
"One UCIe agent should handle the whole link." Then concurrency across boundaries is impossible by construction, active/passive is all-or-nothing, reset means one thing, and the single monitor stream destroys layer-divergence analysis (§10).
"One transaction class is simpler." Most fields are invalid in most contexts, constraints become unwritable, the one-to-many relationships of the identity hierarchy cannot be represented, and the class becomes the hardest decision in the environment to reverse (§12).
"finish_item() means the operation completed." It means the driver accepted the item. With a real peer, a retry or a recovery in between, the semantic completion is a round trip plus retries plus possibly a full recovery later (§23).
"The test ends when the stimulus ends." Then a recovery scenario exits while the recovery is in progress, thirty obligations are live, and the run reports PASS (§27).
"A generous delay is a fine drain." It is right until a run has two recoveries and a retry storm — which is the run the scenario was written to produce (§30).
"Every component should handle reset itself." Then four independent, locally reasonable decisions align with a design bug and the regression is green on a link that loses work (§33).
"A monitor may as well call the scoreboard directly — it has the transaction." Then the monitor cannot be reused, the subscribers cannot be tested alone, ordering becomes implicit, and the monitor has quietly become a checker (§38).
"Sample coverage in the driver — it is the same transaction." It is not: it is what was asked for. A sequence that intended a retry and a DUT that never retried are indistinguishable from the stimulus side (§52).
"Reuse the design's function in the predictor — it is already tested." Then the comparison is f(x) == f(x) and no failure is possible (§56).
"A simple always-ready peer is fine to start with." It is, and it must not stay: it never tests backpressure, variable latency, reordering, batched credit return, a differing capability set, or a far-side recovery (§59).
"Global config_db entries make tests flexible." They make ownership invisible, typos silent, intent unreadable, and two instances impossible to configure differently (§45).
"The factory is there — use it." An override chain means nobody can name the running driver, and a debug component reaches production runs (§49).
"A sequence can force DUT state to reach a corner." Then the state may be unreachable in reality, the models did not observe it, and the corner was reachable legitimately by two primitives the environment already had (§22).
"Monitors can sample in the active region if the design is well behaved." Then observations are intermittent and seed-dependent, they disagree with SVA by one cycle, and the failures present as protocol bugs (§63).
72. Understanding Check
73. Summary and What Comes Next
A UVM environment is a wiring architecture for responsibilities, and it fails the moment its mechanics become the thing a reader must understand instead of the protocol.
Topology follows the DUT boundary. Active agents where the environment owns stimulus, passive monitors everywhere else, and never a driver on an internal boundary — because that is what destroys the layer-divergence analysis the whole plan is built on.
Four transaction layers, not one item. A stimulus item, two observation types and a link event — because a single class cannot represent one-to-many relationships and cannot distinguish what was asked from what happened.
item_done() is the driver's acceptance, and nothing more. A sequence needing semantic completion waits on the model's terminal state and checks which terminal state it reached.
The test ends on a drain condition, not on the stimulus. Six clauses, each naming a different model, with a bounded wait so a hang fails loudly and a correct fatal run is not cut off.
Distribute the meaning of a reset, not the wire. Four semantic kinds, thirteen component policies, and one row — the semantic model's annotate only — that decides whether the environment can detect a recovery clearing state it should preserve.
Monitors publish and do nothing else. No checking, no coverage sampling, no logging, no opinions — because every one of those makes the monitor unreusable and its subscribers untestable.
Coverage of the design is sampled from observations. A sequence that intended a retry and a design that never retried are the same thing on the stimulus side.
The predictor must not use the design's own computation, or the comparison is an identity and no failure is possible.
And the peer must be adversarial, because the standard exists precisely so that the far die was designed by somebody else, to different assumptions, and an accommodating peer verifies the environment's assumptions rather than the link.
Module 20 is now complete as an internal verification methodology: contracts, link composition, properties, models, coverage and the environment that runs them all. But everything in these six chapters answers to the team that wrote it. A green regression against your own reference models, your own coverage plan and your own peer is evidence about your interpretation — and interoperability is a claim about somebody else's. The next chapter draws that line precisely: what the Consortium's published material actually establishes about compliance testing, what an internal regression can and cannot demonstrate, how a conformance matrix and an interoperability matrix differ from a coverage report, and where the honest boundary of a compliance claim lies.
- 20.7 — UCIe Compliance Testing — UCIe-IF / consortium compliance gates.
Browse the full path on the UCIe tutorials index.