Skip to content
VLSI Mentor

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

GroundOwner
Whether a rule was checked at all25.1
Whether a check was wide enough25.2
When a check samples and who is told25.3
Pairing a response to its request25.4
Integrating a commercial VIP25.6
What the coverage percentage is a percentage ofthis chapter

Deferred:

Deferred groundOwner
Vacuity and antecedent firing counts25.3 §6
Cross-cache pair spaces for coherency25.2 §10
Scoreboard keys and end-of-test drain25.4 §5 · §7
Mutation scoring as a coverage metric25.1 §13
Cryptographic primitivesout 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.

A block diagram of a coverage report's denominator. Four hundred specification features exist. Three hundred of them have a bin written in the coverage model. All three hundred are hit, so the model reports a hundred percent, while the true coverage against the specification is seventy-five percent and the hundred unmodelled features are not in the report at all.400 featuresthe specification300 modelledhave a bin100 unmodelledno bin exists100% reportedof the model75% trueof the specall hit12

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five measurements. A 64-bucket automatic maximum.

Distinct valuesExplicit bins · Automatic bins
256256 bins, 1 value each, faithful · 64 buckets, 4 values each, 192 hidden, 100% reported
3232 · 32 · 1 each — auto-binning is faithful here
6464 · 64 · exactly one each, the last faithful count
6565 · 64 buckets, one holding 2 — exactly one value hidden
00 · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five measurements.

Variables / hitsCrossed · Separate
8 × 4, 24 pairs / 12 singles32 bins, 24 hit, 75%, 8 left · 12 bins, all hit, 100%, closed
8 × 4, 32 pairs32 · 32 · 100% · closed · separate closed 20 pairs ago
1 × 44 · 4 · closed · 5 bins — a sum larger than the product
2 × 24 pairs, 3 hit, 1 left · 4 bins — the same number, different meaning
0 × 40 · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five audits. Two hundred bins, twelve genuinely unreachable.

Excluded / hitJustified · Unjustified · Reported
40 / 16012 · 28 unjustified · 100% — the permissive model reports 0 unjustified
12 / 18812 · 0 · exactly sound
13 / 18712 · 1 · one is enough to be unsound
8 / 1508 · 0 · 78% · excluding less than you could is not an abuse
200 / 012 · 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.

A block diagram of how exclusions move a coverage denominator. Two hundred bins exist. Twelve are genuinely unreachable and justifiably excluded. Twenty-eight more are excluded because the regression would not reach them, leaving a hundred and sixty reachable bins, all of which are hit, so the report reads a hundred percent.200 binsthe model12 unreachablejustified28 excludedwould not close160 reachablethe denominator100%160 of 160removedremoved12

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Seven reports. A 900-bin group unless stated.

Big group / small groupWorst group · Weighted total
900 fully covered / 100 at zero0% · 90%
900 covered / 40 at zero0% · 95% — where the weighting starts to hide it
900 covered / 10 at zero0% · 98% — a group never entered, inside a near-closed report
900 covered / 100 covered100% · 100% · closed
both half covered50% · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five measurements. A thousand bins, 900 reached at least once.

Reached enough / thresholdCounted · Shallow · Reported
600 / 10600 · 300 shallow · 60% — the one-hit model reports 900 and 90%
600 / 1600 · 300 still reached once · adequate, because one hit IS the threshold
900 / 10900 · 0 · adequate · both models count nine hundred
899 / 10899 · 1 · one shallow bin fails the depth check
no bins at all0 · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five audits. Four hundred specification features unless stated.

Modelled / hitUnmodelled · Reported · True
300 / 300100 · 100% · 75%
400 / 4000 · 100% · 100% · the model is complete
399 / 3991 · 100% · 99% — the smallest denominator a model can shrink
300 / 150100 · 50% · 37%
no features listed0 · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five projections. Ten thousand runs to eighty percent, aiming at a hundred.

Tail factor / targetGap · Linear · Real · Extra
8 / 10020 · 2,500 · 20,000 · 17,500 more than projected
8 / already at 1000 · 0 · 0 · nothing to project
1 / 10020 · 2,500 · 2,500 · the curve is the line
2 / 10020 · 2,500 · 5,000 · the smallest tail that costs anything
8 / target 70, below where we stand0, 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.

A ten-point waveform of coverage closure over increasing run counts. Coverage climbs quickly through the early runs and flattens near the target. A linear projection line continues at the early rate and reaches a hundred percent long before the actual curve does, showing the gap between the projected and real run counts.80% at 10k runs80% at 10k runslinear says donelinear says doneactually closedactually closedsampleruns_k261012151822262830actual_pct406580869093969899100linear_pct407095100100100100100100100gap_pts0515141074210closedlin_closedoverrunt0t1t2t3t4t5t6t7t8t9
Figure 3 — actual_pct climbs steeply and then flattens; linear_pct extrapolates the early slope and reaches a hundred at twelve thousand runs. The gap_pts row peaks at fifteen points and closes slowly. lin_closed goes high six samples before closed does, and overrun marks every sample on which a linear plan believes the work is finished and it is not — which is the whole span in which a schedule is being reported as green.

