CXL · Module 27
Senior Verification Question
An environment is not a plan. This chapter builds the agent inventory, what a scoreboard actually checks, the coverage cross, what random stimulus reaches, checker independence, error injection, performance observability, honest reuse accounting and a written definition of done.
27.8 walked a transaction. This chapter builds the thing that proves a thousand of them — design a verification environment for a CXL device — and it is the question where the vocabulary is so well established that an answer can be fluent and empty.
We have a UVM environment. True, necessary, and the place most answers start and finish: agents named, a scoreboard mentioned, coverage asserted to exist, and nothing said about what any of them actually check.
1. The Engineering Problem — An Environment Is Not A Plan
An environment is agents, and the count is set by the DUT. Five interfaces with three agents built is two interfaces nothing can drive — and an agent that exists and is not connected drives nothing either. Section 5.
A scoreboard that checks the data is not checking the protocol. Six hundred coherent transactions with two hundred checked for coherence is four hundred that were compared byte-for-byte and never examined for a coherency violation. Section 6.
Each axis is not the cross. Eight request types and six line states, both axes fully hit, is forty-eight combinations of which twenty were produced — and the interesting one is a combination, never an axis. Section 7.
A checker built from the design agrees with the design. Three hundred of five hundred checks written by reading the RTL is three hundred checks that cannot fail, on an environment reporting five hundred green. Section 9.
An environment that never injects an error has verified the happy path. Thirty error paths with twelve exercised is eighteen pieces of DUT logic that have never run, and on a coherent interconnect that is where the hard bugs live. Section 10.
This chapter against 27.8, stated precisely. That one owns what one transaction does. This one owns the apparatus that proves a design does it correctly every time — which is a different discipline with its own failure modes, and the failure modes are almost all of the form "the measurement is measuring itself".
2. The One-Sentence Model
A verification environment is a plan when it exists, when it has an agent for every interface the DUT presents, when its checking is derived from something other than the design, when its coverage is crossed rather than per-axis, when its error paths have been exercised, and when what counts as done was written before the work started — and "we have a UVM environment" is one of those six.
3. What This Chapter Owns
| Ground | Owner |
|---|---|
| What one coherent transaction does | 27.8 |
| Finding a bug in silicon | 26.7 |
| Where the time goes under load | 26.6 |
| Proving the design is correct | this chapter |
| Which device is being verified | 27.6 |
Some vocabulary first, because the UVM words are precise and are frequently used as decoration.
An agent is the thing that drives and monitors one interface. Driver, sequencer, monitor, and a configuration that says whether it is active or passive. The count is a property of the DUT: a Type 2 device presents a CXL link, a memory interface, a configuration interface and whatever the accelerator itself exposes, and each of them needs one.
A scoreboard compares what the DUT did against what it should have done. The second half of that sentence is where the work is, and section 6 is about how much of the protocol the comparison actually covers.
Functional coverage is a record of what the stimulus produced, not of what was checked. Those are independent: an environment can produce a state and not check anything about it, and it can check something thoroughly in a state it never reaches.
A reference model predicts the DUT's behaviour from the specification. Section 9 is about what happens when it is written from the RTL instead, which is the single most common way for a large environment to be worth less than its check count suggests.
And closure criteria are the written statement of what done means — section 13, and the one thing on this list that costs nothing to produce and is most often absent.
4. Teaching-Model Boundary
Every model in this chapter is a teaching model, not a verification environment. It computes the one relationship the section is about and nothing else. There is no UVM, no transaction class and no coverage database anywhere in this file — the models are plain synthesisable Verilog because the subject is the arithmetic of a plan, not the syntax of a framework.
Each model is built twice from one source. A parameter selects between the measured build, which counts what the environment actually proves, and the inventory build, which counts what it contains. Every section's headline number is the gap between them.
| The models do | The models do not |
|---|---|
| Compute one property of a verification plan | Implement or simulate UVM |
| Contrast what exists against what it proves | Model a DUT, a bus or a protocol |
| Saturate and bound every count they publish | Predict any real environment's results |
| Count how often each build was wrong | Replace a verification plan |
5. RTL 1 — An Environment Is Agents
Start with the inventory, because it is the part that is countable and the part a plan is most often asked to justify.
The number of agents is a property of the DUT, not of the team. Every interface the device presents needs something that can drive it and something that can watch it, and an interface with neither is a part of the design that no test can reach. That is not a smaller environment; it is an environment with a hole in a specific shape.
There is a second count that the first one hides. An agent that has been written and not connected to the DUT drives nothing, and on a large environment the two numbers drift apart quietly, because the agent compiles either way.
// RTL 1 - an environment is agents, and the count is decided by the DUT's
// interfaces rather than by how many the team had time to build. An
// environment missing an agent is not a smaller environment; it is one that
// cannot drive a whole class of traffic.
module agent_inventory #(parameter int AN_ENV_IS_AN_ENV = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] ifaces_needed, agents_built, hook_ups, hook_ups_needed,
output logic [15:0] built_ok, agents_missing, agent_pct, hooked_ok,
output logic env_complete,
output logic [7:0] n_evals, n_incomplete,
output logic inventory_err
);
logic [31:0] a_q;
logic [15:0] true_missing;
logic truly_incomplete;
// An environment cannot have more useful agents than the DUT has interfaces.
assign built_ok = (agents_built > ifaces_needed) ? ifaces_needed : agents_built;
assign true_missing = ifaces_needed - built_ok;
assign agents_missing = (AN_ENV_IS_AN_ENV != 0) ? 16'd0 : true_missing;
// An agent that exists and is not connected to the DUT drives nothing.
assign hooked_ok = (hook_ups > hook_ups_needed) ? hook_ups_needed : hook_ups;
assign a_q = (ifaces_needed == 16'd0) ? 32'd100
: (({16'd0, built_ok} * 32'd100) / {16'd0, ifaces_needed});
assign agent_pct = a_q[15:0];
assign env_complete = (agents_missing == 16'd0) && (ifaces_needed != 16'd0);
assign truly_incomplete = (true_missing != 16'd0);
assign inventory_err = evaluate && truly_incomplete && env_complete;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_incomplete <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_incomplete) n_incomplete <= n_incomplete + 8'd1;
end
end
endmoduleFive interfaces with three agents built is two interfaces nothing can drive and sixty percent of the DUT reachable — and the an-env-is-an-env view reports an environment.
| Fact | Value |
|---|---|
| Interfaces on the DUT | 5 |
| Agents built | 3 |
| Undriven interfaces | 2 |
| DUT reachable | 60% |
| Connections made | 3 |
| Connections needed | 5 |
Figure 1 — the two counts an inventory collapses into one. The upper path counts components, which is the number in a status report and the number a team is asked for. The lower path counts what the components can reach: two interfaces with nothing to drive them, and two agents that exist without being wired to anything. Both failures compile, both produce a green regression, and neither appears in an agent count.
The sixth case is the one this model is deliberately honest about failing to catch. Five agents built and not one of them connected is reported as a complete inventory, because a complete inventory is what the model measures — and the environment drives nothing at all. That is the limit of counting agents, and it is why the connection count is a separate column rather than folded into the verdict: an inventory tells you what exists, and existence is the weakest of the six conditions in section 14.
The third case is the boundary worth stating. More agents than the DUT has interfaces is clamped to what can be useful, not counted as extra coverage. An agent for an interface the design does not present is a component with no DUT to drive, and reporting it as a hundred and eighty percent reachability would be arithmetic rather than verification.
The fifth case is the shape of a plan at the start of a project. No agents built, five interfaces undriven, and three connections recorded to agents that do not exist — which is what a plan document looks like when the connection list was written from the architecture and the agent list from the schedule.
The degenerate case bounds it: a DUT with no interfaces cannot be under-driven, and the model reports a hundred percent while declining to call it a complete environment.
It is worth listing the interfaces a CXL Type 2 device actually presents, because "an agent per interface" is otherwise an instruction with no denominator. The CXL link itself, carrying three protocols that behave differently enough that some environments give each its own sub-agent. Configuration space, which is how the device is discovered and how most of the early bring-up failures appear. The device's own memory interface, which the host will address through CXL.mem and which needs a backing model with the right timing. The accelerator's compute interfaces, whatever those are, which are outside the protocol and inside the DUT. And the management or sideband interface if the part is in a fabric, because that is how a fabric manager binds it — 27.7 section 9's subject arriving as a verification component.
That is five before anything about the accelerator, and an environment sized from the block diagram usually counts one, because the block diagram shows a link.
The passive-agent case is worth separating too. An agent can be present and configured passive — monitoring without driving — which is correct for an interface the DUT masters and wrong for one it responds to. An inventory counts it either way, and an interface that needs stimulus and has a passive monitor on it is section 5's failure wearing a component's name.
6. RTL 2 — A Scoreboard That Checks Data Is Not Checking The Protocol
The second thing, and the one where an environment can be simultaneously thorough and blind.
Comparing returned data against a reference is the easy half of checking a coherent device. It catches corruption, it catches addressing errors, it catches a great many real bugs, and it is what a scoreboard does by default. It catches none of the failures that make coherency hard, because those failures produce correct data in the wrong state.
A line handed to a requester as exclusive when a peer still holds it shared has the right bytes. A snoop that invalidated instead of downgrading — 27.8 section 6 — returns the right bytes. A directory that recorded the wrong owner returns the right bytes until the next transaction, and then the failure appears somewhere unrelated.
// RTL 2 - a scoreboard that checks the data is not checking the protocol. A
// CXL environment can compare every byte the DUT returns against a reference
// and still miss every coherence violation, because the bytes are right and
// the state the line was left in is not.
module scoreboard_scope #(parameter int DATA_IS_THE_CHECK = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] txns, data_checked, coh_checked, coh_relevant,
output logic [15:0] data_ok, coh_ok, blind_txns, checked_pct,
output logic fully_checked,
output logic [7:0] n_evals, n_blind,
output logic scope_err
);
logic [31:0] c_q;
logic [15:0] rel_ok, true_blind;
logic truly_blind;
assign data_ok = (data_checked > txns) ? txns : data_checked;
// Only the transactions where coherence means something can be checked for
// it, and only as many of those as the scoreboard actually looks at.
assign rel_ok = (coh_relevant > txns) ? txns : coh_relevant;
assign coh_ok = (coh_checked > rel_ok) ? rel_ok : coh_checked;
assign true_blind = rel_ok - coh_ok;
assign blind_txns = (DATA_IS_THE_CHECK != 0) ? 16'd0 : true_blind;
assign c_q = (rel_ok == 16'd0) ? 32'd100
: (({16'd0, coh_ok} * 32'd100) / {16'd0, rel_ok});
assign checked_pct = c_q[15:0];
assign fully_checked = (blind_txns == 16'd0) && (rel_ok != 16'd0);
assign truly_blind = (true_blind != 16'd0);
assign scope_err = evaluate && truly_blind && fully_checked;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_blind <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_blind) n_blind <= n_blind + 8'd1;
end
end
endmoduleA thousand transactions with six hundred where coherence means something and two hundred checked for it is four hundred transactions compared byte-for-byte and never examined for a protocol violation — a third of the coherent traffic checked, and the data-is-the-check view reports a complete scoreboard.
| Fact | Value |
|---|---|
| Transactions | 1,000 |
| Data compared | 1,000 |
| Coherence-relevant | 600 |
| Checked for coherence | 200 |
| Blind | 400 |
| Protocol checked | 33% |
The fifth case is the one that keeps the section from becoming a blanket rule. Traffic where coherence never applies — a Type 3 memory expander, where there is no CXL.cache and no device-held state to get wrong — has nothing for the protocol half of the scoreboard to check, and the model declines to call the environment blind. It also declines to call it complete, which is the honest position: an environment that has never been shown coherent traffic has not been shown to check it, and that is a different sentence from "it does not check it."
The last case is the one that reaches sign-off. A hundred coherence checks against four thousand relevant transactions — two percent — on a run where five thousand transactions came back green. Everything about that run looks successful, and the number that would say otherwise is not on the report unless somebody asks for it separately.
The fourth case bounds the model. A scoreboard configured against no traffic reports every coherent transaction checked, because none ran, and is an idle scoreboard rather than a complete one.
What the coherence half actually has to check is worth naming, because "check the protocol" is not actionable. The state the line was granted in, against the states every other agent was left holding — which requires the scoreboard to track all agents rather than the one under test. The ordering of the responses against what the requester was told. And the directory's view against the agents' actual states, which is the check that catches a class of bug nothing else will. All three need the scoreboard to model the system, not the DUT, and that is why they are the expensive half and the half that gets deferred.
The cost asymmetry is worth stating plainly because it explains the behaviour rather than just criticising it. The data half scales with the DUT and the protocol half scales with the system. A data comparison needs a reference for one device's responses; a coherence check needs a model of every agent's state and of the ordering between them, and its complexity grows as agents are added. On a two-agent testbench the two halves are comparable. On an eight-agent one the protocol half is most of the environment.
That is also why it is usually built last and delivered partially. The data half produces useful failures on day one — wrong bytes are unambiguous — and the protocol half produces nothing until it is nearly complete, because a half-built coherence model reports disagreements that are its own. A team under schedule pressure will rationally ship the half that pays immediately, and the number in the table is what that costs.
The third case in the stimulus is worth a line for a reason unrelated to the clamp. Claims larger than the traffic — two thousand checks recorded against a thousand transactions — is not a contrived input. It is what a scoreboard reports when it counts comparisons rather than transactions, and a check count that exceeds the transaction count is a signal that the denominator is not what somebody thinks it is.
7. RTL 3 — Each Axis Is Not The Cross
The third thing, and the place where a coverage report is most likely to be read as saying something it does not.
A coverage model with two axes has a third thing in it: the combinations. Eight request types and six line states is fourteen values and forty-eight combinations, and the interesting scenarios are all combinations. A write snoop of a line held modified by a peer is one cell. A read of a line held exclusive elsewhere is another. Neither is an axis value, and a report showing both axes at a hundred percent tells you nothing about either.
// RTL 3 - covering each axis is not covering the interesting combinations.
// An environment that has hit every request type and every line state
// separately may never have hit a snoop of a modified line by a writer, which
// is the only combination that matters.
module coverage_shape #(parameter int AXES_ARE_ENOUGH = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] axis_a, axis_b, axis_hits, pairs_hit,
output logic [15:0] pairs_total, hit_ok, pairs_missing, crossed_pct,
output logic cov_complete,
output logic [7:0] n_evals, n_uncrossed,
output logic coverage_err
);
logic [31:0] raw_pairs, p_q;
logic [15:0] true_missing;
logic truly_uncrossed;
assign raw_pairs = {16'd0, axis_a} * {16'd0, axis_b};
assign pairs_total = (raw_pairs > 32'd9999) ? 16'd9999 : raw_pairs[15:0];
assign hit_ok = (pairs_hit > pairs_total) ? pairs_total : pairs_hit;
assign true_missing = pairs_total - hit_ok;
// The each-axis view reports the axis hits as though they were the cross.
assign pairs_missing = (AXES_ARE_ENOUGH != 0) ? 16'd0 : true_missing;
assign p_q = (pairs_total == 16'd0) ? 32'd100
: (({16'd0, hit_ok} * 32'd100) / {16'd0, pairs_total});
assign crossed_pct = (AXES_ARE_ENOUGH != 0) ? axis_hits : p_q[15:0];
assign cov_complete = (pairs_missing == 16'd0) && (pairs_total != 16'd0);
assign truly_uncrossed = (true_missing != 16'd0);
assign coverage_err = evaluate && truly_uncrossed && cov_complete;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_uncrossed <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_uncrossed) n_uncrossed <= n_uncrossed + 8'd1;
end
end
endmoduleEight request types crossed with six line states is forty-eight combinations with twenty produced — forty-one percent of the cross — and the each-axis view reports a hundred percent because both axes are full.
| Fact | Value |
|---|---|
| Request types | 8 |
| Line states | 6 |
| Combinations | 48 |
| Produced | 20 |
| Never produced | 28 |
| Cross covered | 41% |
Figure 2 — the same stimulus, two reports, and the upper one is not a rounding error. It is a hundred percent against forty-one, and everything the verification effort would have done next depends on which number is on the slide. The twenty-eight missing cells are not a random subset either: they are the combinations the stimulus finds hard to produce, which is exactly the correlation that makes them the interesting ones.
The last case is the boundary that keeps the technique honest. A second axis with exactly one value makes the cross the first axis over again — eight combinations, eight hit, a hundred percent — and both views close it. Neither of them learned anything, and a coverage model built that way is a per-axis model wearing a cross's name. Crossing is only worth its cost when both axes have values the other one interacts with, and a single-valued axis is a sign the model was written to close rather than to measure.
The fifth case is the empty one and it is common early. Both axes present and nothing crossed — forty-eight combinations, none produced — while the per-axis view still reports a hundred percent, because the axes were hit by the very first tests.
The third case shows what happens at realistic scale. Two hundred values on each axis saturates the model's count at nine thousand nine hundred and ninety-nine, twenty of them hit, and rounds to zero percent. A cross of any size is mostly empty, which is why real coverage models cross selectively and say which combinations are illegal — and a model that crosses everything and reports a percentage is producing a number nobody can act on.
Which combinations are illegal is the part of this work that looks like bookkeeping and is not. Every excluded cell is a claim about the specification — that this request type can never see that line state, that this agent count cannot occur with that topology — and writing the exclusion list is a specification review carried out one cell at a time. It frequently finds that a combination everybody assumed was illegal is merely unlikely, which is a coverage hole and often a bug.
An unannotated cross is therefore worse than useless rather than merely incomplete. It reports a low percentage that everyone knows to discount, the discount is not quantified, and the number stops steering anything — which is how a project ends up navigating by axis coverage while a crossed model sits in the report unread.
The fourth case bounds the model at the degenerate end. An axis with no values in it produces no combinations, reports every one of them hit, and is declined as coverage — because a cross against an empty axis is an absent cross rather than a closed one.
8. RTL 4 — Random Reaches The Easy States
The fourth thing, and the one where an environment's cost and its results diverge most sharply.
Constrained-random stimulus is excellent at volume and indifferent to difficulty. It reaches the states that are easy to reach — a read, a write, a miss, a hit — in the first thousand items, and then spends the next fifty thousand reaching them again. The states that need three agents to do specific things in a specific order have a probability that falls off a cliff as the number of participants grows, and no amount of runtime fixes that.
The number that says whether random is buying anything is stimulus items per state reached, and it is a number almost nobody computes.
// RTL 4 - random stimulus reaches the easy states. The interesting states of
// a coherent system are the ones that need several agents to do specific
// things at the same time, and the probability of that happening by chance
// falls off a cliff as the number of participants grows.
module stimulus_reach #(parameter int RANDOM_REACHES_IT = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] states_wanted, reached_random, reached_directed, stim_items,
output logic [15:0] reached_ok, unreached, reach_pct, items_per_state,
output logic all_reached,
output logic [7:0] n_evals, n_unreached,
output logic reach_err
);
logic [31:0] r_q, i_q;
logic [15:0] true_reached, true_unreached, rand_ok;
logic truly_unreached;
// Directed stimulus reaches what it was written for; random stimulus
// reaches what it happens to hit, and the two sets overlap.
assign rand_ok = (reached_random > states_wanted) ? states_wanted : reached_random;
assign true_reached = ((rand_ok + reached_directed) > states_wanted)
? states_wanted : (rand_ok + reached_directed);
assign reached_ok = (RANDOM_REACHES_IT != 0) ? states_wanted : true_reached;
assign true_unreached = states_wanted - true_reached;
assign unreached = (RANDOM_REACHES_IT != 0) ? 16'd0 : true_unreached;
assign r_q = (states_wanted == 16'd0) ? 32'd100
: (({16'd0, reached_ok} * 32'd100) / {16'd0, states_wanted});
assign reach_pct = r_q[15:0];
// How much stimulus each reached state cost, which is the number that says
// whether random is buying anything.
assign i_q = (true_reached == 16'd0) ? 32'd9999
: ({16'd0, stim_items} / {16'd0, true_reached});
assign items_per_state = (i_q > 32'd9999) ? 16'd9999 : i_q[15:0];
assign all_reached = (unreached == 16'd0) && (states_wanted != 16'd0);
assign truly_unreached = (true_unreached != 16'd0);
assign reach_err = evaluate && truly_unreached && all_reached;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_unreached <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_unreached) n_unreached <= n_unreached + 8'd1;
end
end
endmoduleForty interesting states with random reaching twenty-five and directed stimulus adding eight is thirty-three reached, seven never produced, and fifteen hundred stimulus items for every state reached — and the random-reaches-it view reports the whole space covered.
| Fact | Value |
|---|---|
| Interesting states | 40 |
| Reached by random | 25 |
| Added by directed | 8 |
| Reached | 33 |
| Never produced | 7 |
| Items per state | 1,515 |
The sixth case is the one that turns the argument into a decision. Vast stimulus reaching one state saturates the cost-per-state figure, thirty-nine states never produced, and the regression is a full-time consumer of compute producing nothing. That configuration is not hypothetical: it is what a badly-constrained random environment does, and the coverage percentage alone will not distinguish it from a well-constrained one that is genuinely nearly finished.
The fifth case is the other end and it is the one that makes directed stimulus look expensive. Directed alone reaches eight states — exactly the eight somebody wrote tests for — at six thousand items each, and reaches nothing by accident. Random finds what nobody thought of; directed reaches what nobody can reach by chance. The two are complements and the model computes the union rather than a preference.
The last case is the honest gap. States reached with no stimulus counted gives a reach figure with no cost attached, which is what a coverage report is by default. The percentage is not wrong; it just cannot tell you whether the next percent costs an hour or a month.
The degenerate case bounds it: a plan with no interesting states named reports full coverage of an empty set, and the model declines to call that coverage.
There is a structural reason the curve flattens where it does, and knowing it changes what you do about it. The probability of a random scenario falls off geometrically in the number of agents that have to cooperate. A state that needs one agent to do one thing appears immediately. A state that needs two agents to touch the same line in a specific order appears occasionally. A state that needs three agents, a specific set of held states and a directory eviction in the same window has a probability small enough that a week of regression is a coin flip — and it is exactly the kind of state where the bugs are.
That is why the answer to a flat curve is not more runtime and not more seeds. It is either a constraint that biases the stimulus towards the hard region — narrowing the address space so collisions become likely, biasing the request mix towards the states in question — or a directed test that constructs the scenario outright. Both are cheap compared to the compute the flat part of that curve consumes.
The third case is worth noting for what it declines to reward. Random claiming more states than exist is clamped to the forty that were named, not counted as extra coverage, because reaching a state nobody put in the plan is not evidence about the plan. It may be evidence that the plan is incomplete, which is a different and more interesting conversation than a coverage number.
9. RTL 5 — A Checker Built From The Design Agrees With It
The fifth thing, and the one that decides whether a check count means anything at all.
A check can only fail if its expectation came from somewhere other than the thing being checked. A reference model written by reading the RTL encodes the RTL's behaviour, including its bugs, and every check derived from it passes by construction. The environment is large, the check count is impressive, and the number of ways it can report a failure is zero.
This is not a hypothetical failure of discipline. It is what happens naturally when the specification is ambiguous, the schedule is short, and the RTL is available — and it is invisible from inside, because the environment does exactly what an environment is supposed to do.
// RTL 5 - a checker built from the DUT's own outputs agrees with the DUT.
// The only checks that can fail are the ones derived from something other
// than the thing being checked, and an environment whose reference model was
// written by reading the RTL has a large number of checks and few witnesses.
module checker_independence #(parameter int A_CHECK_IS_A_CHECK = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] checks, from_dut, from_spec, rule_count,
output logic [15:0] derived_checks, independent_checks, own_rules, indep_pct,
output logic checking_independent,
output logic [7:0] n_evals, n_derived,
output logic independence_err
);
logic [31:0] i_q;
logic [15:0] dut_ok, true_derived, true_indep;
logic truly_derived;
assign dut_ok = (from_dut > checks) ? checks : from_dut;
assign true_derived = dut_ok;
assign true_indep = checks - dut_ok;
assign derived_checks = (A_CHECK_IS_A_CHECK != 0) ? 16'd0 : true_derived;
assign independent_checks = (A_CHECK_IS_A_CHECK != 0) ? checks : true_indep;
// Rules taken from the specification are the ones that can disagree with
// the design; they cannot exceed the number of checks that carry them.
assign own_rules = (rule_count > from_spec) ? from_spec : rule_count;
assign i_q = (checks == 16'd0) ? 32'd100
: (({16'd0, independent_checks} * 32'd100) / {16'd0, checks});
assign indep_pct = i_q[15:0];
assign checking_independent = (derived_checks == 16'd0) && (checks != 16'd0);
assign truly_derived = (true_derived != 16'd0);
assign independence_err = evaluate && truly_derived && checking_independent;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_derived <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_derived) n_derived <= n_derived + 8'd1;
end
end
endmoduleFive hundred checks with three hundred written from the design is two hundred that could disagree with it — forty percent of the checking independent — and the a-check-is-a-check view reports five hundred.
| Fact | Value |
|---|---|
| Checks | 500 |
| Derived from the design | 300 |
| Independent | 200 |
| Rules from the specification | 150 |
| Independent checking | 40% |
| What a check count reports | 500 |
The fifth case is the one to ask about in a review, and it is a different question from the headline. Rules claimed with no specification behind them — two hundred nominally independent checks and not one traceable to a specification clause — is an environment whose independent half was written from somebody's memory of how it ought to work. That is better than reading the RTL and it is not traceability, and the distinction shows up the first time the specification is re-read and found to say something else.
The third case is the limit. Every check derived from the design reports zero independent checking on five hundred checks, and the a-check-is-a-check view reports five hundred. Nothing in that environment can fail, and nothing about running it will reveal that.
The second and sixth cases are what the section is asking for, and both are achievable. Nothing derived from the design, and every check traced to a specification rule — five hundred checks, five hundred rules, each one pointing at a clause somebody can argue about. The argument is the product: a check whose expectation can be disputed is a check that can be wrong, which is the same property as being able to fail.
The last case is the one that scales badly. Four and a half thousand derived checks of five thousand is a tenth of the checking doing any work, on an environment whose size is itself the argument for its quality — and the larger the environment, the more likely this shape is, because the pressure to produce checks quickly grows with the number of them expected.
The distinction has a practical test that costs nothing. For each check, ask where the expected value came from, and whether the answer names a document. "The specification says the response must carry the original tag" is independent. "It matches what the design does" is not. "It matches what the design does, and we checked that against the specification" is independent, and is also the honest description of a great deal of real reference-model work — the reconciliation is legitimate as long as it happened and as long as the specification won.
What makes this insidious is that the derived checks are not useless. They catch regressions: if the RTL changes and the behaviour moves, a derived check fires. They cannot catch the original design being wrong, which is the failure that reaches silicon. So an environment full of derived checks does real work in the second half of a project and does none at all in the first, and the check count is identical in both cases.
The strongest version of independence is a second implementation — a model written by somebody who has not read the RTL, from the specification alone — and it is expensive enough that it is reserved for the parts that matter. Which returns the question to section 12: the parts that matter are the risky ones, and a plan that knows which components those are can afford independence exactly where it pays.
10. RTL 6 — An Environment That Never Injects An Error
The sixth thing, and the one most likely to be deferred to a phase that does not happen.
Every error path in the DUT is code that has not been executed. Malformed packets, CRC failures, timeouts, unexpected responses, protocol violations from a peer — the design has logic for all of them, and a regression that only produces legal traffic runs none of it. On a coherent interconnect this matters more than usual, because the error paths interact with state: a transaction that fails partway leaves agents and directories in configurations the happy path never produces.
And injecting the error is only half. Checking what the DUT did next is the other half, and it is separable: an environment can drive a malformed packet and observe nothing about the recovery.
// RTL 6 - an environment that never injects an error has verified the happy
// path. Every error path in the DUT is code that has not been executed, and
// on a coherent interconnect the error paths are where the hard bugs are,
// because they are the parts nobody exercises in the lab either.
module error_injection #(parameter int ERRORS_ARE_RARE = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] err_paths, err_injected, recovery_checked, runs,
output logic [15:0] injected_ok, uninjected, recovered_ok, injected_pct,
output logic errors_verified,
output logic [7:0] n_evals, n_uninjected,
output logic injection_err
);
logic [31:0] e_q;
logic [15:0] true_uninjected, true_recovered;
logic truly_uninjected;
assign injected_ok = (err_injected > err_paths) ? err_paths : err_injected;
assign true_uninjected = err_paths - injected_ok;
assign uninjected = (ERRORS_ARE_RARE != 0) ? 16'd0 : true_uninjected;
// Injecting an error is not the same as checking what the DUT did next.
assign true_recovered = (recovery_checked > injected_ok) ? injected_ok : recovery_checked;
assign recovered_ok = (ERRORS_ARE_RARE != 0) ? injected_ok : true_recovered;
assign e_q = (err_paths == 16'd0) ? 32'd100
: (({16'd0, injected_ok} * 32'd100) / {16'd0, err_paths});
assign injected_pct = e_q[15:0];
assign errors_verified = (uninjected == 16'd0) && (recovered_ok == injected_ok)
&& (err_paths != 16'd0);
assign truly_uninjected = (true_uninjected != 16'd0) || (true_recovered != injected_ok);
assign injection_err = evaluate && truly_uninjected && errors_verified;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_uninjected <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_uninjected) n_uninjected <= n_uninjected + 8'd1;
end
end
endmoduleThirty error paths with twelve injected is eighteen never exercised — forty percent of the error surface — and the errors-are-rare view reports verified error handling.
| Fact | Value |
|---|---|
| Error paths | 30 |
| Injected | 12 |
| Never exercised | 18 |
| Recovery checked | 12 |
| Error surface | 40% |
| What an inventory reports | verified |
The fifth case is the one that separates the two halves and it is the more common failure. Every error path injected and the recovery checked on ten of them reports a fully exercised error surface and two thirds of it unverified — the environment drove thirty failures, the DUT did something in response to each, and nobody looked at twenty of those responses. Injection is stimulus; recovery is checking, and a plan that counts only the first has counted the cheap half.
The last case is what a real error surface looks like. Two hundred paths with five injected is two percent, and the two hundred is not an exaggeration — a CXL device's error surface includes every malformed field, every illegal state transition, every timeout and every combination of those with an in-flight coherent transaction.
The degenerate case bounds the model: a DUT with no error paths at all reports a hundred percent exercised and is declined as verified error handling, because there is none to verify.
What counts as an error path on a coherent device is worth enumerating, because the denominator is the number nobody has. Link-level errors: CRC failures, replay, retraining, lane degradation. Protocol-level errors: an unexpected response, a response to a transaction that is not outstanding, a malformed field, a reserved encoding. Timeouts on every step that waits for something — 27.8 section 13's list, each entry of which is a path here. Resource exhaustion: a full directory, a full transaction table, a credit starvation. And the combinations, which is where the count explodes: a link retrain while a coherent transaction is outstanding is a different path from either on its own.
That last category is why the error surface is large and why it is where the hard bugs are. Individually, error paths tend to be simple and correct. Their interaction with in-flight state is neither, and it is the part that only an environment with a deliberately-misbehaving agent will ever produce.
The third case bounds the arithmetic honestly: injecting errors the DUT cannot produce is clamped rather than counted, because driving a malformed field that the design's own encoding makes impossible exercises nothing.
11. RTL 7 — Functionally Correct And Unable To Measure
The seventh thing, and the one discovered latest.
Latency and bandwidth are measurements, and an environment made of transaction-level checks has nothing to measure with. A scoreboard that compares data does not need timestamps. A coverage model does not need them. An environment can be complete, correct and green, and be unable to answer "what is the load-to-use latency" because nothing recorded when anything happened.
The question is not asked until somebody needs the number, which is usually late, and by then the observation points are a retrofit across every monitor in the environment.
// RTL 7 - a functionally correct environment can be unable to say anything
// about performance. Latency and bandwidth are measurements, and an
// environment built entirely of transaction-level checks has no timestamps to
// measure with - which is discovered late, when the numbers are asked for.
module perf_observability #(parameter int FUNCTIONAL_IS_ENOUGH = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] perf_questions, measurable, timestamped_points, points_needed,
output logic [15:0] answerable, perf_gap, points_ok, answerable_pct,
output logic perf_covered,
output logic [7:0] n_evals, n_blind,
output logic perf_err
);
logic [31:0] a_q;
logic [15:0] true_answerable, true_gap;
logic truly_blind;
assign points_ok = (timestamped_points > points_needed) ? points_needed : timestamped_points;
// A question is answerable only if the environment can measure it and has
// the observation points the measurement needs.
assign true_answerable = (measurable > perf_questions) ? perf_questions
: ((points_ok < points_needed) ? 16'd0 : measurable);
assign answerable = (FUNCTIONAL_IS_ENOUGH != 0) ? perf_questions : true_answerable;
assign true_gap = perf_questions - true_answerable;
assign perf_gap = (FUNCTIONAL_IS_ENOUGH != 0) ? 16'd0 : true_gap;
assign a_q = (perf_questions == 16'd0) ? 32'd100
: (({16'd0, answerable} * 32'd100) / {16'd0, perf_questions});
assign answerable_pct = a_q[15:0];
assign perf_covered = (perf_gap == 16'd0) && (perf_questions != 16'd0);
assign truly_blind = (true_gap != 16'd0);
assign perf_err = evaluate && truly_blind && perf_covered;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_blind <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_blind) n_blind <= n_blind + 8'd1;
end
end
endmoduleTwelve performance questions with three observation points of the eight needed is not one question answerable — and the functional-is-enough view reports them all answered.
| Fact | Value |
|---|---|
| Performance questions | 12 |
| Measurable in principle | 12 |
| Observation points needed | 8 |
| Instrumented | 3 |
| Answerable | 0 |
| Performance covered | 0% |
The step in that table worth noticing is that three of eight points gives zero of twelve answers, not five. A latency is a difference between two timestamps, and a chain of measurements missing one of its points yields nothing rather than a partial figure — which is why the model's answerable count collapses rather than degrading. Partial instrumentation of a measurement is no instrumentation.
The fifth case separates the two reasons a question goes unanswered, and they have different fixes. Instrumented and still not measurable — eight points in place, five of twelve questions answerable — means the remaining seven need something the environment does not model at all, not another timestamp. Adding observation points will not help; the environment has to be extended.
The sixth case is the honest one. Questions that need no observation points are answerable from counters alone — a transaction count, a retry count, an occupancy — and both views agree. Those exist, they are the cheapest performance numbers to get, and an environment that has them is not blind, it is partially sighted.
The degenerate case bounds it: an environment asked no performance questions reports full coverage of an empty set, which is not coverage but an unasked question.
The questions worth writing down at the start are short and specific, and writing them is what determines which observation points exist. What is the load-to-use latency for a hit in device memory, and for a miss that a peer supplies? What is the sustained bandwidth at each request mix? What is the occupancy of the transaction table under load, and where does it saturate? How long is the tail, and what is in it? Each of those names a pair of points in the traffic, and each pair costs a timestamp in a monitor that is being written anyway.
The retrofit is expensive for a reason that is not obvious: adding a timestamp is trivial, but correlating two timestamps requires a transaction identity that survives from one monitor to another, and that is a structural property of how the environment models transactions. An environment that did not plan for it often cannot match a request at one interface to its response at another without inference — which is the actual cost, and it is a redesign rather than an addition.
The third case is the clamp and it says something mild and real. More points instrumented than the measurement needs is waste rather than coverage, and the model declines to reward it — an environment with timestamps everywhere and no questions written down has spent the effort and still cannot answer anything.
12. RTL 8 — A PCIe Environment Is Most Of A CXL One
The eighth thing, and the one where an accurate number supports a wrong conclusion.
Reuse is real. A CXL device runs on PCIe electricals, uses PCIe enumeration, presents configuration space, and a PCIe verification environment covers a great deal of that. The reuse percentage is genuinely high and it is genuinely worth having.
The part that does not reuse is the part the device was built for. CXL.cache, CXL.mem, the flit layer, the coherency behaviour, the bias transitions — none of that exists in a PCIe environment, and all of it is where the risk is. A reuse figure computed across all components is an average over a population whose members carry wildly different amounts of risk.
// RTL 8 - a PCIe environment is most of a CXL environment and the part it is
// missing is the part the DUT is being built for. Reuse is real and it is
// worth counting honestly, because the saving is on the components that were
// never the risk.
module reuse_accounting #(parameter int REUSE_IS_REUSE = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] components, reusable, risky, risky_reusable,
output logic [15:0] reused_ok, fresh_work, risk_covered, reuse_pct,
output logic risk_reused,
output logic [7:0] n_evals, n_misleading,
output logic reuse_err
);
logic [31:0] r_q;
logic [15:0] risky_ok, true_risk_covered, true_fresh;
logic truly_misleading;
assign reused_ok = (reusable > components) ? components : reusable;
assign true_fresh = components - reused_ok;
assign fresh_work = true_fresh;
// The components that carry the risk are the ones worth asking about, and
// reuse on those is a different number from reuse overall.
assign risky_ok = (risky > components) ? components : risky;
assign true_risk_covered = (risky_reusable > risky_ok) ? risky_ok : risky_reusable;
assign risk_covered = (REUSE_IS_REUSE != 0) ? risky_ok : true_risk_covered;
assign r_q = (components == 16'd0) ? 32'd0
: (({16'd0, reused_ok} * 32'd100) / {16'd0, components});
assign reuse_pct = r_q[15:0];
assign risk_reused = (risk_covered == risky_ok) && (risky_ok != 16'd0);
assign truly_misleading = (true_risk_covered != risky_ok) && (risky_ok != 16'd0);
assign reuse_err = evaluate && truly_misleading && risk_reused;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_misleading <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_misleading) n_misleading <= n_misleading + 8'd1;
end
end
endmoduleForty components with thirty reusable and twelve carrying the risk, of which two are covered by reuse, is seventy-five percent reuse overall and two of twelve where it matters — and the reuse-is-reuse view reports all twelve covered.
| Fact | Value |
|---|---|
| Components | 40 |
| Reusable | 30 |
| Written fresh | 10 |
| Risky components | 12 |
| Risky and reusable | 2 |
| Reuse overall | 75% |
The last case is the shape at scale and it is the one that appears in a plan review. Ninety percent reuse with five of fifty risky components covered is an excellent-looking figure attached to a plan that has reused the parts nobody was worried about. The number is correct. The conclusion drawn from it — that the environment is nearly done — does not follow.
The fifth case is the honest one and it is worth stating so the section is not read as an argument against reuse. A DUT where nothing is risky — a device that genuinely is a PCIe variant — makes reuse simply reuse, and the model declines to draw any conclusion about risk, because there is none. That case exists and reuse is unambiguously good there.
The sixth case is the other end: nothing reusable at all, forty components written fresh, and the risk entirely new work — which is honest, expensive, and at least correctly costed.
The degenerate case bounds the model: an environment with no components has no reuse story and is an unwritten plan rather than a reuse strategy.
It is worth being specific about which components are which, because the argument only works with a list. Reusable, and genuinely so: the PCIe physical and link-layer agents, configuration-space stimulus and checking, enumeration sequences, the register model, the test harness and build flow, the reporting. That is most of the component count and a real saving.
Not reusable, and carrying the risk: the flit-layer agent, CXL.cache and CXL.mem drivers and monitors, the coherence model in the scoreboard, the bias-transition stimulus, the multi-agent scenario library, the error-injection agent, the coverage model's protocol axes. Every one of those is new work and every one of them is where a bug reaching silicon would come from.
The two lists have very different lengths and very similar importance, which is the whole of the section. A plan that reports one number over both has averaged a large, low-risk population with a small, high-risk one, and the average is dominated by the half that was never in doubt.
13. RTL 9 — Done Has To Be Written Down First
The ninth thing, and the cheapest of the nine to get right.
Closure criteria decided at the end are decided against whatever was achieved. That is not dishonesty; it is what happens when nobody wrote the criteria down at the start, because the only information available at the end is what the environment did. The result is a definition of done that is a description of the schedule.
Criteria written first are a commitment, and the useful property of a commitment is that it can be missed. An environment that is at sixty-two percent of a plan written in month one knows something; an environment that closed on a green regression knows only that the tests it has pass.
// RTL 9 - an environment needs a definition of done written before it is
// built. Closure criteria decided at the end are decided against whatever was
// achieved, which makes them a description of the schedule rather than of the
// verification.
module closure_criteria #(parameter int WE_RAN_THE_TESTS = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] criteria_named, criteria_met, tests_run, tests_passing,
output logic [15:0] named_ok, met_ok, open_items, closure_pct,
output logic closed,
output logic [7:0] n_evals, n_open,
output logic closure_err
);
logic [31:0] c_q, t_q;
logic [15:0] pass_pct, crit_pct;
logic [15:0] true_open;
logic truly_open;
assign named_ok = criteria_named;
assign met_ok = (criteria_met > criteria_named) ? criteria_named : criteria_met;
assign true_open = criteria_named - met_ok;
assign open_items = (WE_RAN_THE_TESTS != 0) ? 16'd0 : true_open;
assign c_q = (criteria_named == 16'd0) ? 32'd0
: (({16'd0, met_ok} * 32'd100) / {16'd0, criteria_named});
// The tests-were-run view measures closure by the pass rate instead.
assign t_q = (tests_run == 16'd0) ? 32'd0
: (({16'd0, tests_passing} * 32'd100) / {16'd0, tests_run});
assign pass_pct = (t_q > 32'd100) ? 16'd100 : t_q[15:0];
assign crit_pct = c_q[15:0];
assign closure_pct = (WE_RAN_THE_TESTS != 0) ? pass_pct : crit_pct;
assign closed = (WE_RAN_THE_TESTS != 0)
? ((pass_pct == 16'd100) && (tests_run != 16'd0))
: ((open_items == 16'd0) && (criteria_named != 16'd0));
assign truly_open = (true_open != 16'd0) || (criteria_named == 16'd0);
assign closure_err = evaluate && truly_open && closed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_open <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_open) n_open <= n_open + 8'd1;
end
end
endmoduleEight closure criteria with five met, on a run where every test passes, is three criteria still open and sixty-two percent of the plan — and the we-ran-the-tests view reports a hundred percent and closes.
| Fact | Value |
|---|---|
| Criteria named | 8 |
| Met | 5 |
| Open | 3 |
| Plan met | 62% |
| Tests passing | 400 of 400 |
| What a pass rate reports | closed |
The fourth case is the sharpest and it is why the criteria count matters as much as the met count. No criteria named at all, with a fully green run, is a plan that was never written — and the we-ran-the-tests view closes it on the pass rate alone. The measured build reports zero, declines to close, and flags it: an unwritten plan is not a met plan, and a pass rate cannot distinguish the two because it never looked at a plan.
The last case is the one that shows the two views are measuring genuinely different things rather than one being a degraded version of the other. Every criterion met and a quarter of the tests failing closes the criteria and not the regression. That is a real and instructive state: it means a criterion was mis-stated, or a failing test is testing something nobody agreed was required. Either way the disagreement is information, and an environment with only one of the two numbers cannot see it.
The fifth case is the mirror. Criteria open and no tests run yet leaves both views unable to close, for different reasons, which is the one configuration where they agree by accident.
The third case drives both clamps at once: more criteria met than were named, clamped to the eight that exist, and more tests passing than ran, with the pass rate saturating at a hundred rather than exceeding it.
What belongs in the criteria is worth stating, because "write down what done means" is otherwise as unhelpful as "characterise the workload". Crossed coverage targets with the illegal cells already excluded — section 7. A stated independent-check count, or at minimum a stated traceability requirement — section 9. An error-path inventory with an injection and recovery-check target — section 10. The performance questions and their observation points — section 11. A named list of scenarios that must run, from the specification rather than from the test list. And a bug-discovery-rate condition, because a project still finding bugs weekly is not close to done whatever else it has met.
Six criteria, all of which can be written in an afternoon in month one, and each of which becomes impossible to write honestly once the results are known. That is the entire argument for doing it first: not that the criteria will be better, but that they will be criteria rather than observations.
14. RTL 10 — A CXL Verification Environment Assembled
Nine sections of inputs. This one puts them in one place and makes the confident answer visible as what it is: one bit of six.
// RTL 10 - a CXL verification environment assembled. Nine sections of inputs,
// one summary. "We have a UVM environment" is bit 0: true, necessary, and one
// sixth of what makes an environment a verification plan.
module env_signoff #(parameter int AN_ENV_IS_A_PLAN = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic env_exists, agents_complete, checking_independent,
input logic cov_crossed, errors_injected, closure_defined,
output logic [5:0] fail_mask,
output logic [15:0] conditions_met, sound_pct,
output logic sound,
output logic [7:0] n_evals, n_sound, n_claimed,
output logic signoff_err
);
logic [31:0] s_q;
logic truly_sound, claimed;
assign fail_mask[0] = ~env_exists;
assign fail_mask[1] = ~agents_complete;
assign fail_mask[2] = ~checking_independent;
assign fail_mask[3] = ~cov_crossed;
assign fail_mask[4] = ~errors_injected;
assign fail_mask[5] = ~closure_defined;
assign conditions_met = {15'd0, env_exists} + {15'd0, agents_complete}
+ {15'd0, checking_independent} + {15'd0, cov_crossed}
+ {15'd0, errors_injected} + {15'd0, closure_defined};
assign s_q = ({16'd0, conditions_met} * 32'd100) / 32'd6;
// No clamp: conditions_met sums six one-bit values, so the quotient cannot
// exceed a hundred and a ceiling would be unreachable code.
assign sound_pct = s_q[15:0];
assign truly_sound = (fail_mask == 6'd0);
// The environment-exists view reads bit 0 and stops.
assign claimed = (AN_ENV_IS_A_PLAN != 0) ? env_exists : truly_sound;
assign sound = claimed;
assign signoff_err = evaluate && !truly_sound && claimed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_sound <= 8'd0; n_claimed <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_sound) n_sound <= n_sound + 8'd1;
if (claimed) n_claimed <= n_claimed + 8'd1;
end
end
endmoduleThe stimulus walks all six bits one at a time. When an environment exists and any one of the other five fails, the assembled model reports that the plan is not sound and the env-exists view reports a plan.
| Bit | Condition, and the section that builds it |
|---|---|
| 0 | An environment exists at all — §14 |
| 1 | There is an agent for every interface — §5 |
| 2 | The checking is independent of the design — §9 |
| 3 | The coverage is crossed, not per-axis — §7 |
| 4 | The error paths have been exercised — §10 |
| 5 | Done was written down first — §13 |
Across the eight evaluations, the assembled model calls one plan sound and the env-exists view calls six of them a plan.
The bit order is by how much of the plan's value each condition carries. Bit 0 is the container. Bit 1 is whether the DUT can be reached at all. Bit 2 is whether anything can fail, which is the single most valuable property on the list. Bits 3 and 4 are the two measurements that decide whether the stimulus went anywhere. Bit 5 is last because it is a statement about the other five rather than about the environment.
"We have a UVM environment" is bit 0, and it is true. That is what makes it the right weak definition for this chapter: it names a real and necessary thing, in the vocabulary of the field, and it says nothing about whether the environment can reach the DUT, whether any check in it can fail, whether the stimulus produced the interesting combinations, whether the error logic has ever run, or what would count as finished.
The five other bits fail independently, which is what makes the mask a summary rather than a score. A complete agent inventory can sit under checking that cannot fail. Independent checking can sit under coverage that was never crossed. A crossed coverage model can be closed without a single error injected. And all five can be true in an environment that will declare itself done when the schedule ends. None implies any other, and a review that checks one and infers the rest is checking one.
Figure 4 — the mask ordered by how much of the plan's value each condition carries, which is close to the reverse of how visible each one is. The agent count and the coverage percentage are reported automatically by the tooling. Whether a check can fail is reported by nothing, takes a review to establish, and is the most valuable bit on the diagram — which is the whole reason it sits in the middle of a flow that a status report walks past.
15. Quantitative Reasoning
Two interfaces nothing can drive, of five, and sixty percent of the DUT reachable — with the connection count a separate number that an inventory does not see.
Four hundred coherent transactions compared byte-for-byte and never checked for a protocol violation, of six hundred. A third of the coherent traffic checked, on a run that came back green.
Twenty-eight combinations never produced, of forty-eight, on a coverage model whose two axes are both at a hundred percent.
Fifteen hundred stimulus items per state reached, and thirty-nine states never produced when the cost figure saturates. The percentage keeps climbing; the cost says when to stop.
Three hundred checks that cannot fail, of five hundred, on an environment reporting five hundred green — and forty percent independent checking.
Eighteen error paths never exercised, of thirty, plus twenty injected errors whose recovery nobody checked.
Zero performance questions answerable from three of eight observation points — not five of twelve, because a measurement missing a point yields nothing.
Two of twelve risky components covered by reuse, under a seventy-five percent overall reuse figure that is entirely accurate.
Three closure criteria open on a fully green regression, and a plan with no criteria at all closed by a pass rate.
One plan of eight sound; the env-exists view counts six. The assembled model's summary, and the chapter's.
16. Assertions
The testbenches carry 565 checks across ten models.
Every output of every model is asserted as a value, in both builds. The output listing step reported nothing on either testbench — the second chapter running where it found no gap, because the outputs were assembled from the model's port list while the stimulus was being written rather than afterwards.
Both builds are asserted on every degenerate case. A DUT with no interfaces, a scoreboard with no traffic, traffic where coherence never applies, a coverage axis with no values, a plan with no interesting states, an environment with no checks, a DUT with no error paths, an environment asked no performance questions, a plan with no components, and a plan with no criteria.
Every clamp that an input can reach is driven past its limit exactly once. More agents than interfaces, more connections than the DUT needs, checks claimed on more transactions than ran, more combinations hit than exist, more states reached than were named, more derived checks than checks, more rules than specification clauses, more injections than error paths, more observation points than the measurement needs, more reuse than components, more criteria met than named, and more tests passing than ran.
Every percentage whose numerator is clamped is asserted in the case that over-claims. That sentence is new to this chapter and section 17 explains why: three separate models across two chapters had a mutation survive because the over-claim case asserted every output except the percentage.
Every error output is checked in both directions in every case. Section 5's second, third and sixth cases, section 6's second and fifth, section 7's second and last, section 8's second, section 9's second and sixth, section 10's second and third, section 11's second and sixth, section 12's second and fifth, and section 13's second and last exist to assert the quiet half. Each is a case where the inventory view is right, and a model that alarmed on them would be unusable.
17. Mutation Testing
117 mutations, 117 killed. Fifty-seven against the first testbench, sixty against the second. The first testbench was clean on its first run; the second left one.
| Mutation family | Count, and what it breaks |
|---|---|
| Clamp inverted or removed | 23 — a bounded count reports the raw value, or wraps |
| Parameter-selected branches swapped | 24 — each build computes the other one's answer |
| Guard or zero-case result flipped | 12 — a degenerate input reports a confident answer |
| Boundary loosened or tightened | 4 — an equality lands on the wrong side |
| Conjunction turned into a disjunction | 11 — a two-part condition becomes a one-part one |
| Arithmetic reversed or wrong operator | 17 — a difference underflows, a product becomes a sum |
| Mask bit inverted | 6 — one condition reports the opposite of itself |
| Counter inverted or double-stepped | 20 — a decision is corrupted with no output changing |
The single survivor was the third instance of one defect in two chapters, and that is the result worth recording. A percentage whose numerator is a clamped value — built_ok rather than agents_built, injected_ok rather than err_injected — can have its numerator swapped back to the raw claim, and the mutation survives unless the over-claim case asserts the percentage. In the over-claim case every other output is unchanged: the clamp does its job, the counts are right, the verdict is right, and only the percentage moves. It is the easiest assertion in the whole stimulus to leave out.
It appeared twice in 27.8 and once here. The rule extracted is: whenever a percentage's numerator is a clamped value, the over-claim case must assert that percentage — and applying it while writing this chapter's first testbench is why that testbench was clean on its first run and why only the one model that predates the rule survived.
No dominated guards, and no equivalent clamp mutations. Both classes were discovered by survivors in earlier batches and both were designed out here rather than found: every truly_X in this chapter is a single term with no dead conjunct behind it, and every clamp mutation removes the clamp rather than raising its ceiling. Raising a ceiling past a value the input already exceeds leaves the clamp firing identically, which made two of 27.7's mutations equivalent; removing it and letting the value wrap tests what the clamp is for.
18. Verification Strategy
Count the DUT's interfaces, then the agents, then the connections. Section 5. Three numbers, and the third is the one that drifts.
Separate what the scoreboard checks about the data from what it checks about the protocol. Section 6. The second needs a model of the system rather than of the DUT.
Report the cross, not the axes. Section 7. And cross selectively, marking the illegal combinations, or the percentage is unactionable.
Compute stimulus items per state reached. Section 8. That is the number that says when directed stimulus starts paying, and the coverage percentage is not.
Trace every check to a specification clause. Section 9. A check whose expectation can be argued about is a check that can fail.
Count the error paths, inject them, and check the recovery separately. Section 10. The second count is the one that gets skipped.
Ask the performance questions at the start. Section 11. Observation points are cheap to add while the monitors are being written and expensive afterwards.
Compute reuse over the risky components, not over all of them. Section 12.
Write down what done means before starting. Section 13. It costs an afternoon and it is the only one of the nine that is free.
19. Synthesis and Implementation Reality
A CXL environment needs more agents than the link count suggests. The link is one interface; configuration space is another; the device's memory interface is a third; and on a Type 2 the accelerator's own interfaces are more. An environment sized from the block diagram is usually sized from the link alone.
Checking coherence requires modelling every agent, not just the DUT. That is the structural reason the protocol half of the scoreboard costs more than the data half — it is a system model, and its complexity grows with the number of agents rather than with the DUT.
Illegal-combination annotation is most of the work in a cross. A full cross of any realistic model is mostly cells that cannot legally occur, and the coverage percentage is meaningless until they are excluded — which is manual, specification-driven work that looks like bookkeeping and is actually specification review.
Error injection on a coherent interface needs an agent that can misbehave deliberately, which is a different component from one that drives legal traffic and is frequently not in the plan at all.
And timestamping every monitor is nearly free at write time. The cost is entirely in the retrofit, which is why section 11's failure is a planning failure rather than an engineering one.
20. Silicon Observability
Free, and from the plan. Interface count, agent count, connection count. Section 5.
Free, from the environment itself. Check counts, coverage percentages, test pass rates. These are the numbers a regression reports by default, and each of them is the weaker half of a pair this chapter builds.
Cheap, and almost never computed. Stimulus items per state reached. Section 8 — both numbers are already recorded and nobody divides them.
Moderate. The split of checks into derived and independent. Section 9 requires somebody to go through the reference model and say where each expectation came from, which is a review rather than a measurement.
Moderate. The error-path inventory. Section 10's denominator requires reading the design for its error handling, which is the same work as reviewing it.
Expensive, and the reason section 11 fails late. A latency measurement needs observation points designed in. Retrofitting them across an environment is a change to every monitor.
Unobtainable from inside the environment. Whether a check can fail. Section 9's number cannot be derived from any artefact the environment produces, which is why it takes a review and why it is skipped.
21. Debug Lab
A regression is green, coverage is closed, and silicon came back with a coherency bug.
Step 1 — check whether the failing scenario was in the cross. Section 7. If the coverage model was per-axis, the combination was very likely never produced, and this is over quickly.
Step 2 — check whether the check that should have caught it was independent. Section 9. A check derived from the RTL would have agreed with the bug.
Step 3 — check whether the scenario needs an agent the environment does not have. Section 5. A bug on an interface nothing drives cannot be found.
Step 4 — check whether it is on an error path. Section 10. If the trigger is a recovery sequence, the path may never have executed.
Step 5 — check whether the scoreboard looks at coherence at all for that transaction type. Section 6. Right data, wrong state, green run.
Step 6 — check the stimulus cost curve around that state. Section 8. If the state is reachable only by chance and the cost curve had flattened, the regression was never going to produce it.
The first five are reads from the environment's own artefacts, which makes this cheap. The answer is usually one of them, and the value of asking in this order is that each step is a different fix.
22. Design Review
How many interfaces does the DUT present, how many agents exist, and how many are connected?
What does the scoreboard check about the protocol, as distinct from the data?
Is the coverage report per-axis or crossed, and which combinations are marked illegal?
How many stimulus items per newly reached state, and where did that curve flatten?
How many checks are derived from the RTL, and can each independent check be traced to a specification clause?
How many error paths does the DUT have, how many have been injected, and on how many was the recovery checked?
Which performance questions will be asked at the end, and are the observation points for them in the monitors now?
What is the reuse percentage over the risky components specifically?
What was written down as the definition of done, and when?
23. How This Appears In Real Engineering
The environment is large, well-built, on schedule, and proves less than its numbers suggest.
The most common shape is section 9, and it is a schedule story rather than a competence one. The specification was ambiguous on a handful of behaviours and the RTL was available, so the reference model was reconciled against the design for those cases and then, gradually, for others. Nobody decided to do it. The check count kept rising and the number of checks that could fail stopped rising at some point nobody recorded.
The second shape is section 7. The coverage report is per-axis because the tool produces per-axis by default, the crosses were added late, and the illegal-combination annotation was never finished — so the cross shows forty percent, everybody agrees the number is misleading because of the illegal cells, and the number is quietly set aside rather than fixed. The result is a project steering on axis coverage.
The third is section 10 and it is a phase-ordering story. Error injection was scheduled after functional closure, functional closure slipped, and the error phase was compressed into whatever remained. The error paths that were exercised are the ones that were easy to drive, which correlates with the ones that were easy to design.
The fourth is section 11 and it is the one that produces the worst week. Performance numbers are requested for a design review, the environment cannot produce them, and a retrofit across every monitor lands in the same fortnight as the review.
The pattern is that each of these is a rational local decision whose cost is invisible until the artefact it damaged is needed, and every one of them is caught by a number that could have been computed at any point and was not.
24. Common Misconceptions
"We have a UVM environment." One condition of six. Section 14.
"There is an agent for every interface." Connected to the DUT? Section 5.
"The scoreboard checks every transaction." The data, or the protocol? Section 6.
"Coverage is at a hundred percent." Per axis, or crossed? Section 7.
"Random will reach it eventually." Compute the items per state and the cost curve. Section 8.
"We have five thousand checks." How many can fail? Section 9.
"Error injection is in the next phase." Section 10, and the next phase is where it stays.
"We will get the performance numbers at the end." From which observation points? Section 11.
"We are reusing ninety percent of the PCIe environment." Over the risky components? Section 12.
"The regression is green, so we are done." Against which written criteria? Section 13.
25. Interview Reasoning
"Design a verification environment for a CXL Type 2 device." Start from the DUT's interfaces and name an agent for each — the link, configuration space, the device memory interface, the accelerator's own. Then the checking: a scoreboard with a data half and a protocol half, where the protocol half models every agent's state rather than just the DUT's, and a reference model written from the specification with each rule traceable to a clause. Then coverage, crossed and with the illegal combinations annotated. Then error injection, with an agent that can misbehave deliberately. Then the observation points for whatever performance questions will be asked. And a written definition of done, before any of it starts.
"How do you know the scoreboard is checking coherence?" Because the protocol half compares the state every agent was left holding against what the transaction should have produced, which needs a system model. A scoreboard that only compares returned data will pass a line granted exclusive while a peer still holds it.
"Your coverage is at a hundred percent. What does that mean?" It depends whether that is per-axis or crossed. Per-axis at a hundred percent is consistent with never having produced the interesting combination, because the interesting scenarios are cells rather than values.
"How do you decide when to stop running random and start writing directed tests?" Stimulus items per newly reached state. When that curve flattens, more runtime buys nothing and the remaining states need to be constructed. The coverage percentage is still climbing at that point, which is why it is the wrong signal.
"Your reference model — where did the expected values come from?" That is the question I would want asked of mine. If any of it came from reading the RTL, those checks cannot fail, and the useful count is the independent one.
"How many error paths does the DUT have?" If I cannot answer that, I do not have an error-injection plan — I have some error tests. And injecting is separate from checking the recovery; a plan that counts only injections has counted the cheap half.
"You reused ninety percent of the PCIe environment." Over which components? The reusable ones are the ones that were not the risk. The useful figure is reuse over the components that carry the new behaviour, and it is usually much lower.
26. Exercises
1. A DUT presents 7 interfaces. The plan has 5 agents and 4 connections. Compute the reachable fraction. Which of the three numbers would you ask about first, and why?
2. 4,000 transactions, 2,500 coherence-relevant, 900 checked for coherence. Compute the blind count and the protocol-checked fraction. What does the figure become if the DUT is a Type 3?
3. A coverage model has 12 request types, 5 line states and 3 agent counts. How many three-way combinations? If 400 are hit and 900 are illegal, what is the honest percentage?
4. Random reaches 30 of 55 states in 80,000 items; directed adds 9 in 2,000 more. Compute items per state for each, and say where the crossover is.
5. 3,000 checks, 1,800 from the RTL, 900 traceable to specification clauses. Compute the independent count and the traceable fraction of it. Which number would you put on a sign-off slide?
6. A DUT has 85 error paths; 40 are injected and 25 have their recovery checked. Compute both fractions. Which is the one that a plan usually reports?
7. 20 performance questions, 15 measurable in principle, 6 of 14 observation points instrumented. How many are answerable? What does instrumenting 13 of 14 give?
8. 120 components, 95 reusable, 35 risky of which 6 are reusable. Compute overall reuse and risk reuse. What does the plan look like if the 6 are excluded?
9. Extend the assembled model with a seventh bit for a condition this chapter does not cover. Justify its position using the rule that the ordering is by how much of the plan's value each condition carries.
27. Summary
An environment is agents, and the count comes from the DUT — with the connection count a separate number that an inventory does not see.
A scoreboard that checks data is not checking the protocol, and coherency failures return the right bytes in the wrong state.
Each axis is not the cross. Both axes at a hundred percent is consistent with forty-one percent of the combinations and none of the interesting ones.
Random reaches the easy states, and the number that says when to stop is items per state reached, not the coverage percentage.
A checker built from the design agrees with the design, and a check that cannot fail is not a check however it is counted.
An environment that never injects an error has verified the happy path, and injecting is only half — the recovery has to be checked separately.
A functionally correct environment can be unable to measure anything, and observation points are cheap to add early and expensive to retrofit.
A PCIe environment is most of a CXL one, and the part that does not reuse is the part the device was built for.
Done has to be written down first, or it is a description of the schedule.
Six bits, and "we have a UVM environment" is one of them. One plan of eight is sound; the env-exists view counts six.
Continue learning
Related tutorials
- Related topic
Senior Verification
Designing a UVM environment for a UCIe subsystem — why an environment only checks anything when the expected value comes from somewhere other than the thing being checked, how a mirrored predictor passes every test while verifying nothing, and the scoreboard structure that separates orphan, stale-generation, reallocation and mismatch into four distinct failures.
- 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
Verification Review Checklist
The DV signoff gate — why coverage of contracts beats coverage of code, how to audit a predictor for independence rather than trust it, the monitor and scoreboard defects that make a testbench agree with the design's bugs, and why twenty thousand passing tests at full code coverage can still be a FAIL.
- Related topic
Verification Checklist — Can This Environment Fail?
A predictor fed from the DUT's own output agrees with itself forever, and ten thousand passing tests prove nothing. The gate is whether every contract has independent stimulus, an independent checker, and a demonstrated failure signature.
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.
