CXL · Module 25
UVM Architecture for CXL
A UVM environment that builds and completes can still be blind to most of what it watches. This chapter builds agent scope, active and passive modes, factory timing, objections, configuration scope, arbitration, analysis wiring, cross-agent coordination, elaboration cost and the assembled sign-off.
25.6 closed on a VIP reporting no errors. This chapter is the environment around it, and it has the same property: a UVM environment that elaborates without a warning and finishes without a hang is not an environment that saw anything.
Section 5 is the shape. One agent watching three interfaces cannot say which interface a transaction came from — and because it cannot count what it failed to attribute, it reports a hundred percent attributed, on six hundred transactions of which a hundred and fifty are unattributable.
1. The Engineering Problem — Six Silent Elaborations
One agent cannot attribute traffic from three interfaces. A hundred and fifty ambiguous transactions, and the environment reports none, because counting them needs the thing it does not have. Section 5.
An active agent on a DUT-driven bus drives against the design. Five active agents where two interfaces are host-driven is three sets of contended wires, and elaboration reports it clean. Section 6.
A factory override registered after the build does nothing. Twelve overrides with eight in time is four silent and twenty-four instances of the wrong type. Section 7.
A run phase waits only for the components that objected. Six producers with four raising is two nobody waits for and forty transactions lost at the end of the test. Section 8.
And a wildcard configuration path reaches whatever matches it. A path naming four targets that matches nine configures five components it did not name. Section 9.
This chapter against 25.6, stated precisely. That one owns a component you cannot read. This one owns the components you wrote yourself — and the finding is the same, which is the point: the opacity is structural rather than commercial. Section 14's weak definition is a simulation log with no errors in it.
2. The One-Sentence Model
A UVM environment is sound when every transaction's interface is named, no active agent sits on a bus the DUT drives, every factory override took effect, the run phase waits for every producer, and every analysis port has a subscriber — and "it elaborates and it runs" is none of those five.
3. What This Chapter Owns
| Ground | Owner |
|---|---|
| Whether a rule was checked at all | 25.1 |
| Whether a check was wide enough | 25.2 |
| When a check samples and who is told | 25.3 |
| Pairing a response to its request | 25.4 |
| What the coverage percentage measures | 25.5 |
| A component you did not write | 25.6 |
| The structure that holds all of them, and what it can see | this chapter |
Deferred:
| Deferred ground | Owner |
|---|---|
| Scoreboard keying and end-of-test drain | 25.4 §5 · §7 |
| Coverage bins, crosses and exclusions | 25.5 §5 · §6 · §7 |
| VIP configuration surface and revision gaps | 25.6 §5 · §6 |
| Trace-driven debug of a real failure | 26.1 onward |
| Cryptographic primitives | out of scope — see §4 |
4. Teaching-Model Boundary
Icarus Verilog 13.0 has no class support, so nothing in this chapter is written as UVM. Every model is a small synchronous block that computes the consequence of one architectural decision — how many agents, which are active, when an override is registered, who objects — and each is checked procedurally. The arithmetic is the teaching content; the class library is in IEEE 1800.2 and is not reproduced.
Three simplifications are worth stating. Section 5 treats attribution as a count rather than a per-transaction property. Section 10 models arbitration as a single served-count rather than a schedule over time. Section 13 prices elaboration as linear in component count, ignoring the factory's own lookup cost. In each case the conclusion is the same and the model is abbreviated.
Each model is built twice — a correct build and a broken build selected by a parameter. Every broken build here is the environment that is quicker to write: one agent, all agents active, overrides wherever they are convenient, objections in one place, a wildcard path, strict priority, ports assumed connected, per-agent stimulus. Each of them elaborates, which is the entire problem: UVM's build phase checks that the structure is legal, not that it is adequate.
Figure 1 — Both environments report a hundred percent attributed. The one on the bottom cannot count what it failed to attribute, because counting it would require the per-interface monitors it does not have — which is why the number is not merely wrong but unreachable from inside.
5. RTL 1 — One Agent Cannot Name Three Interfaces
// RTL 1 - one agent per interface. A single agent watching several interfaces
// cannot say which one a transaction came from, and the scoreboard needs to.
module agent_scope #(parameter int ONE_AGENT_FOR_ALL = 0) (
input logic clk, rst_n,
input logic build_it,
input logic [15:0] interfaces, txns_per_if, ambiguous_pairs,
output logic [15:0] agents, attributable, unattributable, attributed_pct,
output logic [15:0] truly_unattributable,
output logic source_known,
output logic [7:0] n_builds, n_ambiguous,
output logic source_lost_err
);
logic [31:0] t_q, p_q;
assign agents = (ONE_AGENT_FOR_ALL != 0) ? 16'd1 : interfaces;
assign t_q = {16'd0, interfaces} * {16'd0, txns_per_if};
// A transaction is attributable when its interface has its own monitor.
assign truly_unattributable = (agents >= interfaces) ? 16'd0
: ((ambiguous_pairs > t_q[15:0]) ? t_q[15:0] : ambiguous_pairs);
// A single agent has no way to count what it could not attribute.
assign unattributable = (ONE_AGENT_FOR_ALL != 0) ? 16'd0 : truly_unattributable;
assign attributable = (t_q[15:0] > unattributable)
? (t_q[15:0] - unattributable) : 16'd0;
assign p_q = (t_q == 32'd0) ? 32'd100
: (({16'd0, attributable} * 32'd100) / t_q);
assign attributed_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign source_known = (unattributable == 16'd0);
// Transactions whose interface cannot be named, on an environment that
// reports every source known.
assign source_lost_err = build_it && (truly_unattributable != 16'd0) && source_known;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_builds <= 8'd0; n_ambiguous <= 8'd0;
end else if (build_it) begin
n_builds <= n_builds + 8'd1;
if (!source_known) n_ambiguous <= n_ambiguous + 8'd1;
end
end
endmoduleFive builds. Two hundred transactions per interface unless stated.
| Interfaces / ambiguous | An agent each · One agent for all |
|---|---|
| 3 / 150 | 3 agents, 0 truly lost, 600 attributed, 100% · 1 agent, 150 truly lost, 0 reported, 100% claimed |
| 1 / 150 | 1 · 1 · both are the same environment |
| 2 / 0 ambiguous | 1 agent for two interfaces, nothing ambiguous to lose |
| 3 / 900 — more than exist | 600, clamped · still reported as 0 |
| 3, no transactions | nothing to attribute · nothing to lose |
The per-interface build never loses a source; the single agent never reports losing one.
A scoreboard needs to know which interface a transaction arrived on, and a monitor that watches three cannot tell it. The transactions are observed — every one of them — and the field that would name the source was never captured, because the monitor has no per-interface context to capture it from.
Row one is why the number is not merely wrong. The single agent reports zero unattributable because counting them requires knowing which interface each came from, which is exactly the capability it lacks. The report is self-consistent and unfalsifiable from inside. An environment cannot measure a blindness that is structural.
Row three is the configuration that hides it. Two interfaces and no ambiguous traffic: one agent attributes everything correctly, because nothing overlapped. Early directed tests look exactly like this, and the defect appears when the interfaces start carrying traffic at the same time.
6. RTL 2 — An Active Agent On A DUT-Driven Bus Elaborates Cleanly
// RTL 2 - active against passive. An agent on an interface the DUT drives must
// be passive, and an active one on the same wires fights the design.
module agent_mode #(parameter int ALL_AGENTS_ACTIVE = 0) (
input logic clk, rst_n,
input logic elaborate,
input logic [15:0] agents, dut_driven, host_driven,
output logic [15:0] active_count, passive_count, contended, driver_cost,
output logic modes_correct,
output logic [7:0] n_elabs, n_contended,
output logic contention_err
);
// Only the interfaces the environment drives need an active agent.
assign active_count = (ALL_AGENTS_ACTIVE != 0) ? agents : host_driven;
assign passive_count = (agents > active_count) ? (agents - active_count) : 16'd0;
// An active agent on a DUT-driven interface contends for the wires.
assign contended = (active_count > host_driven)
? (active_count - host_driven) : 16'd0;
// Each active agent carries a driver and a sequencer.
assign driver_cost = active_count * 16'd2;
// Nothing in elaboration checks for two drivers on one set of wires.
assign modes_correct = (ALL_AGENTS_ACTIVE != 0) ? 1'b1 : (contended == 16'd0);
assign contention_err = elaborate && (contended != 16'd0) && modes_correct;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_elabs <= 8'd0; n_contended <= 8'd0;
end else if (elaborate) begin
n_elabs <= n_elabs + 8'd1;
if (!modes_correct) n_contended <= n_contended + 8'd1;
end
end
endmoduleFour elaborations. Five agents unless stated.
| Host-driven / DUT-driven | Mode-aware · All-active |
|---|---|
| 2 / 3 | 2 active, 3 passive, 0 contended, 4 components · 5 active, 3 contended, 10 components, clean |
| 5 / 0 | 5 active, 0 passive · both builds identical |
| 4 / 1 | 4 active · 1 contended — the smallest that elaborates cleanly |
| no agents | 0 · 0 · nothing to contend |
The mode-aware build never contends; the all-active build never reports contending.
UVM's is_active field decides whether an agent carries a driver and a sequencer, and getting it wrong is not a structural error. An active agent on an interface the DUT drives instantiates a driver onto wires that already have one — two drivers, one net, and a build phase that has nothing to say about it.
Row one is the cost twice over. Three contended interfaces, and ten driver-and-sequencer components instead of four — the wrong configuration is also the more expensive one, which section 13 prices. The contention itself shows up as X propagation or as a signal that is simply never what the DUT drove.
Row three is the smallest instance. One interface misconfigured of five, elaborating clean, producing a single bus where the environment and the design disagree about who drives. It is the failure that looks like a DUT bug for a day.
7. RTL 3 — An Override Registered After The Build Does Nothing
// RTL 3 - the factory. An override registered after the component it would
// replace has been built changes nothing, and reports nothing.
module factory_timing #(parameter int OVERRIDE_ANY_TIME = 0) (
input logic clk, rst_n,
input logic elaborate,
input logic [15:0] overrides, before_build, instances_each,
output logic [15:0] effective, ineffective, wrong_instances, effective_pct,
output logic overrides_applied,
output logic [7:0] n_elabs, n_silent,
output logic late_override_err
);
logic [31:0] p_q;
// An override only reaches instances created after it is registered.
assign effective = (OVERRIDE_ANY_TIME != 0) ? overrides
: ((before_build > overrides) ? overrides : before_build);
assign ineffective = (overrides > effective) ? (overrides - effective) : 16'd0;
assign wrong_instances = ineffective * instances_each;
assign p_q = (overrides == 16'd0) ? 32'd100
: (({16'd0, effective} * 32'd100) / {16'd0, overrides});
assign effective_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign overrides_applied = (ineffective == 16'd0);
// Overrides registered too late, on an environment that reports them applied.
assign late_override_err = elaborate && (overrides > before_build)
&& overrides_applied;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_elabs <= 8'd0; n_silent <= 8'd0;
end else if (elaborate) begin
n_elabs <= n_elabs + 8'd1;
if (!overrides_applied) n_silent <= n_silent + 8'd1;
end
end
endmoduleFive elaborations. Twelve overrides, six instances each.
| Registered before the build | Effective · Ineffective · Wrong instances |
|---|---|
| 8 | 8 · 4 silent · 24 instances of the wrong type · 66% |
| 12 | 12 · 0 · 0 · 100% · applied |
| 11 | 11 · 1 · 6 wrong instances |
| 20 — more than exist | 12, clamped · 0 |
| no overrides | 0 · 0 · nothing to report |
Two elaborations with a silent override; none when the timing is ignored.
The factory resolves a type when the component is constructed, so an override registered afterwards resolves nothing. It does not warn, does not fail, and does not appear in any report — the environment builds the original type and runs. Four late overrides at six instances each is twenty-four components that are not what the test asked for.
Row one is the arithmetic of a common mistake. Overrides placed in a test's run_phase, or after super.build_phase() has already constructed the sub-components, are exactly this. The test's intent — a different driver, an error-injecting sequence item — silently does not happen, and the test passes for the wrong reason.
Row three is the sensitivity. One override of twelve registered late is six instances, which is enough to change a test's behaviour and small enough that nothing looks obviously wrong. The detector is a factory print at the end of elaboration, compared against the override list.
8. RTL 4 — The Run Phase Waits Only For Whoever Objected
// RTL 4 - objections. A run phase ends when the last objection drops, so a
// component that never raises one is a component the phase does not wait for.
module objection_scope #(parameter int TRUST_THE_PHASE = 0) (
input logic clk, rst_n,
input logic run_it,
input logic [15:0] producers, raising, drain_cycles, in_flight_at_end,
output logic [15:0] waited_for, unwaited, lost_txns, covered_pct,
output logic phase_waits,
output logic [7:0] n_runs, n_early,
output logic early_finish_err
);
logic [31:0] p_q;
// The phase waits only for the components that objected.
assign waited_for = (TRUST_THE_PHASE != 0) ? producers
: ((raising > producers) ? producers : raising);
assign unwaited = (producers > waited_for) ? (producers - waited_for) : 16'd0;
// A component nobody waits for loses whatever it still had in flight, unless
// the drain time happens to cover it.
assign lost_txns = (drain_cycles != 16'd0) ? 16'd0
: ((unwaited != 16'd0) ? in_flight_at_end : 16'd0);
assign p_q = (producers == 16'd0) ? 32'd100
: (({16'd0, waited_for} * 32'd100) / {16'd0, producers});
assign covered_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign phase_waits = (unwaited == 16'd0);
// Producers the phase does not wait for, on a run reported as complete.
assign early_finish_err = run_it && (producers > raising) && phase_waits;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_runs <= 8'd0; n_early <= 8'd0;
end else if (run_it) begin
n_runs <= n_runs + 8'd1;
if (!phase_waits) n_early <= n_early + 8'd1;
end
end
endmoduleSix runs. Six producers, forty transactions in flight at the end unless stated.
| Raising / drain | Waited for · Unwaited · Lost |
|---|---|
| 4 / none | 4 · 2 · 40 lost · 66% — the trusting model reports 100% and complete |
| 6 / none | 6 · 0 · 0 · complete |
| 5 / a drain time | 5 · 1 unwaited · 0 lost — the drain covered it |
| 9 — more than exist | 6, clamped · 0 |
| 0 / none | 0 · 6 unwaited · 40 lost · 0% |
| no producers at all | 0 · 0 · 100% · complete |
Three runs finishing early; none when the phase is trusted.
A UVM run phase ends when the last objection is dropped, and a component that never raised one is invisible to that decision. Six producers with four objecting means the phase ends while two are still working — and whatever they had in flight is discarded, silently, as part of a normal end of test.
Row three separates the two failures, and it matters. A producer that does not object but finishes inside the drain time loses nothing — the environment got away with it. The objection is still missing, and the next test with slightly different timing does not get away with it. phase_waits and lost_txns are different outputs because a latent defect and a manifested one are different facts.
Row five is the version that is easy to create and hard to see. Nobody raises an objection at all, the run phase ends immediately, and the test passes in almost no time — which looks like a fast test rather than an empty one. The counter that catches it is transactions completed per test.
9. RTL 5 — A Wildcard Path Configures What It Did Not Name
// RTL 5 - configuration scope. A wildcard path reaches more components than it
// names, and a setting nobody reads is a setting that did nothing.
module config_scope #(parameter int WILDCARD_EVERYTHING = 0) (
input logic clk, rst_n,
input logic apply_it,
input logic [15:0] targets, matched_by_path, readers, settings,
output logic [15:0] reached, over_reached, unread, reached_pct,
output logic scope_exact,
output logic [7:0] n_applies, n_wrong,
output logic scope_too_wide_err
);
logic [31:0] p_q;
// A precise path reaches its targets; a wildcard reaches whatever matches.
assign reached = (WILDCARD_EVERYTHING != 0) ? matched_by_path
: ((matched_by_path > targets) ? targets : matched_by_path);
assign over_reached = (reached > targets) ? (reached - targets) : 16'd0;
// A setting written and never read did nothing at all.
assign unread = (settings > readers) ? (settings - readers) : 16'd0;
assign p_q = (targets == 16'd0) ? 32'd100
: (({16'd0, reached} * 32'd100) / {16'd0, targets});
assign reached_pct = (p_q > 32'd65535) ? 16'hFFFF : p_q[15:0];
assign scope_exact = (over_reached == 16'd0) && (unread == 16'd0);
// Components configured that the path did not name.
assign scope_too_wide_err = apply_it && (over_reached != 16'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_applies <= 8'd0; n_wrong <= 8'd0;
end else if (apply_it) begin
n_applies <= n_applies + 8'd1;
if (!scope_exact) n_wrong <= n_wrong + 8'd1;
end
end
endmoduleFive applications. A path naming four targets, twelve settings.
| Matched by path / settings read | Reached · Over-reached · Unread |
|---|---|
| 9 / all 12 read | 4 reached, 0 over · 9 reached, 5 over, 225% of what it named |
| 4 / all read | 4 · 0 · the wildcard happens to be precise |
| 5 / all read | 4 · 1 over — the smallest over-reach |
| 4 / 9 of 12 read | 4 · 0 · 3 settings written and never read |
| no targets | 0 · 0 · 100% |
One inexact with a precise path — the unread settings; three with a wildcard.
A uvm_config_db set with a wildcard in the path reaches every component whose full name matches, which is usually more than the author had in mind. A path naming four agents that matches nine configures five components that were never intended to receive it — and those five get a setting that may be harmless, or may quietly change their behaviour for the rest of the run.
Row one is the number that makes it concrete. 225% of what the path named. A configuration reach above a hundred percent is the direct symptom, and it is computable at elaboration from the path and the component tree.
Row four is the opposite failure and it is in the same model on purpose. Three settings written and never read: nothing over-reached, and three decisions that did nothing. A setting with no get is as inert as an override registered late (section 7), and neither produces a message.
10. RTL 6 — Strict Priority Starves And Reports A Share
// RTL 6 - sequencer arbitration. A strict-priority sequencer starves the
// lowest priority requester for as long as a higher one has work.
module arb_policy #(parameter int STRICT_PRIORITY = 0) (
input logic clk, rst_n,
input logic run_it,
input logic [15:0] seq_count, grants, high_pri_busy_pct, seq_items,
output logic [15:0] served, starved, low_pri_share, worst_wait,
output logic fair_enough,
output logic [7:0] n_runs, n_starved,
output logic starvation_err
);
logic [31:0] s_q, w_q;
// Round-robin shares the grants; strict priority gives them to the top.
assign s_q = (STRICT_PRIORITY != 0) ? 32'd1
: ((seq_count == 16'd0) ? 32'd0 : {16'd0, seq_count});
assign served = (s_q > 32'd65535) ? 16'hFFFF : s_q[15:0];
assign starved = (seq_count > served) ? (seq_count - served) : 16'd0;
assign low_pri_share = (STRICT_PRIORITY != 0) ? (16'd100 - high_pri_busy_pct)
: ((seq_count == 16'd0) ? 16'd0 : (16'd100 / seq_count));
// The lowest priority waits as long as the high-priority stream is busy.
assign w_q = (STRICT_PRIORITY != 0)
? (({16'd0, seq_items} * {16'd0, high_pri_busy_pct}) / 32'd100)
: {16'd0, seq_count};
assign worst_wait = (w_q > 32'd65535) ? 16'hFFFF : w_q[15:0];
assign fair_enough = (starved == 16'd0);
// A requester that never gets a grant, reported with a share of the
// bandwidth it never receives.
assign starvation_err = run_it && (starved != 16'd0) && (low_pri_share != 16'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_runs <= 8'd0; n_starved <= 8'd0;
end else if (run_it) begin
n_runs <= n_runs + 8'd1;
if (!fair_enough) n_starved <= n_starved + 8'd1;
end
end
endmoduleFive runs. Two hundred items, a high-priority stream busy 80% of the time.
| Requesters / busy | Round-robin · Strict priority |
|---|---|
| 4 / 80% | 4 served, 0 starved, 25% each, wait 4 · 1 served, 3 starved, 20% claimed, wait 160 |
| 4 / 100% | — · 3 starved, 0% claimed — honest |
| 1 / 80% | 1 · 1 · both identical |
| 2 / 80% | 2 served · 1 starved with a 20% share claimed |
| no requesters | 0 · nothing to share |
Round-robin never starves; strict priority starves on three of five.
A sequencer's arbitration mode decides which sequence gets the next item, and the default is not fair. SEQ_ARB_STRICT_FIFO and priority modes will hand every grant to the highest-priority requester for as long as it has work — and a background traffic sequence at a lower priority simply never runs.
Row two is the honest case and it is worth contrasting with row one. A high-priority stream that never idles reports a zero percent share for the lower priorities — which is exactly what they get. The error fires on row one, not row two: claiming twenty percent to a requester that receives nothing is the defect, and claiming nothing is merely bad news.
Row four is the smallest instance. Two requesters, one starved, and a reported twenty percent share that never materialises. Two sequences on one sequencer is the most common configuration in any environment.
11. RTL 7 — An Analysis Port With No Subscriber Discards Silently
// RTL 7 - analysis connectivity. A monitor writes to an analysis port, and a
// port with no subscriber discards every transaction without complaint.
module analysis_wiring #(parameter int ASSUME_CONNECTED = 0) (
input logic clk, rst_n,
input logic wire_it,
input logic [15:0] ports, subscribed, txns_per_port,
output logic [15:0] connected, dangling, dropped, connected_pct,
output logic fully_wired,
output logic [7:0] n_wirings, n_dangling,
output logic dangling_port_err
);
logic [31:0] p_q;
assign connected = (subscribed > ports) ? ports : subscribed;
assign dangling = (ports > connected) ? (ports - connected) : 16'd0;
assign dropped = dangling * txns_per_port;
assign p_q = (ports == 16'd0) ? 32'd100
: (({16'd0, connected} * 32'd100) / {16'd0, ports});
assign connected_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
// Nothing reports an unsubscribed analysis port.
assign fully_wired = (ASSUME_CONNECTED != 0) ? 1'b1 : (dangling == 16'd0);
assign dangling_port_err = wire_it && (dangling != 16'd0) && fully_wired;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_wirings <= 8'd0; n_dangling <= 8'd0;
end else if (wire_it) begin
n_wirings <= n_wirings + 8'd1;
if (!fully_wired) n_dangling <= n_dangling + 8'd1;
end
end
endmoduleFive wirings. Nine analysis ports, 300 transactions each.
| Subscribed | Connected · Dangling · Discarded |
|---|---|
| 6 | 6 · 3 dangling · 900 transactions discarded · 66% |
| 9 | 9 · 0 · 0 · fully wired |
| 8 | 8 · 1 · 300 discarded |
| 12 — more than exist | 9, clamped · 0 |
| no ports at all | 0 · 0 · 100% |
Two wirings with a dangling port; none when connection is assumed.
uvm_analysis_port::write() on a port with no subscriber is a no-op. It does not warn, does not return a status, and costs nothing — the monitor observes the transaction, publishes it, and it goes nowhere. Three unsubscribed ports at three hundred transactions each is nine hundred observations that were made and thrown away.
Row one is the shape of the failure. The monitor works. The scoreboard works. The connect_phase line that joins them was never written, or was written against the wrong handle, and every component involved reports itself healthy. The coverage model and the scoreboard both go quiet in a way that looks like clean traffic.
Row three is the detector's sensitivity. One dangling port of nine is three hundred transactions, and the environment's only symptom is that one checker never fires — which is 25.3 §6's vacuity, arriving through the connectivity rather than through the property.
12. RTL 8 — Per-Agent Stimulus Cannot Express A Cross-Interface Order
// RTL 8 - coordinating agents. A scenario that needs two interfaces in a fixed
// order needs something above both sequencers, and per-agent stimulus cannot
// express it.
module cross_agent #(parameter int PER_AGENT_ONLY = 0) (
input logic clk, rst_n,
input logic plan_it,
input logic [15:0] scenarios, single_agent_ok, ordered_pairs,
output logic [15:0] expressible, inexpressible, orderings_lost, express_pct,
output logic scenarios_covered,
output logic [7:0] n_plans, n_short,
output logic coordination_missing_err
);
logic [31:0] p_q;
// Per-agent stimulus can only express what one interface can do alone.
assign expressible = (PER_AGENT_ONLY != 0)
? ((single_agent_ok > scenarios) ? scenarios : single_agent_ok)
: scenarios;
assign inexpressible = (scenarios > expressible)
? (scenarios - expressible) : 16'd0;
assign orderings_lost = (PER_AGENT_ONLY != 0) ? ordered_pairs : 16'd0;
assign p_q = (scenarios == 16'd0) ? 32'd100
: (({16'd0, expressible} * 32'd100) / {16'd0, scenarios});
assign express_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign scenarios_covered = (inexpressible == 16'd0);
// Ordered cross-interface scenarios that nothing can produce.
assign coordination_missing_err = plan_it && (orderings_lost != 16'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_plans <= 8'd0; n_short <= 8'd0;
end else if (plan_it) begin
n_plans <= n_plans + 8'd1;
if (!scenarios_covered) n_short <= n_short + 8'd1;
end
end
endmoduleFive plans. Forty scenarios.
| Doable by one interface / ordered pairs | Coordinated · Per-agent |
|---|---|
| 28 / 12 | 40 expressible, 0 lost · 28 expressible, 12 lost, 70% |
| 40 / 0 | 40 · 40 · no coordination needed |
| 39 / 1 | 40 · 1 inexpressible, 1 ordering lost |
| 60 — more than exist | 40, clamped · 12 orderings still lost |
| no scenarios | 0 · 0 · 100% |
The coordinating layer is never short; per-agent stimulus is short on two of five.
A scenario like "issue a CXL.mem read, then a CXL.cache snoop to the same line, in that order" is not a property of either interface. Each agent's sequencer knows only its own stream, so a fixed order between two of them needs a virtual sequence above both — and without one, the twelve ordered pairs are not generated late or rarely, they are not expressible at all.
Row four is the row that separates the two counts. More solo-capable scenarios than exist clamps the expressible count to forty and still loses twelve orderings — because an ordering is a property of a pair, not a count of scenarios. inexpressible and orderings_lost measure different things, which is why both are outputs.
Row two is the exemption. A scenario set that needs no cross-interface ordering is fully expressible per-agent, and the coordinating layer is complexity with no return. The decision is a property of the scenario list, and it is worth making explicitly rather than by default.
13. RTL 9 — Elaboration Is Paid On Every Test Before Any Stimulus
// RTL 9 - what the environment costs to build and run. Every component is
// constructed on every test, and the elaboration happens before any stimulus.
module env_cost #(parameter int RUNTIME_ONLY = 0) (
input logic clk, rst_n,
input logic budget,
input logic [15:0] components, ms_per_component, tests, run_seconds,
input logic [15:0] budget_min,
output logic [15:0] elab_sec, elab_min, run_min, total_min, overrun,
output logic affordable,
output logic [7:0] n_budgets, n_over,
output logic elab_cost_ignored_err
);
logic [31:0] e_q, em_q, r_q, t_q;
assign e_q = ({16'd0, components} * {16'd0, ms_per_component}) / 32'd1000;
assign elab_sec = (e_q > 32'd65535) ? 16'hFFFF : e_q[15:0];
// Elaboration is paid on every test, before a single transaction.
assign em_q = (RUNTIME_ONLY != 0) ? 32'd0
: (({16'd0, elab_sec} * {16'd0, tests}) / 32'd60);
assign elab_min = (em_q > 32'd65535) ? 16'hFFFF : em_q[15:0];
assign r_q = ({16'd0, run_seconds} * {16'd0, tests}) / 32'd60;
assign run_min = (r_q > 32'd65535) ? 16'hFFFF : r_q[15:0];
assign t_q = {16'd0, elab_min} + {16'd0, run_min};
assign total_min = (t_q > 32'd65535) ? 16'hFFFF : t_q[15:0];
assign overrun = (total_min > budget_min) ? (total_min - budget_min) : 16'd0;
assign affordable = (total_min <= budget_min);
// An elaboration paid on every test, costed at nothing.
assign elab_cost_ignored_err = budget && (elab_sec != 16'd0) && (tests != 16'd0)
&& (elab_min == 16'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_budgets <= 8'd0; n_over <= 8'd0;
end else if (budget) begin
n_budgets <= n_budgets + 8'd1;
if (!affordable) n_over <= n_over + 8'd1;
end
end
endmoduleSix budgets. Two thousand tests of ninety seconds, 40 ms per component.
| Components / budget | Elaborate · Across the suite · Run · Total |
|---|---|
| 1,200 / 3,000 min | 48 sec · 1,600 min · 3,000 · 4,600 — 1,600 over |
| 1,200 / 4,600 min | 48 · 1,600 · 3,000 · exactly fits |
| 300 / 4,600 | 12 · 400 · 3,000 · 3,400 |
| no components | 0 · 0 · 3,000 · 3,000 |
| 1,200, one test | 48 · 0 — rounds to no minutes · both models agree |
| 1,200, no tests | 48 · 0 · 0 · a suite with no tests, not an ignored cost |
One budget over when the elaboration is charged; none when it is not.
Forty-eight seconds of elaboration is unremarkable; sixteen hundred minutes of it is a nightly regression that does not finish. The cost is per test, it happens before any stimulus, and it scales with the component count that sections 6 and 11 both increase — ten driver-and-sequencer components instead of four is a direct contribution here.
Row one is the trade that gets made badly. Elaboration is thirty-five percent of the total and it produces no stimulus at all. The corrective is architectural — fewer components, or a shared elaboration across tests where the tool supports it — and it is invisible until somebody separates the two numbers.
Rows five and six are two different zeros. One test rounds the per-suite elaboration to nothing and both models agree, which is a rounding boundary. No tests at all costs nothing because nothing ran, and the error correctly stays quiet — a suite with no tests is not an environment with an ignored cost.
Figure 3 — Thirty-five percent of the regression produces no stimulus at all. The component count is set by decisions in sections 6 and 11 — an all-active environment carries ten driver-and-sequencer components where four would do — so the architecture and the wall clock are the same conversation.
14. RTL 10 — A UVM Environment Assembled
// RTL 10 - a UVM environment assembled. Everything that must hold before "the
// environment builds and runs" is a claim about what it can observe and drive.
module env_signoff #(parameter int IT_RUNS = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic it_runs, // the environment elaborates and completes
input logic sources_known, // every transaction's interface is named
input logic modes_correct, // no active agent on a DUT-driven bus
input logic overrides_applied,// every factory override took effect
input logic phase_waits, // the run phase waits for every producer
input logic fully_wired, // every analysis port has a subscriber
output logic sound,
output logic [5:0] fail_mask,
output logic [7:0] n_eval, n_sound,
output logic false_soundness_err
);
assign fail_mask[0] = ~it_runs;
assign fail_mask[1] = ~sources_known;
assign fail_mask[2] = ~modes_correct;
assign fail_mask[3] = ~overrides_applied;
assign fail_mask[4] = ~phase_waits;
assign fail_mask[5] = ~fully_wired;
// The it-runs build is what a clean simulation log says.
assign sound = (IT_RUNS != 0) ? it_runs : (fail_mask == 6'd0);
assign false_soundness_err = evaluate && sound && (fail_mask != 6'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_eval <= 8'd0; n_sound <= 8'd0;
end else if (evaluate) begin
n_eval <= n_eval + 8'd1;
if (sound) n_sound <= n_sound + 8'd1;
end
end
endmoduleSeven configurations.
| What fails | Mask · Full model · Simulation log |
|---|---|
| nothing | 000000 · sound · sound |
| one agent for three interfaces — §5 | 000010 · not sound · claims sound |
| active agents on DUT-driven wires — §6 | 000100 · not sound · claims sound |
| four overrides that never applied — §7 | 001000 · not sound · claims sound |
| two producers the phase ignores — §8 | 010000 · not sound · claims sound |
| three unsubscribed analysis ports — §11 | 100000 · not sound · claims sound |
| the environment failed to run | 000001 · not sound · not sound |
One sound under the full model; six under the log.
Every one of the five middle rows elaborates without a warning and finishes without a hang. UVM's build and connect phases check that the structure is legal — that handles resolve, that ports have a type, that the tree constructs. None of them checks that the structure is adequate to observe the design, and there is no phase that could.
Rows two and six are the same failure in two places. A transaction the environment cannot attribute and a transaction it publishes to nobody are both observations that were made and discarded — the first by a monitor with no context, the second by a port with no subscriber. Neither is an error in any component.
These six are not 25.6's six, and the relationship is worth stating: that chapter's failures all lived inside a component somebody else wrote. These live inside components you wrote yourself, and they are just as silent — which means the opacity was never about the vendor.
Figure 4 — Attribution is asked first because it is the failure the environment cannot measure about itself; the others are all countable at elaboration by somebody who knows to count. Every exit above is a clean simulation log.
15. Quantitative Reasoning
Agent scope. One agent for three interfaces is 150 of 600 transactions unattributable and 0 reported — a hundred percent claimed.
Agent modes. Five active agents where two interfaces are host-driven is three contended buses and ten driver components instead of four.
Factory timing. Twelve overrides with eight in time is four silent and 24 instances of the wrong type; one late override is six.
Objections. Six producers with four raising is two unwaited and forty transactions lost; nobody raising is all six and the test finishes immediately.
Configuration scope. A path naming four targets that matches nine reaches 225% of what it named; three settings of twelve never read is three decisions that did nothing.
Arbitration. Four requesters under strict priority is one served and three starved, with a twenty percent share reported and a worst wait of 160 items.
Analysis wiring. Nine ports with six subscribed is three dangling and 900 transactions discarded; one dangling port is three hundred.
Coordination. Forty scenarios with twenty-eight doable solo is twelve inexpressible and twelve orderings lost — 70%.
Elaboration. Twelve hundred components at 40 ms is 48 seconds, paid 2,000 times — 1,600 minutes against 3,000 of run time.
The assembled model. Six properties, seven configurations, one sound. The log called six sound.
| Quantity | Correct · Broken · Ratio |
|---|---|
| Transactions attributed, of 600 | 600 · 450 actual, 600 claimed |
| Contended buses, 5 agents | 0 · 3 · all the DUT-driven ones |
| Driver components for 5 agents | 4 · 10 · 2.5x |
| Overrides that took effect, of 12 | 8 · 12 claimed · 24 wrong instances |
| Producers the phase waits for, of 6 | 4 · 6 claimed · 40 lost |
| Configuration reach against 4 targets | 100% · 225% · 5 extra |
| Requesters served, of 4 | 4 · 1 · 3 starved |
| Analysis ports connected, of 9 | 6 · 9 assumed · 900 discarded |
| Scenarios expressible, of 40 | 40 · 28 · 12 orderings lost |
| Regression minutes for 2,000 tests | 4,600 · 3,000 counted · 35% unbudgeted |
| Configurations called sound, of 7 | 1 · 6 · 5 false claims |
16. Assertions
Every check is an explicit comparison against an exact value. Icarus Verilog 13.0 has no class support and no concurrent assertions, so each is a procedural comparison against 1'b1, and every one is an equality.
Two checks were run before the campaign, and both found something.
First, every error signal was tested for mutual exclusivity with its own guard. An expression of the form (x != 0) && flag where flag is (x == 0) can never fire — and three of the first six models were written that way. Sections 5, 6 and 10 were all repaired before a single testbench line existed, by separating what is true from what the environment reports. That distinction is the chapter's subject, so getting it wrong in the models was apt and worth recording.
Second, the scripted output-listing step — extract every output, map it to its testbench net through that module's own instantiations, and check the net appears in a chk. It reported seventeen nets and four were real gaps, all four the same thing: the broken build's headline percentage, the number it reports about itself. The scoping to the module's own instantiations was added this batch after a name shared between one model's output and another's input produced a false positive.
Alongside those: every inclusive threshold at exactly equal, every clamp driven past its cap, every floor past its boundary, and both builds asserted on every degenerate case.
Agent scope. The ambiguity count is driven above the transaction count, which exercises the clamp.
chk(aBv == 16'd600,"the count clamps at the transactions that exist");
chk(aBp == 16'd100,"reporting a hundred percent attributed");Agent modes. One interface short of all-host-driven is driven — the smallest contention that elaborates.
Factory timing. Overrides registered before the build are driven above the override count, and one short of complete.
Objections. A producer that does not object but finishes inside the drain time is driven, separating the latent defect from the manifested one.
chk(bGu == 16'd1, "one producer unwaited");
chk(bGl == 16'd0, "but the drain time covers it, so nothing is lost");Configuration scope. Over-reach and unread settings are driven separately, because they are different defects reaching the same boolean.
Arbitration. A high-priority stream at a hundred percent busy is driven, where strict priority reports a zero share and the error correctly stays quiet.
Analysis wiring. More subscribers than ports is driven, exercising the clamp.
Coordination. More solo-capable scenarios than exist is driven, where the expressible count clamps and twelve orderings are still lost.
Elaboration. A one-test suite (rounds to zero, both builds agree) and a zero-test suite are both driven — the second is what killed one of the two survivors.
The assembled model. Every fail mask is asserted as an exact six-bit value, and each of the six bits is driven false alone.
Totals: 299 checks across two testbenches, 154 on the front five models and 145 on the back five, all passing on the unmutated sources.
17. Mutation Testing
Eighty mutations were injected one at a time. 80 injected, 80 killed, after two survivors — the lowest survivor count of any chapter in the last three batches.
| Mutation class | Killed by |
|---|---|
| The two agent-count models swapped | Three agents against one — §5 row one |
| Attribution requiring strictly more agents than interfaces | Three agents for three interfaces — §5 row one |
| The ambiguity clamp taken the wrong way | 900 ambiguous against 600 transactions — §5 row four |
| The single agent reporting what it cannot count | 0 reported against 150 truly lost — §5 row one |
| The two mode models swapped | Two active against five — §6 row one |
| Contention counted against the DUT-driven side | Three contended, not two — §6 row one |
| One component per active agent | Four components, not two — §6 row one |
| The all-active build reporting its own contention | A clean elaboration on three contended buses — §6 row one |
| The effective-override clamp taken the wrong way | Twenty registered against twelve overrides — §7 row four |
| Overrides equal to the pre-build count called late | Twelve of twelve in time — §7 row two |
| The objection wait clamp taken the wrong way | Nine raising against six producers — §8 row four |
| A drain time losing the transactions anyway | One unwaited producer inside the drain — §8 row three |
| No producers reporting nothing covered | An environment with no producers — §8 row six |
| The configuration reach clamp taken the wrong way | Nine matched against four targets — §9 row one |
| Exact scope ignoring the unread settings | Three settings written and never read — §9 row four |
| The two arbitration policies swapped | Four served against one — §10 row one |
| A zero share still counting as a claim | A high-priority stream at 100% busy — §10 row two |
| The analysis connection clamp taken the wrong way | Twelve subscribers against nine ports — §11 row four |
| The coordinating layer losing the orderings | Zero lost against twelve — §12 row one |
| The elaboration paid once rather than per test | 1,600 minutes, not zero — §13 row one |
| The has-tests guard dropped | A suite with no tests — §13 row six |
| Each of the six mask bits reading a neighbour | Six configurations, each failing one property alone — §14 |
| Every counter's polarity inverted | Ten pairs of totals — every section |
Both survivors were undriven degenerate inputs, and neither was a clamp or a dominated guard — the two classes that dominated batch 024. An environment with no producers and a suite with no tests are both zero-valued inputs at the top of the model rather than at a threshold, and the corrective was one case each.
The pre-campaign checks are why there were only two. Eleven of the twenty mutation classes above target a clamp, and every one of them was killed on the first run because the clamps were driven past their caps while the testbench was being written rather than after a survivor asked. Batch 024's rule — for every clamp, drive the input past the limit once — cost nothing to apply and removed the entire class.
And three dead error signals never reached the campaign at all, because the mutual-exclusivity check ran first. Those would have appeared as three unkillable mutations and three diagnosis cycles.
18. Verification Strategy
What a testbench for an architecture model must cover.
Check every error signal for mutual exclusivity with its own guard, before writing a testbench. Three of ten models here. The pattern is (x != 0) && flag where flag is derived from x == 0 — write out what the environment reports separately from what is true, and the expression becomes writable.
Run the scripted output-listing step, scoped to each module's own instantiations. Four real gaps here, and the scoping matters: an output of one model sharing a name with an input of another produced a false positive before it was added.
For every clamp, drive the input past the limit once. Eleven mutation classes, all killed on the first run. This is the highest-return single habit in the last two batches.
Separate the latent defect from the manifested one. §8's phase_waits and lost_txns are different outputs because a producer that does not object but finishes in time has the defect and not the symptom — and the next test with different timing has both.
The cases where the quicker environment is right. A single interface. Every interface host-driven. All overrides in time. Every producer objecting. A precise path. One requester. Every port subscribed. A scenario set needing no coordination. Eight exemptions across nine models, each a real configuration.
Counters as a second signature. Ten models, ten pairs of totals, differing in six. Four are deliberately equal — §5's n_ambiguous (the single agent never reports losing a source, which is the whole point), §6's n_contended, and two more where the counted property is a fact about the inputs rather than about the reporting.
What a real environment needs that these models do not have. Per-transaction attribution rather than a count for §5, a time-ordered arbitration schedule for §10, and a factory-aware elaboration model for §13. All three are abbreviations that preserve the conclusion, and section 26 exercises 1, 6 and 9 are where they come back.
19. Synthesis and Implementation Reality
is_active is a field, and nothing validates it against the design. The corrective is to derive it: a table of interfaces with who drives each, checked at elaboration against every agent's configuration. It is twenty lines and it removes section 6 permanently.
Factory overrides belong in build_phase before super.build_phase(), which is the line most often placed wrongly. A print of the factory's override table at the end of elaboration, compared against the test's intent, is the only reliable detector — and uvm_factory::print() already produces it.
uvm_config_db wildcards are matched against full component names, so a path like *_agent* reaches everything whose name contains it — including components added a year later by somebody who never saw the path. Precise paths age better than convenient ones.
Objections are the mechanism most often delegated to one place, usually the test or a single sequence. That is correct when one component's completion implies everything else's, and section 8 is what happens when it does not. The rule is that anything with transactions in flight at end of test must object.
connect_phase failures are silent by construction, because an unconnected analysis port is a legal object. A loop over the environment's ports checking get_provided_to() or the subscriber count is the detector, and nothing in the library runs it for you.
Elaboration time scales with component count and with the factory's lookup, so section 13's linear model is optimistic. The practical lever is the same either way: fewer components, which sections 6 and 11 both influence.
20. Silicon Observability
| Counter | Why it matters |
|---|---|
| Transactions per interface, from each monitor separately | §5 — a single total cannot show an attribution hole |
| Agents by mode, against a table of who drives each interface | §6 — the check nothing runs for you |
| Factory override table at end of elaboration, against the test's list | §7 — the only reliable detector, and it already exists |
| Objections raised, by component, per run | §8 — the list of who the phase actually waits for |
| Transactions in flight at end of phase | §8 — the number that turns a latent defect into a measured one |
| Configuration entries set against entries read | §9 — a set with no get did nothing |
| Components matched by each wildcard path | §9 — reach above a hundred percent is the direct symptom |
| Grants per sequence, per sequencer | §10 — a share of zero is a starved requester |
| Analysis ports with zero subscribers | §11 — a loop over the tree, and nothing runs it |
| Elaboration seconds against run seconds, per test | §13 — two numbers that are always reported as one |
"Analysis ports with zero subscribers" is the cheapest and it is a loop. Walk the component tree, ask each analysis port how many subscribers it has, and fail elaboration on zero. It is perhaps thirty lines, it runs in milliseconds, and it removes an entire failure class — and the reason it is rare is that nothing in the methodology suggests it.
21. Debug Lab
Symptom. A CXL device's UVM environment has been running a clean nightly for five months — zero failures, coverage climbing, no hangs. At integration with the SoC, three bugs surface in a week, all in areas the environment nominally covers.
Step 1 — how many transactions did the scoreboard actually compare? 25.4 §20's counter is added. It reports comparisons well below the monitor's transaction count. The gap points at either the key or the wiring.
Step 2 — the wiring. A loop over the component tree counts subscribers on every analysis port. Section 11. Three of nine have none — the connect_phase for the error-injection monitor was written against a handle that was reassigned, so it connects to a port that nothing reads. Nine hundred transactions per run, observed and discarded.
Step 3 — the attribution. The scoreboard receives .mem and .cache traffic from a single monitor. Section 5. The transaction class has no source field because the monitor has no source to record — one agent, three interfaces. The scoreboard has been comparing .cache responses against .mem requests whenever the tags happened to collide.
Step 4 — why coverage still climbed. The coverage model samples from the same analysis port as the scoreboard, and it was one of the six that were connected. Coverage is a genuine measurement of traffic that genuinely occurred — it simply cannot distinguish traffic that was checked from traffic that was not. 25.5 §10, reached through the wiring.
Step 5 — the error injection that never ran. The test registers a factory override for an error-injecting driver. Section 7. It is registered after super.build_phase(), so the original driver is constructed and the override resolves nothing. Five months of nightly error-injection tests ran the normal driver.
Step 6 — and one more. The environment's background traffic sequence sits at a lower priority on a sequencer set to SEQ_ARB_STRICT_FIFO. Section 10. The foreground sequence is busy for most of every test, so the background sequence has issued almost nothing since it was added.
The finding. Four independent structural defects, none of which produced a single message in five months: three dangling ports, one agent for three interfaces, a late factory override, and a starved sequence. Every component in the environment was correct.
The fix. Split the agent per interface and add a source field to the transaction. Move the override before super.build_phase(). Set the sequencer to round-robin. Fix the connect_phase handle. Then add the four elaboration checks from section 20 — subscriber count, agent modes against the interface table, the factory table against the test's list, and objections by component — because every one of these four would have failed elaboration on day one.
What made this hard. Nothing was broken, nothing warned, and the environment did exactly what it was built to do. The five months of green were accurate reports about a structure that could not see the design.
22. Design Review
1. How many agents, and how many interfaces? One agent cannot name three sources. Section 5.
2. Does the transaction class carry the interface it came from? If the monitor has no source, the class cannot have the field. Section 5.
3. Which agents are active, and which interfaces does the DUT drive? A table of the second, checked against the first, at elaboration. Sections 6 and 19.
4. Where are the factory overrides registered, relative to super.build_phase()? After it, they resolve nothing. Sections 7 and 19.
5. Does the factory's override table match the test's intent? uvm_factory::print() already produces it. Sections 7 and 20.
6. Which components raise objections, and does everything with traffic in flight? Six producers, four objecting, forty lost. Section 8.
7. How many analysis ports have zero subscribers? A thirty-line loop that nothing in the methodology suggests. Sections 11 and 19.
8. What is each wildcard configuration path's reach, and its intended target count? 225% is the direct symptom. Section 9.
9. What is the sequencer arbitration mode, and the grant count per sequence? A share of zero is a starved sequence. Section 10.
10. What does "it elaborates and it runs" establish? Section 14 exists because the answer is the last property only.
23. How This Appears In Real Engineering
A verification engineer builds the environment to get a first test running, and one agent is faster than three. Every broken build in this chapter is the shortest path to a working elaboration, which is the right first goal and a bad final state.
A verification lead reviews the test plan and the coverage model, rarely the component tree. Sections 5, 6 and 11 are all visible only in the tree, and all three are settled in the first fortnight of a project.
A designer meets this chapter as section 21 — five clean months and three bugs in a week at integration. The corrective is not more tests; it is four elaboration checks that would each have failed on day one.
Anybody inheriting an environment meets it worst of all, because a structure that cannot see something gives no sign of it — and the twenty lines that would say so were not written by the person who built it.
24. Common Misconceptions
"It elaborates without warnings." UVM's build and connect phases check that the structure is legal, not that it is adequate. All five of section 14's middle rows elaborate silently.
"One agent is simpler." It is, and it cannot name which interface a transaction came from (section 5) — and because it cannot count what it failed to attribute, it reports a hundred percent attributed.
"Make all the agents active; it's more flexible." An active agent on a DUT-driven bus is two drivers on one net (section 6), and it is also two and a half times the components, which section 13 charges for.
"The override is in the test, so it applies." Only if it is registered before the component is constructed. After super.build_phase() it resolves nothing (section 7), silently.
"The test passed and it was fast." A run phase with no objections ends immediately (section 8 row five). A fast test and an empty test look identical in everything except transactions completed.
"The monitor is publishing, so the scoreboard is receiving." write() on a port with no subscriber is a no-op (section 11). Nine hundred transactions a run, observed and discarded, with every component reporting healthy.
25. Interview Reasoning
"How many agents would you build for a CXL device?" One per interface, and the reasoning is what matters: the scoreboard needs to know which interface a transaction arrived on, and a monitor watching three has no source to record. A candidate who says "one, it's simpler" has optimised the build and lost the check.
"Your environment elaborates cleanly. What does that tell you?" That the structure is legal. Not that any agent is on the right bus, that any override applied, that the phase waits for anything, or that any port is connected — section 14's six, and five of them elaborate silently.
"A nightly regression has been green for five months and integration finds three bugs. Where do you look first?" Comparisons against transactions, then subscribers per analysis port. Both are counters that already exist or are a loop away, and both distinguish "no failures" from "no checks."
"Your error-injection test passes every night. How would you confirm it injected anything?" The factory override table at end of elaboration (section 7), and a count of injected errors. A test that runs the normal driver passes for the wrong reason, and nothing in the log distinguishes it.
"When is strict-priority arbitration the right choice?" When the lower-priority stream genuinely should not run while the higher one has work — which is rare, and is a decision rather than a default. Ask back: what is the grant count for the lowest-priority sequence in the last regression?
26. Exercises
1. Add attribution properly. §5 counts. Add a source field to the transaction class and state where the monitor gets it from — then say what changes if two interfaces share a physical bus.
2. Build the mode table. For a CXL type-2 device, list every interface and who drives it, then write the elaboration check that compares it against each agent's is_active.
3. Audit a factory. Take a test with six overrides. Determine for each whether it is registered before the components it targets are constructed, and design the report that would say so automatically.
4. Find the objection gap. For an environment with six producers, write the rule that decides which must object and the end-of-phase check that catches one that did not.
5. Measure a wildcard's reach. Given a component tree and three configuration paths, compute each path's reach against its intended target count. Which of the three would you rewrite?
6. Schedule the arbitration. §10 uses a served-count. Model a time-ordered schedule for two sequences at different priorities with the high-priority one busy 80% of the time, and find the lower sequence's actual grant rate.
7. Write the subscriber loop. Walk a component tree and report every analysis port with zero subscribers. How many lines, and where in the phase order does it belong?
8. Decide on a virtual sequence. From a list of twenty scenarios, classify each as solo or cross-interface, and state the cost of the coordinating layer against the twelve orderings it buys.
9. Model elaboration properly. §13 is linear in components. Add the factory's type-lookup cost and re-derive the per-test elaboration for 300, 1,200 and 4,800 components.
10. Add the seventh property. Propose one none of §14's six implies, name its section, and construct the configuration where the six hold and it fails. A property that cannot fail alone is not a seventh property.
27. Summary
A UVM environment that elaborates and runs is not an environment that saw anything. UVM's phases check that the structure is legal; nothing checks that it is adequate, and there is no phase that could.
One agent cannot name three interfaces. A hundred and fifty of six hundred transactions unattributable — and zero reported, because counting them needs exactly the per-interface context the single agent does not have. The blindness is not measurable from inside it.
An active agent on a DUT-driven bus is two drivers on one net, and it elaborates clean. Three contended buses, and ten driver components where four would do — the wrong configuration is also the more expensive one.
A factory override registered after the build resolves nothing. Four of twelve late is twenty-four instances of the wrong type, and the test's whole intent silently does not happen.
A run phase waits for whoever objected. Six producers with four raising loses forty transactions at a normal end of test — and with nobody raising, the test finishes immediately and looks fast rather than empty.
A wildcard configuration path reaches 225% of what it named. Five components configured that the author never intended — and, in the same model, three settings written and never read, which did nothing at all.
Strict-priority arbitration starves and reports a share. Three of four requesters served nothing while a twenty percent share is reported to one of them; at a hundred percent busy it reports zero, which is at least honest.
An analysis port with no subscriber is a no-op. Three dangling ports of nine is nine hundred transactions a run, observed and discarded, with every component reporting healthy.
Per-agent stimulus cannot express a cross-interface order. Twelve of forty scenarios are not rare or late — they are not expressible, and the twelve orderings need a layer above both sequencers.
And elaboration is thirty-five percent of the regression. Forty-eight seconds paid on two thousand tests is sixteen hundred minutes before a single transaction, on a component count that sections 6 and 11 both inflate.
Two checks ran before the mutation campaign and both earned their place. The mutual-exclusivity check found three dead error signals in the first six models — an apt mistake in a chapter about the difference between what is true and what is reported. The scripted output-listing step found four unasserted headline percentages. Between them, the campaign produced two survivors from eighty mutations, the lowest rate in three batches, and eleven mutation classes targeting clamps were all killed on the first run because batch 024's rule was applied while drafting.
"It elaborates and it runs" is one property of six. The log called six of seven configurations sound when one was — and §21 is an environment five months green with four structural defects, none of which produced a single message, and every one of which would have failed an elaboration check that takes twenty lines to write.
26.1 — Discovery Failures leaves the environment and opens Module 26. Every chapter of Module 25 asked what a green report establishes; Module 26 starts from a failure that is already happening and asks what a trace narrows it to.
Continue learning
Related tutorials
- Related topic
CXL Scoreboards
A scoreboard matches a response to a request by a key, and everything else follows from whether that key is unique. This chapter builds keying, out-of-order matching, end-of-test drain, timeouts, distributed audits, comparison granularity, capacity, ordering rules, cost and the assembled sign-off.
- Related topic
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.
- Related topic
Device-Type Selection Discipline
Selecting a device type is selecting a mode space: a two-engine device is four devices, each with its own obligations. Negotiation that can only reduce, disables that must quiesce, and state that must not outlive its protocol. Seven RTL models, twenty-six mutations, twenty-six killed.
- Related topic
CXL State Management
A coherency state is not a name, it is a tuple of facts, and most of the state space is the transient states nobody draws. The encoding, the legal-edge graph, the machinery that applies a transition atomically, and what happens to a snoop that arrives mid-flight.
Standards & specifications
- Governing standard
- CXL Specification (CXL Consortium)(opens CXL Consortium in a new tab)
Defines CXL.io, CXL.cache and CXL.mem, and the coherence and memory-pooling behaviour built on them. System design and deployment topology are not mandated.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the CXL curriculum.