12. RTL 8 — Sampling On The Clock Records The Idle Bus

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Five measurements. Ten thousand cycles unless stated.

Valid cycles / idle valuesEvent sampling · Clock sampling
2,000 / 32,000 samples, all real · 10,000 samples, 8,000 junk, 80%
10,000 / 310,000 · 10,000 · both sound — the bus is never idle
9,999 / 39,999 · 1 junk — the smallest amount of idle bus recorded
2,000 / no distinct idle value2,000 · 8,000 junk · but they all land in one bin
a run of no cycles0 · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Six budgets. Twenty thousand bins, 40 million samples at 150 ns.

Runs / budgetSampling · Merging · Total
2,000 / 120 min100 min · 4,000 min · 4,100 · 3,980 over
200 / 500 min100 · 400 · exactly 500 — it fits
200, sampling free per sample / 5000 · 400 · 400
200, no bins / 500100 · 0 · 100
200, 100k samples / 5000 — fifteen seconds rounds to nothing · both models agree
200, never sampled / 5000 · 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Seven configurations.

What failsMask · Full model · Coverage report
nothing000000 · closed · closed
features with no bin — §10000010 · not closed · claims closed
variables summed, not crossed — §6000100 · not closed · claims closed
reachable bins excluded — §7001000 · not closed · claims closed
bins reached once — §9010000 · not closed · claims closed
idle-bus values recorded — §12100000 · not closed · claims closed
the report is genuinely below a hundred000001 · 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.

A flowchart for deciding whether coverage is closed. Starting from a report reading a hundred percent, the flow asks in turn whether every specification feature has a bin, whether variables are crossed rather than summed, whether every exclusion was justified, whether bins were exercised rather than merely reached, and whether the sampling event records only live values. Failing any one means the report was accepted rather than earned.noyesnoyesnoyesnoyesnoyesreport reads 100%every specfeature has abin?pairs crossed,not summed?everyexclusionjustified?binsexercised, notreached?sampling on alive event?a hole in the map— §1020 pairs unseen —§628 bins removed —§7300 reached once —§9idle bus recorded— §12closed

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.

QuantityCorrect · Broken · Ratio
Values represented by 64 automatic bins256 · 64 · 192 hidden
Bins in an 8-by-4 space32 · 12 · 2.7x
Coverage of 24 hits in that space75% · 100% · 20 pairs unseen
Bins removed by unjustified exclusion0 · 28 · 14% of the model
Coverage with a 10-deep hit threshold60% · 90% · 30 points
True coverage of a 400-feature spec75% · 100% reported · a quarter absent
Runs to close from 80%20,000 · 2,500 projected · 8x
Samples that record a live value2,000 · 2,000 of 10,000 · 80% junk
Minutes to merge 2,000 runs4,000 · 0 counted · the whole flow
Configurations called closed, of 71 · 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 classKilled by
The two bin models swapped64 buckets against 256 bins — §5 row one
Values per bin floored instead of ceiledFour per bin, not three — §5 row one
Hidden values subtracting the bin width192, not 252 — §5 row one
A value count equal to the bins called folded64 values in 64 buckets — §5 row three
The cross adding instead of multiplying32, not 12 — §6 row one
The understatement test comparing against a sumTwelve bins against a 32-pair space — §6 row one
The justified clamp taken the wrong wayTwelve justified of forty — §7 row one
No reachable bins reporting nothingA hundred percent on an empty denominator — §7 row five
Excluding exactly the unreachable called abuseTwelve excluded, twelve unreachable — §7 row two
The unweighted view taking the better groupThe worst group, not the best — §8 row one
The spread taken one way onlyA small group better covered than a big one — §8 row six
Every group closed at ninety-nineA worst group of exactly 99 — §8 row seven
The one-hit model counting the exercisedNine hundred against six hundred — §9 row one
The threshold-of-one exemption droppedA threshold of one with 300 shallow bins — §9 row two
A threshold of one counted as deepThe same row, on the error signal — §9 row two
The two denominators swappedFour hundred, not three hundred — §10 row one
The true percentage divided by the model75%, not 100% — §10 row one
The curve model dropping the tail factorTwenty thousand runs, not 2,500 — §11 row one
A tail factor of one counted as ignoredThe linear case — §11 row three
The two samplers swapped2,000 samples against 10,000 — §12 row one
The has-samples guard droppedA model that is never sampled — §13 row six
Merging scaled by a thousand4,000 minutes, not 40,000 — §13 row one
Each of the six mask bits reading a neighbourSix configurations, each failing one property alone — §14
Every counter's polarity invertedTen 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

CounterWhy 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

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.