CXL · Module 25
CXL Functional Coverage
Coverage is a percentage of a denominator somebody chose. This chapter builds bin partitioning, crosses, exclusions, weighting, hit thresholds, model completeness, the closure curve, sampling points, cost and the assembled sign-off.
Every chapter of this module has leaned on a number it never defined. 25.2 reported 100% on a quarter of the real space. 25.3 reported ten thousand passes of which two hundred meant anything. 25.4 reported a match rate whose denominator nobody printed. Coverage is supposed to be the measurement that says when any of it is enough.
It is a percentage, and a percentage has a denominator somebody chose. Four hundred specification features with three hundred bins written for them reports a hundred percent at a true seventy-five, and the missing quarter is not in the report because it is not in the model.
1. The Engineering Problem — The Denominator Is A Choice
Automatic bins fold distinct values together. Two hundred and fifty-six meaningful values in sixty-four buckets is 192 values that share a bin with something else, each reported covered when any one of them is hit. Section 5.
Two variables covered separately are not the pairs they take together. Eight request types and four response codes is thirty-two pairs against twelve bins — and the separate model closes at a hundred percent with twenty pairs never seen. Section 6.
An exclusion made to reach closure is a hole with a name. Forty bins excluded against twelve genuinely unreachable is twenty-eight reachable bins removed from the denominator. Section 7.
A bin hit once has been reached, not exercised. Nine hundred bins reached and six hundred exercised is ninety percent or sixty, depending on which number the report prints. Section 9.
And the last twenty points cost more than the first eighty. Ten thousand runs to eighty percent projects linearly to 2,500 more; at a tail factor of eight it is twenty thousand. Section 11.
This chapter against 25.4, stated precisely. That one owns whether a check happened. This one owns whether enough of them happened, measured against what — which is why every model here is about a denominator, and why section 14's weak definition is a coverage report reading 100%.
2. The One-Sentence Model
Coverage is closed when every specification feature has a bin, pairs are crossed rather than summed, nothing reachable was excluded, bins were exercised rather than merely reached, and no idle-bus value was recorded — and "the report says a hundred percent" 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 |
| Integrating a commercial VIP | 25.6 |
| What the coverage percentage is a percentage of | this chapter |
Deferred:
| Deferred ground | Owner |
|---|---|
| Vacuity and antecedent firing counts | 25.3 §6 |
| Cross-cache pair spaces for coherency | 25.2 §10 |
| Scoreboard keys and end-of-test drain | 25.4 §5 · §7 |
| Mutation scoring as a coverage metric | 25.1 §13 |
| Cryptographic primitives | out of scope — see §4 |
4. Teaching-Model Boundary
Every model is a small synchronous block that computes one property of a coverage number. A real coverage model is a set of covergroups with coverpoints, crosses, bins and sampling events, plus a database and a merge flow, and none of that is reproduced. What is reproduced is the arithmetic behind each number a report prints, and the shape of the mistake when the number is read without its denominator.
Three simplifications are worth stating. Section 5 assumes values are uniformly distributed across automatic bins, which they are not. Section 11 uses a single tail factor where a real closure curve is continuous. Section 13 prices merging as linear in runs and bins, ignoring database structure. 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 a defensible reading of the same data: let the tool pick the bins, cover the variables separately, exclude what will not close, count a hit as a hit, take the model as the specification, project the tail at the head's rate, sample on the clock. None of them is a lie, and each produces a higher number than the truth.
Figure 1 — The hundred unmodelled features are not uncovered in the report; they are absent from it. A coverage percentage cannot show a hole in its own denominator, which is why section 10's audit is against the specification rather than against the model.
5. RTL 1 — Automatic Bins Fold Distinct Values Together
// RTL 1 - the bin count. Automatic bins split a range into however many the
// tool defaults to, which is not the number of values that mean anything.
module bin_partition #(parameter int AUTO_BINS = 0) (
input logic clk, rst_n,
input logic measure,
input logic [15:0] distinct_values, auto_bin_max, values_hit,
output logic [15:0] bin_count, values_per_bin, hidden_values, covered_pct,
output logic partition_faithful,
output logic [7:0] n_measures, n_unfaithful,
output logic binning_hides_err
);
logic [31:0] v_q, p_q;
// One bin per meaningful value, or a fixed number of buckets over the range.
assign bin_count = (AUTO_BINS != 0)
? ((distinct_values > auto_bin_max) ? auto_bin_max
: distinct_values)
: distinct_values;
assign v_q = (bin_count == 16'd0) ? 32'd0
: (({16'd0, distinct_values} + {16'd0, bin_count} - 32'd1)
/ {16'd0, bin_count});
assign values_per_bin = (v_q > 32'd65535) ? 16'hFFFF : v_q[15:0];
// Values sharing a bin are hit when any one of them is. No guard on
// values_per_bin is needed: it exceeds one only when distinct_values
// exceeds bin_count, and the subtraction is zero otherwise.
assign hidden_values = (distinct_values > bin_count)
? (distinct_values - bin_count) : 16'd0;
assign p_q = (bin_count == 16'd0) ? 32'd0
: (({16'd0, values_hit} * 32'd100) / {16'd0, bin_count});
assign covered_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign partition_faithful = (hidden_values == 16'd0);
// Distinct values folded into one bin, reported as covered.
assign binning_hides_err = measure && (distinct_values > bin_count)
&& partition_faithful;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_measures <= 8'd0; n_unfaithful <= 8'd0;
end else if (measure) begin
n_measures <= n_measures + 8'd1;
if (!partition_faithful) n_unfaithful <= n_unfaithful + 8'd1;
end
end
endmoduleFive measurements. A 64-bucket automatic maximum.
| Distinct values | Explicit bins · Automatic bins |
|---|---|
| 256 | 256 bins, 1 value each, faithful · 64 buckets, 4 values each, 192 hidden, 100% reported |
| 32 | 32 · 32 · 1 each — auto-binning is faithful here |
| 64 | 64 · 64 · exactly one each, the last faithful count |
| 65 | 65 · 64 buckets, one holding 2 — exactly one value hidden |
| 0 | 0 · 0 · nothing to report |
An explicit partition is never unfaithful; auto-binning is, twice of five.
A bin is a claim that everything inside it is equivalent, and the tool's default makes that claim for you. Sixty-four buckets over two hundred and fifty-six values means four values share each bin, and the bin goes green when any one of the four is hit. The report says a hundred percent; three quarters of the values were never generated.
Rows three and four are the boundary. Sixty-four values in sixty-four buckets is faithful and sixty-five is not — one value hidden is enough to break the equivalence claim. The number that matters is not the bucket count but whether it equals the count of values that mean something different.
Row two is the case that hides the problem. With fewer values than buckets the automatic partition is exactly the explicit one, and every small enumerated field looks fine. The failure appears on wide fields — addresses, tags, lengths — which are exactly the ones a designer would not enumerate by hand.
6. RTL 2 — Two Variables Covered Separately Are Not Their Pairs
// RTL 2 - the cross. Two variables covered separately are not the pairs they
// can take together, and the bug lives in a pair.
module cross_space #(parameter int SEPARATE_POINTS = 0) (
input logic clk, rst_n,
input logic measure,
input logic [15:0] states_a, states_b, pairs_hit, singles_hit,
output logic [15:0] space, hit, covered_pct, remaining,
output logic closed,
output logic [7:0] n_measures, n_open,
output logic cross_missing_err
);
logic [31:0] s_q, p_q;
// Covering a and b separately is a + b bins; crossing them is a * b.
assign s_q = (SEPARATE_POINTS != 0)
? ({16'd0, states_a} + {16'd0, states_b})
: ({16'd0, states_a} * {16'd0, states_b});
assign space = (s_q > 32'd65535) ? 16'hFFFF : s_q[15:0];
assign hit = (SEPARATE_POINTS != 0)
? ((singles_hit > space) ? space : singles_hit)
: ((pairs_hit > space) ? space : pairs_hit);
assign p_q = (space == 16'd0) ? 32'd0
: (({16'd0, hit} * 32'd100) / {16'd0, space});
assign covered_pct = (p_q > 32'd65535) ? 16'hFFFF : p_q[15:0];
assign remaining = space - hit;
assign closed = (remaining == 16'd0);
// A pair space reported as a sum of two ranges. No guard on either state
// count is needed: a sum falls below a product only when both operands
// exceed one, so the comparison alone is exactly the condition.
assign cross_missing_err = measure && (space < (states_a * states_b));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_measures <= 8'd0; n_open <= 8'd0;
end else if (measure) begin
n_measures <= n_measures + 8'd1;
if (!closed) n_open <= n_open + 8'd1;
end
end
endmoduleFive measurements.
| Variables / hits | Crossed · Separate |
|---|---|
| 8 × 4, 24 pairs / 12 singles | 32 bins, 24 hit, 75%, 8 left · 12 bins, all hit, 100%, closed |
| 8 × 4, 32 pairs | 32 · 32 · 100% · closed · separate closed 20 pairs ago |
| 1 × 4 | 4 · 4 · closed · 5 bins — a sum larger than the product |
| 2 × 2 | 4 pairs, 3 hit, 1 left · 4 bins — the same number, different meaning |
| 0 × 4 | 0 · nothing to report |
Two open against the cross; one against separate points.
A bug lives in a combination, and a sum is not a product. Eight request types and four response codes is thirty-two pairs; covering the types and the codes separately is twelve bins, and twelve bins go green after twelve well-chosen transactions. The twenty pairs never generated include every interesting one — an error code on a request type that should never produce it.
Row four is the row worth reading twice. Two variables of two values each gives four pairs and four separate bins — the same number, which is exactly why the distinction is easy to miss in a small example. The bins are not the same bins: the separate model's four are a=0, a=1, b=0, b=1, and the cross's four are the pairs. The count coincides and the meaning does not.
Row three is the exemption. A variable with a single value makes the cross degenerate, and the separate model's sum is actually larger than the product. The cross is not always bigger — it is bigger whenever both variables have more than one value, which is the condition the error signal encodes exactly.
7. RTL 3 — An Exclusion Made To Reach Closure Is A Named Hole
// RTL 3 - exclusions. A bin excluded because it was unreachable is fine; one
// excluded because it would not close is a hole with a name.
module exclusions #(parameter int EXCLUDE_TO_CLOSE = 0) (
input logic clk, rst_n,
input logic audit,
input logic [15:0] total_bins, truly_unreachable, excluded, hit,
output logic [15:0] reachable, justified, unjustified, covered_pct,
output logic exclusions_sound,
output logic [7:0] n_audits, n_unsound,
output logic exclusion_abused_err
);
logic [31:0] p_q;
assign justified = (excluded > truly_unreachable) ? truly_unreachable : excluded;
assign unjustified = (EXCLUDE_TO_CLOSE != 0) ? 16'd0
: ((excluded > justified) ? (excluded - justified) : 16'd0);
assign reachable = (total_bins > excluded) ? (total_bins - excluded) : 16'd0;
assign p_q = (reachable == 16'd0) ? 32'd100
: (({16'd0, hit} * 32'd100) / {16'd0, reachable});
assign covered_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign exclusions_sound = (unjustified == 16'd0);
// Reachable bins excluded, and the exclusion reported as sound.
assign exclusion_abused_err = audit && (excluded > truly_unreachable)
&& exclusions_sound;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_audits <= 8'd0; n_unsound <= 8'd0;
end else if (audit) begin
n_audits <= n_audits + 8'd1;
if (!exclusions_sound) n_unsound <= n_unsound + 8'd1;
end
end
endmoduleFive audits. Two hundred bins, twelve genuinely unreachable.
| Excluded / hit | Justified · Unjustified · Reported |
|---|---|
| 40 / 160 | 12 · 28 unjustified · 100% — the permissive model reports 0 unjustified |
| 12 / 188 | 12 · 0 · exactly sound |
| 13 / 187 | 12 · 1 · one is enough to be unsound |
| 8 / 150 | 8 · 0 · 78% · excluding less than you could is not an abuse |
| 200 / 0 | 12 · 188 · 100% on no reachable bins at all |
Three unsound when the exclusions are audited; none when they are not.
Exclusions are the only mechanism in a coverage model that moves the denominator, and they are applied under deadline. Twelve bins are genuinely unreachable — a device type this build does not implement, a response the design cannot generate. Twenty-eight more are excluded because the regression would not reach them, and the report goes to a hundred percent.
Row five is the reduction to absurdity, and it is reachable in practice. Exclude every bin and the model reports a hundred percent on nothing — the arithmetic is consistent and the number is meaningless. Any coverage report should be read alongside its reachable-bin count, and almost none are.
Row four is the direction that is safe. Excluding fewer bins than are unreachable leaves real coverage on the table — 78% instead of a higher number — and the audit correctly does not flag it. The check is for exclusions that remove reachable bins, not for exclusions in general.
Figure 2 — Both exclusion paths reach the denominator by the same route and look identical in the report. Only one of them is a claim that the design cannot produce the value, and the difference lives in a comment that twenty-eight of these forty do not have.
8. RTL 4 — A Weighted Total Lets A Big Group Carry A Small One
// RTL 4 - weighting. A weighted total lets a large well-covered group carry a
// small uncovered one, and the report is a single number.
module weighted_total #(parameter int WEIGHT_BY_SIZE = 0) (
input logic clk, rst_n,
input logic report_it,
input logic [15:0] big_bins, big_hit, small_bins, small_hit,
output logic [15:0] weighted_pct, worst_group_pct, spread,
output logic every_group_closed,
output logic [7:0] n_reports, n_hiding,
output logic weighting_hides_err
);
logic [31:0] w_q, b_q, s_q;
assign b_q = (big_bins == 16'd0) ? 32'd100
: (({16'd0, big_hit} * 32'd100) / {16'd0, big_bins});
assign s_q = (small_bins == 16'd0) ? 32'd100
: (({16'd0, small_hit} * 32'd100) / {16'd0, small_bins});
// Weighting by size lets the big group dominate; the unweighted view is the
// worst group.
assign w_q = (WEIGHT_BY_SIZE != 0)
? ((({16'd0, big_hit} + {16'd0, small_hit}) * 32'd100)
/ (({16'd0, big_bins} + {16'd0, small_bins}) == 32'd0 ? 32'd1
: ({16'd0, big_bins} + {16'd0, small_bins})))
: ((b_q < s_q) ? b_q : s_q);
assign weighted_pct = (w_q > 32'd100) ? 16'd100 : w_q[15:0];
assign worst_group_pct = ((b_q < s_q) ? b_q[15:0] : s_q[15:0]);
assign spread = (b_q > s_q) ? (b_q[15:0] - s_q[15:0]) : (s_q[15:0] - b_q[15:0]);
assign every_group_closed = (worst_group_pct >= 16'd100);
// A group at zero, hidden inside a high total.
assign weighting_hides_err = report_it && (worst_group_pct < 16'd100)
&& (weighted_pct >= 16'd95);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_reports <= 8'd0; n_hiding <= 8'd0;
end else if (report_it) begin
n_reports <= n_reports + 8'd1;
if (!every_group_closed) n_hiding <= n_hiding + 8'd1;
end
end
endmoduleSeven reports. A 900-bin group unless stated.
| Big group / small group | Worst group · Weighted total |
|---|---|
| 900 fully covered / 100 at zero | 0% · 90% |
| 900 covered / 40 at zero | 0% · 95% — where the weighting starts to hide it |
| 900 covered / 10 at zero | 0% · 98% — a group never entered, inside a near-closed report |
| 900 covered / 100 covered | 100% · 100% · closed |
| both half covered | 50% · 50% · the two views agree |
| 900 at 50% / 100 at 100% | 50% · the spread computed the other way round |
| 900 at 99% / 100 at 100% | 99% · not closed — the threshold is a hundred |
Six of seven with a group short — the same six either way, because the worst group is a fact regardless of the weighting.
A single coverage number is an average, and averages hide their tails. A thousand bins at ninety-eight percent sounds like two dozen stragglers. It can be a ten-bin group that has never been entered at all — an entire feature, an entire error class, an entire device type — carried by nine hundred bins of routine traffic.
Rows one to three are the same hole at three sizes, and the smaller the uncovered group the higher the total and the less likely anyone looks. The uncovered group's size is inversely related to its visibility, which is exactly backwards from how it should work.
Row seven is why the closure threshold is exactly a hundred. A worst group at ninety-nine percent is one bin short, and one bin is a feature. A "close enough" threshold of ninety-nine turns every group into a group with a permitted hole, which is section 7's exclusion problem arriving through a different door.
9. RTL 5 — A Bin Hit Once Has Been Reached, Not Exercised
// RTL 5 - the hit threshold. A bin hit once has been reached; a bin hit enough
// times has been exercised, and only one of those is evidence.
module hit_threshold #(parameter int ONE_HIT_IS_ENOUGH = 0) (
input logic clk, rst_n,
input logic measure,
input logic [15:0] total_bins, hit_once, hit_enough, at_least,
output logic [15:0] counted, shallow, covered_pct,
output logic depth_adequate,
output logic [7:0] n_measures, n_shallow,
output logic shallow_coverage_err
);
logic [31:0] p_q;
assign counted = (ONE_HIT_IS_ENOUGH != 0) ? hit_once : hit_enough;
assign shallow = (hit_once > hit_enough) ? (hit_once - hit_enough) : 16'd0;
assign p_q = (total_bins == 16'd0) ? 32'd0
: (({16'd0, counted} * 32'd100) / {16'd0, total_bins});
assign covered_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign depth_adequate = (shallow == 16'd0) || (at_least <= 16'd1);
// Bins reached once, counted as exercised.
assign shallow_coverage_err = measure && (at_least > 16'd1)
&& (shallow != 16'd0) && (counted == hit_once);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_measures <= 8'd0; n_shallow <= 8'd0;
end else if (measure) begin
n_measures <= n_measures + 8'd1;
if (!depth_adequate) n_shallow <= n_shallow + 8'd1;
end
end
endmoduleFive measurements. A thousand bins, 900 reached at least once.
| Reached enough / threshold | Counted · Shallow · Reported |
|---|---|
| 600 / 10 | 600 · 300 shallow · 60% — the one-hit model reports 900 and 90% |
| 600 / 1 | 600 · 300 still reached once · adequate, because one hit IS the threshold |
| 900 / 10 | 900 · 0 · adequate · both models count nine hundred |
| 899 / 10 | 899 · 1 · one shallow bin fails the depth check |
| no bins at all | 0 · 0 · nothing to report |
Two with inadequate depth — the same two either way, because depth is a fact regardless of what is counted.
"Hit" and "exercised" are different facts and most reports print only the first. A bin reached once has been reached by one stimulus, on one path, in one context. A threshold of ten says the tool must see it ten times before it counts, and the difference between the two numbers here is thirty percentage points on the same regression.
Row two is the exemption and it is the one that keeps the check honest. With a threshold of one, three hundred bins reached once are exactly as exercised as the model requires — the depth is adequate by definition. The defect is not a low threshold; it is a threshold above one whose shortfall is counted as coverage anyway.
Row four is the smallest failure. One bin of a thousand reached but not exercised is enough to fail the depth check, and it will not move the percentage by a visible amount. The depth check has to be a separate boolean, because it cannot be seen in the number.
10. RTL 6 — The Model Is Not The Specification
// RTL 6 - coverage against the specification. A model covers the bins somebody
// wrote, and the features nobody wrote a bin for are not in the denominator.
module model_completeness #(parameter int MODEL_IS_THE_SPEC = 0) (
input logic clk, rst_n,
input logic audit,
input logic [15:0] spec_features, modelled_features, hit_features,
output logic [15:0] denominator, unmodelled, reported_pct, true_pct,
output logic model_complete,
output logic [7:0] n_audits, n_incomplete,
output logic denominator_shrunk_err
);
logic [31:0] r_q, t_q;
// The honest denominator is the specification, not the model.
assign denominator = (MODEL_IS_THE_SPEC != 0) ? modelled_features : spec_features;
assign unmodelled = (spec_features > modelled_features)
? (spec_features - modelled_features) : 16'd0;
assign r_q = (modelled_features == 16'd0) ? 32'd0
: (({16'd0, hit_features} * 32'd100) / {16'd0, modelled_features});
assign reported_pct = (r_q > 32'd100) ? 16'd100 : r_q[15:0];
assign t_q = (spec_features == 16'd0) ? 32'd0
: (({16'd0, hit_features} * 32'd100) / {16'd0, spec_features});
assign true_pct = (t_q > 32'd100) ? 16'd100 : t_q[15:0];
assign model_complete = (unmodelled == 16'd0);
// Specification features with no bin, left out of the denominator.
assign denominator_shrunk_err = audit && (unmodelled != 16'd0)
&& (denominator == modelled_features);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_audits <= 8'd0; n_incomplete <= 8'd0;
end else if (audit) begin
n_audits <= n_audits + 8'd1;
if (!model_complete) n_incomplete <= n_incomplete + 8'd1;
end
end
endmoduleFive audits. Four hundred specification features unless stated.
| Modelled / hit | Unmodelled · Reported · True |
|---|---|
| 300 / 300 | 100 · 100% · 75% |
| 400 / 400 | 0 · 100% · 100% · the model is complete |
| 399 / 399 | 1 · 100% · 99% — the smallest denominator a model can shrink |
| 300 / 150 | 100 · 50% · 37% |
| no features listed | 0 · 0 · nothing to report |
Three incomplete models — the same three either way, because completeness is a fact regardless of which denominator is printed.
This is the failure the other nine sections are special cases of. A coverage model measures the bins somebody wrote. A specification feature with no bin does not appear as an uncovered bin; it does not appear. There is no number in any report that goes down when a feature is forgotten.
Row four separates the two ways to be short. Fifty percent reported and thirty-seven percent true: the gap between them is the unmodelled quarter, and only the second number answers "how much of the specification have we exercised". A team reading the first is measuring its own model's completeness against itself.
Row three is the audit's sensitivity. One feature of four hundred with no bin still reports a hundred percent — the report cannot round down far enough to show it. The audit has to be a line-by-line comparison against the specification's feature list, which is a day's work and the only thing that finds it.
11. RTL 7 — The Last Twenty Points Cost More Than The First Eighty
// RTL 7 - the closure curve. Random stimulus finds the easy bins immediately
// and the last few cost more runs than all the others together.
module closure_curve #(parameter int LINEAR_PROJECTION = 0) (
input logic clk, rst_n,
input logic project,
input logic [15:0] runs_so_far, pct_so_far, target_pct, hard_bin_factor,
output logic [15:0] gap, linear_runs, real_runs, extra_runs,
output logic projection_safe,
output logic [7:0] n_projections, n_optimistic,
output logic curve_ignored_err
);
logic [31:0] l_q, r_q;
assign gap = (target_pct > pct_so_far) ? (target_pct - pct_so_far) : 16'd0;
assign l_q = (pct_so_far == 16'd0) ? 32'd0
: (({16'd0, runs_so_far} * {16'd0, gap}) / {16'd0, pct_so_far});
assign linear_runs = (l_q > 32'd65535) ? 16'hFFFF : l_q[15:0];
// The remaining bins are the rare ones, and they cost a multiple.
assign r_q = (LINEAR_PROJECTION != 0) ? {16'd0, linear_runs}
: ({16'd0, linear_runs} * {16'd0, hard_bin_factor});
assign real_runs = (r_q > 32'd65535) ? 16'hFFFF : r_q[15:0];
assign extra_runs = (real_runs > linear_runs) ? (real_runs - linear_runs) : 16'd0;
assign projection_safe = (real_runs <= linear_runs);
// A tail projected at the rate of the easy bins.
assign curve_ignored_err = project && (hard_bin_factor > 16'd1)
&& (gap != 16'd0) && (real_runs == linear_runs);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_projections <= 8'd0; n_optimistic <= 8'd0;
end else if (project) begin
n_projections <= n_projections + 8'd1;
if (!projection_safe) n_optimistic <= n_optimistic + 8'd1;
end
end
endmoduleFive projections. Ten thousand runs to eighty percent, aiming at a hundred.
| Tail factor / target | Gap · Linear · Real · Extra |
|---|---|
| 8 / 100 | 20 · 2,500 · 20,000 · 17,500 more than projected |
| 8 / already at 100 | 0 · 0 · 0 · nothing to project |
| 1 / 100 | 20 · 2,500 · 2,500 · the curve is the line |
| 2 / 100 | 20 · 2,500 · 5,000 · the smallest tail that costs anything |
| 8 / target 70, below where we stand | 0, floored · 0 · 0 |
Two optimistic when the tail factor is applied; none when it is not.
Random stimulus finds the common cases immediately and the rare ones asymptotically. Eighty percent in ten thousand runs makes twenty more points look like a quarter of the work done so far. The remaining bins are the ones random stimulus is bad at reaching — that is why they are still open — and at a tail factor of eight they cost twenty thousand runs, twice the entire campaign to date.
Row three is the honest linear case and it does exist. A tail factor of one means the remaining bins are as easy as the ones already hit, which happens when coverage is limited by run count rather than by stimulus shape. The model reports it as safe rather than as an ignored curve, because a linear tail is a real outcome and not a modelling error.
Row one is why closure plans slip. The plan says 2,500 runs and the machine says twenty thousand, and the discrepancy is discovered by running the 2,500. The corrective is directed stimulus for the tail, which is a different activity with a different estimate — and the decision to switch is made better at eighty percent than at ninety-five.
12. RTL 8 — Sampling On The Clock Records The Idle Bus
// RTL 8 - the sampling point. A coverage group samples on an event, and an
// event chosen for convenience records a value that was never live.
module sample_point #(parameter int SAMPLE_ON_ANY_CLOCK = 0) (
input logic clk, rst_n,
input logic measure,
input logic [15:0] cycles, valid_cycles, distinct_live, distinct_idle,
output logic [15:0] samples, real_samples, junk_samples, junk_pct,
output logic sampling_sound,
output logic [7:0] n_measures, n_unsound,
output logic junk_recorded_err
);
logic [31:0] j_q;
// Sampling on the transaction event records only live values; sampling on
// the clock records whatever the bus held between transactions.
assign samples = (SAMPLE_ON_ANY_CLOCK != 0) ? cycles : valid_cycles;
assign real_samples = (valid_cycles > samples) ? samples : valid_cycles;
assign junk_samples = (samples > real_samples) ? (samples - real_samples) : 16'd0;
assign j_q = (samples == 16'd0) ? 32'd0
: (({16'd0, junk_samples} * 32'd100) / {16'd0, samples});
assign junk_pct = (j_q > 32'd65535) ? 16'hFFFF : j_q[15:0];
assign sampling_sound = (junk_samples == 16'd0);
// Idle-bus values recorded as coverage.
assign junk_recorded_err = measure && (distinct_idle != 16'd0)
&& (junk_samples != 16'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_measures <= 8'd0; n_unsound <= 8'd0;
end else if (measure) begin
n_measures <= n_measures + 8'd1;
if (!sampling_sound) n_unsound <= n_unsound + 8'd1;
end
end
endmoduleFive measurements. Ten thousand cycles unless stated.
| Valid cycles / idle values | Event sampling · Clock sampling |
|---|---|
| 2,000 / 3 | 2,000 samples, all real · 10,000 samples, 8,000 junk, 80% |
| 10,000 / 3 | 10,000 · 10,000 · both sound — the bus is never idle |
| 9,999 / 3 | 9,999 · 1 junk — the smallest amount of idle bus recorded |
| 2,000 / no distinct idle value | 2,000 · 8,000 junk · but they all land in one bin |
| a run of no cycles | 0 · 0 · nothing to report |
The event sampler is never unsound; the clock sampler is, three times of five.
A covergroup samples when its event fires, and choosing the clock is the shortest way to make it fire. On a bus that carries a transaction on twenty percent of cycles, eight thousand of ten thousand samples record whatever the signals held between transactions — a parked address, a stale opcode, an idle encoding.
Row four is the reason the defect is survivable. If the idle bus holds one constant value, all eight thousand junk samples land in a single bin, which shows up as one implausibly-hot bin and nothing else. The model reports the junk without the error, because a single extra bin is a nuisance rather than a distortion.
Row one is when it becomes a distortion. Three distinct idle values scattered across the coverage space are three bins that go green without a transaction ever producing them — and if any of them is a legitimate encoding, a real hole has been filled with idle noise.
13. RTL 9 — Every Bin Is A Counter And Every Run Is A Merge
// RTL 9 - what coverage costs. Every bin is a counter written on every sample,
// and every run's database is merged with every other run's.
module coverage_cost #(parameter int SAMPLING_IS_FREE = 0) (
input logic clk, rst_n,
input logic budget,
input logic [15:0] bin_count, samples_k, ns_per_sample, runs,
input logic [15:0] budget_min,
output logic [15:0] sample_min, merge_min, total_min, overrun,
output logic affordable,
output logic [7:0] n_budgets, n_over,
output logic sampling_cost_ignored_err
);
logic [31:0] s_q, m_q, t_q;
assign s_q = (SAMPLING_IS_FREE != 0) ? 32'd0
: (({16'd0, samples_k} * {16'd0, ns_per_sample}) / 32'd60000);
assign sample_min = (s_q > 32'd65535) ? 16'hFFFF : s_q[15:0];
// Merging is linear in runs and in bins.
assign m_q = (SAMPLING_IS_FREE != 0) ? 32'd0
: (({16'd0, runs} * {16'd0, bin_count}) / 32'd10000);
assign merge_min = (m_q > 32'd65535) ? 16'hFFFF : m_q[15:0];
assign t_q = {16'd0, sample_min} + {16'd0, merge_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);
// Sampling that happens on every transaction, costed at nothing.
assign sampling_cost_ignored_err = budget && (samples_k != 16'd0)
&& (ns_per_sample != 16'd0)
&& (sample_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. Twenty thousand bins, 40 million samples at 150 ns.
| Runs / budget | Sampling · Merging · Total |
|---|---|
| 2,000 / 120 min | 100 min · 4,000 min · 4,100 · 3,980 over |
| 200 / 500 min | 100 · 400 · exactly 500 — it fits |
| 200, sampling free per sample / 500 | 0 · 400 · 400 |
| 200, no bins / 500 | 100 · 0 · 100 |
| 200, 100k samples / 500 | 0 — fifteen seconds rounds to nothing · both models agree |
| 200, never sampled / 500 | 0 · 400 — merging happens regardless |
One over budget when the sampling is charged; none when it is not.
Merging is the cost nobody predicts, and it is forty times the sampling here. A hundred minutes of sampling across a regression is noticeable; four thousand minutes of merging two thousand databases of twenty thousand bins each is a nightly flow that does not finish. Merge time is linear in both runs and bins, so a coverage model that doubles is a merge that doubles.
Row six is the one worth stating explicitly. A model that is never sampled — a covergroup instantiated and left unconnected — still costs the full merge, because the database exists for every run. It is the worst possible position: all of the cost, none of the information.
Row five is a degenerate case both models agree on, driven deliberately. A hundred thousand samples at 150 ns is fifteen seconds, which rounds to zero minutes, so the full model and the free-sampling model report the same total and both raise the same flag.
14. RTL 10 — A Coverage Sign-Off Assembled
// RTL 10 - a coverage sign-off assembled. Everything that must hold before
// "coverage is at 100%" is a claim about the model rather than the design.
module coverage_signoff #(parameter int HUNDRED_PERCENT = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic at_hundred, // the report says 100%
input logic model_covers_spec, // every spec feature has a bin
input logic crosses_present, // pairs are crossed, not summed
input logic exclusions_sound, // nothing reachable was excluded
input logic depth_adequate, // bins were hit enough, not once
input logic sampling_sound, // no idle-bus values recorded
output logic closed,
output logic [5:0] fail_mask,
output logic [7:0] n_eval, n_closed,
output logic false_closure_err
);
assign fail_mask[0] = ~at_hundred;
assign fail_mask[1] = ~model_covers_spec;
assign fail_mask[2] = ~crosses_present;
assign fail_mask[3] = ~exclusions_sound;
assign fail_mask[4] = ~depth_adequate;
assign fail_mask[5] = ~sampling_sound;
// The hundred-percent build is what a coverage report says.
assign closed = (HUNDRED_PERCENT != 0) ? at_hundred : (fail_mask == 6'd0);
assign false_closure_err = evaluate && closed && (fail_mask != 6'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_eval <= 8'd0; n_closed <= 8'd0;
end else if (evaluate) begin
n_eval <= n_eval + 8'd1;
if (closed) n_closed <= n_closed + 8'd1;
end
end
endmoduleSeven configurations.
| What fails | Mask · Full model · Coverage report |
|---|---|
| nothing | 000000 · closed · closed |
| features with no bin — §10 | 000010 · not closed · claims closed |
| variables summed, not crossed — §6 | 000100 · not closed · claims closed |
| reachable bins excluded — §7 | 001000 · not closed · claims closed |
| bins reached once — §9 | 010000 · not closed · claims closed |
| idle-bus values recorded — §12 | 100000 · not closed · claims closed |
| the report is genuinely below a hundred | 000001 · not closed · not closed |
One closed under the full model; six under the report.
Every one of the five middle rows produces a report reading exactly a hundred percent. Not ninety-eight, not "closed with waivers" — a hundred, arrived at honestly by a tool doing exactly what it was asked. The number is correct and the question it answers is not the one being asked.
Rows three, four and five each move the denominator; row six adds to the numerator. Summing instead of crossing shrinks the space, excluding removes bins, a one-hit threshold lowers the bar, and idle sampling fills bins with values no transaction produced. Four different mechanisms, one report.
Row two is the one no amount of care in the other four prevents. A feature with no bin is invisible to every check in this chapter — it is not a hole in the coverage, it is a hole in the map — and the only detector is a comparison against the specification's own feature list.
Figure 4 — Model completeness is asked first because it is the only failure that cannot be seen in any number the report prints. The other four are visible to somebody who knows to look at the bin count, the cross list, the exclusion list and the hit threshold. Every exit above reads a hundred percent.
15. Quantitative Reasoning
Bin partitioning. Two hundred and fifty-six values in 64 automatic buckets is four values per bin and 192 hidden; sixty-four values is exactly faithful and sixty-five is not.
Crosses. Eight request types by four response codes is thirty-two pairs against twelve separate bins — and twenty-four pairs hit is 75% crossed and 100% summed.
Exclusions. Forty excluded against twelve unreachable is twenty-eight reachable bins removed; excluding all two hundred reports a hundred percent on nothing.
Weighting. A 900-bin group covered and a 10-bin group at zero is 98% weighted and 0% worst-group; the smaller the hole the higher the total.
Hit thresholds. Nine hundred bins reached and six hundred exercised is 90% or 60%, thirty points apart on one regression.
Model completeness. Three hundred bins for four hundred features is 100% reported against 75% true, and one feature short still reports a hundred.
The closure curve. Ten thousand runs to 80% projects to 2,500 more linearly and 20,000 at a tail factor of eight — 17,500 runs of difference.
Sampling. A bus valid on 2,000 of 10,000 cycles sampled on the clock is 8,000 junk samples, eighty percent, and one idle cycle is the smallest amount recordable.
Cost. Twenty thousand bins over 2,000 runs is 4,000 minutes of merging against 100 of sampling — forty to one.
The assembled model. Six properties, seven configurations, one closed. The report called six closed.
| Quantity | Correct · Broken · Ratio |
|---|---|
| Values represented by 64 automatic bins | 256 · 64 · 192 hidden |
| Bins in an 8-by-4 space | 32 · 12 · 2.7x |
| Coverage of 24 hits in that space | 75% · 100% · 20 pairs unseen |
| Bins removed by unjustified exclusion | 0 · 28 · 14% of the model |
| Coverage with a 10-deep hit threshold | 60% · 90% · 30 points |
| True coverage of a 400-feature spec | 75% · 100% reported · a quarter absent |
| Runs to close from 80% | 20,000 · 2,500 projected · 8x |
| Samples that record a live value | 2,000 · 2,000 of 10,000 · 80% junk |
| Minutes to merge 2,000 runs | 4,000 · 0 counted · the whole flow |
| Configurations called closed, 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 concurrent assertion support, so each is a procedural comparison against 1'b1, and every one is an equality.
Every model's outputs were listed and each confirmed to appear in an equality before the campaign ran. It found one gap here: real_samples in section 12, computed and observed only through junk_samples. Three chapters, three catches, roughly a minute each — the step now finds something every time it runs.
Alongside it: every inclusive threshold at exactly equal, every ceiling on and off its boundary, every floor past it, and both builds asserted on every degenerate case.
Bin partitioning. The faithful boundary is driven at sixty-four values and sixty-five, which is where automatic binning stops representing the field.
chk(pBv == 16'd1, "exactly one each");
chk(pBh == 16'd1, "hiding exactly one");Crosses. A two-by-two cross is driven, where the sum and the product are both four and only the meaning differs — the configuration in which the mistake is invisible.
Exclusions. Exclusions equal to the unreachable count and one greater are both driven, and excluding fewer than possible is asserted not an abuse.
Weighting. A worst group at exactly ninety-nine is constructed, and the spread is driven in both directions — a small group better covered than a big one.
chk(wGs == 16'd50, "a spread of fifty, computed the other way round");
chk(wGo == 16'd99, "the worst group is at ninety-nine");Hit thresholds. A threshold of one with shallow bins present is driven — the configuration in which the exemption is load-bearing rather than incidental.
Model completeness. One feature short of complete is driven, which still reports a hundred percent.
The closure curve. A tail factor of one and of two are both driven — the linear case and the smallest tail that costs anything — and a target below current coverage is asserted to floor rather than wrap.
Sampling. A single idle cycle is driven, and an idle bus with no distinct value is asserted quiet in both builds.
Cost. A model that is never sampled is driven and asserted to cost the full merge.
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: 290 checks across two testbenches, 146 on the front five models and 144 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 seven survivors — the most of any chapter in this batch.
| Mutation class | Killed by |
|---|---|
| The two bin models swapped | 64 buckets against 256 bins — §5 row one |
| Values per bin floored instead of ceiled | Four per bin, not three — §5 row one |
| Hidden values subtracting the bin width | 192, not 252 — §5 row one |
| A value count equal to the bins called folded | 64 values in 64 buckets — §5 row three |
| The cross adding instead of multiplying | 32, not 12 — §6 row one |
| The understatement test comparing against a sum | Twelve bins against a 32-pair space — §6 row one |
| The justified clamp taken the wrong way | Twelve justified of forty — §7 row one |
| No reachable bins reporting nothing | A hundred percent on an empty denominator — §7 row five |
| Excluding exactly the unreachable called abuse | Twelve excluded, twelve unreachable — §7 row two |
| The unweighted view taking the better group | The worst group, not the best — §8 row one |
| The spread taken one way only | A small group better covered than a big one — §8 row six |
| Every group closed at ninety-nine | A worst group of exactly 99 — §8 row seven |
| The one-hit model counting the exercised | Nine hundred against six hundred — §9 row one |
| The threshold-of-one exemption dropped | A threshold of one with 300 shallow bins — §9 row two |
| A threshold of one counted as deep | The same row, on the error signal — §9 row two |
| The two denominators swapped | Four hundred, not three hundred — §10 row one |
| The true percentage divided by the model | 75%, not 100% — §10 row one |
| The curve model dropping the tail factor | Twenty thousand runs, not 2,500 — §11 row one |
| A tail factor of one counted as ignored | The linear case — §11 row three |
| The two samplers swapped | 2,000 samples against 10,000 — §12 row one |
| The has-samples guard dropped | A model that is never sampled — §13 row six |
| Merging scaled by a thousand | 4,000 minutes, not 40,000 — §13 row one |
| 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 |
Five of the seven survivors were genuinely undriven cases, and they cluster. Three of them — the spread's second direction, a worst group of exactly ninety-nine, a threshold of one with shallow bins present — are cases where the model's own exemption or absolute value is load-bearing and my stimulus only ever approached from one side. The fourth was a model never sampled; the fifth was that same exemption seen through its error signal.
Two were dominated guards, and both were dominated by an upstream clamp — the same mechanism as 25.4 §17. Section 5's values_per_bin > 1 restates distinct_values > bin_count; section 6's guards on states_a and states_b restate the comparison space < a * b, which is exactly the condition and needs no guard at all.
Two replacement mutations were themselves equivalent and had to be replaced again. Changing section 5's hidden-value condition changed nothing because the subtraction was the same in both branches; changing section 6's remaining guard changed nothing for the same reason the first one did. A replacement mutation needs the same reachability argument as the original, and assuming it inherits one is how a campaign quietly loses coverage of its own.
That is ten dead guards across four chapters, holding at roughly a quarter of survivors, and the clamp is now the identified commonest cause.
18. Verification Strategy
What a testbench for a measurement model must cover.
Run the output-listing step before the campaign. Three chapters, three catches. It has found something every single time, which is the argument for making it a step rather than a principle.
When a guard follows a clamp, assume it is dead until proved otherwise. Four of this batch's ten dominated guards restate a min or a ceiling that already happened upstream. The test is whether the guard can be false while the rest of the expression is true.
Re-derive reachability for a replacement mutation. Two of this chapter's replacements were equivalent mutants. A mutation is not valid because it replaced a valid one — it needs its own argument, and the cheapest one is to name the assertion that will kill it before running.
Drive an absolute value from both sides. §8's spread and §12's junk count are both differences, and a testbench that only ever puts the same operand on top executes half of each.
Drive an exemption where it is load-bearing. §9's threshold-of-one exemption is only meaningful when there are shallow bins to exempt. An exemption driven in a configuration that does not need it proves nothing about the exemption.
The cases where the simpler model is right. Fewer values than buckets. A single-valued variable. Excluding only what is unreachable. Groups that agree. A threshold of one. A complete model. A linear tail. A bus that is never idle. Eight exemptions across nine models.
Counters as a second signature. Ten models, ten pairs of totals, differing in seven. Three are deliberately equal — §8's n_hiding, §9's n_shallow and §10's n_incomplete — because the worst group, the shallow count and the model's completeness are facts about the data rather than about the reporting choice. Manufacturing a difference there would misrepresent what the parameter controls.
What a real coverage effort needs that these models do not have. A non-uniform value distribution for §5, a continuous closure curve for §11, and a database-structure-aware merge model for §13. All three are abbreviations that preserve the conclusion, and section 26 exercises 1, 7 and 9 are where they come back.
19. Synthesis and Implementation Reality
auto_bin_max defaults to 64 and almost nobody changes it. A coverpoint on an 8-bit field silently becomes sixty-four buckets of four values; on a 32-bit field it becomes sixty-four buckets of sixty-seven million. The default is a number, and it is in the report's denominator.
A cross is one line and multiplies the bin count. That is the real reason crosses are omitted: the model gets large, the merge gets slow (section 13), and the pressure to sum instead of cross is a runtime pressure rather than a conceptual one.
ignore_bins and illegal_bins are different declarations with the same effect on the denominator. Only one of them is a claim that the design cannot produce the value. Section 7's audit has to read which was used and why, and the "why" is almost never recorded next to it.
option.at_least defaults to one. Section 9's entire distinction is a defaulted option, and raising it makes the coverage number go down — which is why it is raised on new models and quietly left at one on models that are close to closing.
The sampling event is chosen when the covergroup is written and rarely revisited. A sample() call in a monitor's transaction handler is right; one in an always @(posedge clk) block is section 12. Both compile, and only one of them is a measurement.
Merge is a flow step with no owner. Section 13's four thousand minutes appear as "the coverage job is slow", and the corrective — fewer bins, fewer runs merged, hierarchical merging — is a modelling decision made by whoever owns the flow rather than the model.
20. Silicon Observability
| Counter | Why it matters |
|---|---|
| Reachable bin count, printed beside every percentage | §7 — a hundred percent of nothing is a hundred percent |
| Bins excluded, with the declaration used and a reason | §7 — ignore and illegal differ in meaning and not in effect |
| Specification features against modelled features | §10 — the only detector for a hole in the map |
| Worst group percentage alongside the weighted total | §8 — the total hides the tail by construction |
at_least value in force, per covergroup | §9 — the difference between reached and exercised |
| Bins at exactly one hit, as a count | §9 — the shallow population, invisible in the percentage |
| Values per automatic bin, per coverpoint | §5 — the equivalence claim the tool made for you |
| Samples taken against transactions observed | §12 — a ratio above one is idle-bus sampling |
| Coverage gained per thousand runs, over time | §11 — the closure curve, measured rather than assumed |
| Merge wall-clock per regression, against bin count | §13 — the cost that grows with the model |
"Samples taken against transactions observed" is the cheapest audit here. Both numbers already exist in every environment, and a ratio above one is section 12 happening. It takes one line to print and it makes an invisible distortion into an obvious one.
21. Debug Lab
Symptom. A CXL device reaches 100% functional coverage and signs off. Four months later a customer reports data corruption on a specific combination: a partial write to a device-bias line during a bias transition. The team's first reaction is that the coverage model must have a hole. It does not — the model reports the combination as covered.
Step 1 — is the bin actually hit? It is, forty-one times. Section 9's question next: what is at_least? It is one, the default, and the bin was hit forty-one times by a single stimulus pattern repeated. Reached, not exercised.
Step 2 — was the combination ever crossed? The model has a coverpoint for write type and a coverpoint for bias state. Section 6. They are not crossed. Both are fully covered separately; the specific pair — partial write against device bias — is one of the twenty pairs never generated. The bin the team looked at in step 1 was the write-type bin, not the pair.
Step 3 — how the model came to have no cross. The cross existed in the first version and was removed. The commit message says "coverage merge exceeding nightly window". Section 13: twenty thousand bins over two thousand runs is four thousand minutes of merging, and removing the cross brought it inside the window. A runtime problem was solved by deleting a measurement.
Step 4 — the exclusions. Forty bins are excluded. Twelve have a comment naming an unimplemented device type. Twenty-eight have no comment, and were added in a single commit two weeks before sign-off. Section 7. Three of them are bias-transition bins.
Step 5 — the sampling. The covergroup samples on posedge clk. Section 12. The bus carries a transaction on about a fifth of cycles, so four fifths of the samples record the idle bus. The idle encoding for write type happens to be a legal value — which means one write-type bin has been green since day one without a single transaction producing it.
Step 6 — what a hundred percent meant. A model missing the cross that mattered, with twenty-eight unexplained exclusions, a default hit threshold, and eighty percent of its samples taken from an idle bus. Every one of those is a defensible engineering decision made under a real constraint, and the four of them together produced a number that meant nothing.
The finding. One hardware bug and four independent reasons the coverage number could not have flagged it. Section 14's rows six, three, four and five, all at once — and the report read exactly a hundred.
The fix. In the RTL, hold the partial write until the bias transition completes. In the environment: restore the cross and solve the merge cost properly — hierarchical merge, or fewer runs merged nightly — raise at_least above one, require a reason string on every exclusion, and move the sampling to the transaction event. Then print the reachable bin count next to the percentage, so the next version of this cannot be silent.
What made this hard. Nobody was careless. The cross was removed to fix a real flow problem, the exclusions were added to hit a real deadline, the threshold was a default, and the sampling event was the obvious one — and the sum of four reasonable decisions was a sign-off on a number with no content.
22. Design Review
1. What is the reachable bin count, printed next to the percentage? A hundred percent of nothing is a hundred percent. Sections 7 and 20.
2. Which specification features have no bin? The only failure invisible to every other check here. Section 10.
3. Which variable pairs are crossed, and which were removed — and why? A cross deleted for merge time is a measurement deleted. Sections 6 and 13.
4. What is at_least, per covergroup? The default is one, and it is the difference between reached and exercised. Sections 9 and 19.
5. How many bins sit at exactly one hit? The shallow population, invisible in the percentage. Section 9.
6. Does every exclusion carry a reason, and is it a reachability claim? Twenty-eight of forty had no comment. Section 7.
7. How many values share each automatic bin? Sixty-four buckets is a default, not a decision. Sections 5 and 19.
8. What is the ratio of samples taken to transactions observed? Above one is idle-bus sampling. Sections 12 and 20.
9. What is the measured coverage gain per thousand runs, and what does it project? A linear projection of a non-linear tail is 17,500 runs optimistic. Section 11.
10. What does a hundred percent establish? Section 14 exists because the answer is the last property only.
23. How This Appears In Real Engineering
A verification engineer writes a covergroup and reads a percentage. Nothing in the ordinary flow prints the denominator, and every failure in this chapter is a denominator problem.
A verification lead is measured on the coverage number and owns the decisions that raise it. Exclusions, thresholds and cross removal all raise it honestly, which is the structural problem — the metric and the means to improve it are the same lever.
A project manager sees a percentage and a projection. Section 11 is why the projection is wrong and why "we're at 92%, two weeks to go" has a specific and predictable failure mode.
An architect owns the feature list that ought to be the denominator. Section 10 is the gap between that list and the covergroup, and closing it is a comparison nobody is assigned.
24. Common Misconceptions
"Coverage is at 100%." Of a model somebody wrote. Three hundred bins for four hundred features is a hundred percent reported at seventy-five percent true (section 10), and no number in the report goes down when a feature is forgotten.
"The tool picked sensible bins." The tool picked sixty-four. On a 256-value field that is four values sharing every bin (section 5), each going green when any one of them is generated.
"We cover both variables." Separately. Eight types and four codes is thirty-two pairs against twelve bins (section 6), and the twenty pairs never generated are where the interesting bugs are.
"The excluded bins are unreachable." Twelve of them were. Twenty-eight were excluded to reach closure (section 7), and the two look identical in the report.
"The bin is covered — it was hit." Once, possibly by one stimulus pattern repeated. at_least defaults to one (sections 9 and 19), and raising it makes the number go down.
"We're at 92%, so we're nearly done." The last eight points are the bins random stimulus is worst at reaching. A linear projection of the tail is 17,500 runs optimistic (section 11).
25. Interview Reasoning
"Your coverage is at 100%. Are you done?" The answer is a question about the denominator: how many specification features have no bin? A candidate who talks about the number is describing the model; one who asks what the model was measured against has understood what coverage is.
"How would you cover a 32-bit address field?" Not with automatic bins. The default gives sixty-four buckets of sixty-seven million values each (section 5), and the right answer names the equivalence classes that actually matter — aligned, unaligned, crossing a boundary, at the top of a region.
"Someone excluded forty bins to hit the deadline. What do you do?" Separate the twelve that are unreachability claims from the twenty-eight that are not, and put the reachable-bin count next to the percentage. The exclusion is not the problem; an exclusion indistinguishable from a reachability claim is.
"Coverage went down when you changed a setting. Which setting, and is that bad?" at_least (section 9). Coverage going down is the model becoming more honest — reached became exercised — and a team that treats every decrease as a regression will never raise it.
"You're at 80% after ten thousand runs and you need 100%. How many more runs?" Not 2,500. The remaining bins are the ones random stimulus is worst at, so the honest answer is a measurement — coverage gained per thousand runs, extrapolated on the actual curve — plus a decision about when to switch to directed stimulus (section 11).
26. Exercises
1. Bin a real field. Take CXL's opcode encoding and define the equivalence classes by hand, then compare the bin count against what auto_bin_max would produce. Which values does the automatic partition merge that should not be merged?
2. Enumerate a cross. For request type against response code, list the pairs that are legal, illegal and unreachable. How many bins should the cross actually declare, and how many should be illegal_bins?
3. Price a cross. Using §13's model, compute the merge cost of adding a cross that multiplies the bin count by eight, and propose two ways to afford it that do not delete the measurement.
4. Audit exclusions. Given forty exclusions, design the record that distinguishes a reachability claim from a deadline decision, and state what the flow does when the record is missing.
5. Choose at_least. For a bin representing an error-injection case, argue a threshold from first principles — what would make you believe the case was exercised rather than reached?
6. Compare model to specification. Take twenty CXL features and check each has a bin. Report the true coverage against the reported coverage, and estimate how long the full audit would take.
7. Fit a closure curve. Given coverage at 1k, 5k, 10k and 20k runs, fit the curve and project closure, then state the confidence interval and at what point directed stimulus becomes the cheaper option.
8. Find idle-bus bins. Using §20's samples-to-transactions ratio, identify which bins could have been filled by idle values and design the check that prevents it.
9. Model the merge. §13 is linear. Add a hierarchical merge and re-derive the cost for 2,000 runs and 20,000 bins. At what run count does hierarchy start paying?
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
Coverage is a percentage and a percentage has a denominator somebody chose. Three hundred bins written for four hundred specification features reports a hundred percent at a true seventy-five, and no number in the report goes down when a feature is forgotten.
Automatic bins make an equivalence claim on your behalf. Two hundred and fifty-six values in sixty-four buckets is four values per bin and 192 hidden — sixty-four values is exactly faithful, sixty-five is not.
A sum is not a product. Eight request types and four response codes is thirty-two pairs against twelve bins, and the twelve close after twelve well-chosen transactions with twenty pairs never generated.
And at two-by-two the two counts coincide. Four pairs and four separate bins — the same number, different bins, and the distinction invisible in the small example that everybody reasons about.
An exclusion made to reach closure is a hole with a name. Forty excluded against twelve unreachable is twenty-eight reachable bins removed from the denominator, and excluding all two hundred reports a hundred percent on nothing.
A weighted total hides its tail by construction. A 900-bin group covered and a 10-bin group never entered is ninety-eight percent — and the smaller the hole, the higher the number and the less likely anyone looks.
A bin hit once has been reached, not exercised. Nine hundred reached and six hundred exercised is ninety percent or sixty, and at_least defaults to one.
Sampling on the clock records the idle bus. A bus valid on a fifth of cycles gives eight thousand junk samples of ten thousand — and if an idle encoding is a legal value, a bin has been green since day one without a transaction producing it.
The last twenty points cost more than the first eighty. Ten thousand runs to eighty percent projects to 2,500 more and costs twenty thousand — 17,500 runs of difference, discovered by running the 2,500.
And the model costs four thousand minutes to merge against a hundred to sample — forty to one, which is the pressure that deletes crosses.
Seven mutations survived, the most in this batch, and five were undriven cases that cluster: an absolute value approached from one side, an exemption driven where it was not load-bearing, a boundary one point below the threshold. Two were guards dominated by an upstream clamp — the fourth chapter running — and two replacement mutations were themselves equivalent, which is the batch's sharpest process finding: a replacement mutation needs its own reachability argument, and does not inherit one.
"A hundred percent" is one property of six. The report called six of seven configurations closed when one was — and §21 is a sign-off at a hundred percent on a model whose cross had been deleted for merge time, whose exclusions were undocumented, whose threshold was a default, and eighty percent of whose samples came from an idle bus.
25.6 — CXL VIP Usage takes all five chapters of this module and puts them inside somebody else's code. Every property here — what is checked, at what scope, when it samples, how it matches, what it counts — becomes a question about a component you did not write and cannot read.
Continue learning
Related tutorials
- Related topic
CXL Protocol Verification
What a passing suite actually proves. This chapter builds rule coverage, checker reachability, vacuity, stimulus legality, checker strength, error injection, coverage closure, mutation scoring, verification cost and the assembled model.
- Related topic
Coherency Verification
A coherency bug lives in a pair of states across two caches, and most checkers look at one. This chapter builds the single-writer invariant, the data-value invariant, snoop-filter recall, the observation window, bias flushes, pair coverage, race windows, model lag, checker cost and the assembled sign-off.
- Related topic
Coverage — Evidence About What You Did Not Test
Baseline random traffic reached 12 of 16 bins, and every one of the four it missed was an ownership-boundary cross — which is exactly where every measured defect in this curriculum lives.
- 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.
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.
