Closing holes

Read a coverage report, decide whether each hole is a stimulus gap or a model mistake, and close the real holes with constraints aimed at them.

Lesson
C7
Time
25 min
Runs on
Coverage build
Check
5 questions

A team needed its last two coverage holes closed before tape-out and tripled the length of every random test. A week of simulation later, the holes were still open. Both holes needed a single-beat packet, which the random stimulus produced in only a few percent of packets, and more of the same stimulus could not change that.

Coverage over rounds of stimulus
0123round1234stimulus10 random+ aimed-coverage (%)90.48100holes20two aimed packets close both holes

The example: ten random packets leave two holes, and one packet aimed at each closes them.

Read the report before adding stimulus

A coverage report lists every bin with its hit count. The total says how far you are; the empty bins say where to go. Start every closure effort from the list of holes, not from the percentage.

Cross cellHitsStatus
READ / one0hole
READ / short2covered
READ / long1covered
WRITE / one0hole
WRITE / short2covered
WRITE / long4covered
CONFIG / one1covered
CONFIG / short, long-ignored: impossible

The cross after 10 random packets in the example, as a report would show it. Coverage 90.48%.

Each hole has one of three causes, and each cause has its own fix. The case may be impossible, in which case the model is wrong and the cell needs an ignore_bins. The case may be possible but rare under the current stimulus, which calls for aimed constraints. Or the stimulus may never produce it at all, because of a constraint or a missing feature, which is a bug in the testbench.

Why more random stimulus is slow

Random stimulus covers likely cases quickly and rare cases slowly. When the constraints allow 17 combinations of kind and length with equal chance, a single-beat READ is one of them, so it appears in about 1 packet in 17. Common cells fill in the first few dozen packets, and the last rare one can take hundreds more.

Holes that close in some seeds and stay open in others are a warning sign. A bin that depends on luck should be made reliable with a dist before the model is called complete.

Module 5 showed how solve order and weights shape those chances. The coverage report tells you which chances need changing; the constraints are how you change them.

Aim constraints at the holes

The direct fix is a packet aimed at each hole: randomize() with { kind == WRITE; len == 1; } produces exactly the missing combination, while every other field stays random and legal. The example finds the holes, aims one packet at each, and reaches 100%.

For holes that should appear naturally in every regression, change the distribution instead, with a dist such as len dist {1 := 3, [2:4] :/ 3, [5:8] :/ 3}, which makes single beats as common as each burst class. A dist keeps the stimulus random while moving the probabilities toward the cases the model asks for.

  1. List the holes

    Read the empty bins from the report, not just the total.

  2. Classify each hole

    Impossible: fix the model with ignore_bins. Possible: decide whether it is rare or unreachable.

  3. Aim or reweight

    Rare: adjust a dist. Needed now: aim a packet with an inline constraint. Unreachable: find the constraint or feature that blocks it.

  4. Rerun and recheck

    Confirm the hole is closed and that no check failed on the new stimulus.

A closed hole must be checked

A hole is closed only when the scoreboard has checked the packet that filled it. Aimed stimulus that reaches a bin while the checker is disconnected closes nothing, so keep every aimed packet inside the normal checked flow.

Aimed packets are directed tests in disguise, so keep them few. Each one should name the hole it closes, in a comment or a message, so a later reader knows why it exists. After closing holes, rerun with several new seeds to check that the total stays at 100% without the aimed packets doing all the work.

Record how each hole was closed. A dist change helps every future regression, while a one-off aimed packet must stay in the test list, or the next change to the constraints can quietly reopen the hole.

Sometimes the honest answer is that a case is not worth covering, for example a combination the product will never use. Then remove it from the model in a review, with the reason written next to the ignore_bins, never by quietly lowering the goal.

Count holes across the whole regression, not just per test. Merged coverage over every seed is what a sign-off review looks at, and it shows which holes no seed ever reached.

Try it

Ten random packets leave the two single-beat cells for READ and WRITE empty. The second round reads the holes from its own hit table and aims one packet at each, with an inline constraint on kind and length class.

Change repeat (10) to repeat (100) and predict whether round 1 still finds holes. The example keeps its own hit table because the coverage build cannot report a single bin's count; a simulator's coverage report lists the same cells.

RUNVerilator (coverage build) ยท simulator service
Edit the code, then run it
Expected: round 1: 10 random packets, 90.48%, holes: READ/one WRITE/one round 2: aimed packets added, 100.00%, holes: none

Check yourself

Predict the outputFive packets are sampled into the packet model, which has the 7 legal cross cells. Predict the exact output.
typedef enum bit [1:0] {READ, WRITE, CONFIG} kind_e;

// One packet on the link. CONFIG packets carry a single beat.
class packet;
  rand kind_e    kind;
  rand bit [3:0] len;              // payload beats, 1 to 8
  constraint legal {
    kind inside {READ, WRITE, CONFIG};
    len inside {[1:8]};
    kind == CONFIG -> len == 1;
  }
endclass

covergroup pkt_cross with function sample(packet p);
  option.per_instance = 1;
  cp_kind: coverpoint p.kind;
  cp_len:  coverpoint p.len { bins one = {1}; bins short_ = {[2:4]}; bins long_ = {[5:8]}; }
  kind_x_len: cross cp_kind, cp_len {
    ignore_bins config_multi = binsof(cp_kind) intersect {CONFIG} && !binsof(cp_len.one);
  }
endgroup


module tb;
  pkt_cross cov = new();
  function automatic packet make(kind_e k, bit [3:0] len);
    packet p = new();
    p.kind = k; p.len = len;
    return p;
  endfunction
  initial begin
    cov.sample(make(READ, 3));
    cov.sample(make(READ, 7));
    cov.sample(make(WRITE, 2));
    cov.sample(make(WRITE, 8));
    cov.sample(make(CONFIG, 1));
    $display("coverage=%0.2f", cov.get_inst_coverage());
  end
endmodule
Spot the bugThe only hole left is WRITE/one. Each option is the inline constraint used to aim one packet at it. Which option never closes the hole?
ConceptAfter a week of longer random runs, the hole CONFIG/long is still empty. What is the most likely explanation?
ConceptWhy is len dist {1 := 3, [2:4] :/ 3, [5:8] :/ 3} better than one aimed packet for a hole that every regression should hit?
ConceptCoverage reached 100% after aimed packets were added, but the scoreboard was disabled for those runs. Is the hole closed?

Remember

  • Start closure from the list of empty bins, not from the total.
  • Classify each hole: impossible (fix the model), rare (reweight), or unreachable (fix the testbench).
  • Aim a packet at a hole with an inline constraint, or reweight with dist so every run reaches it.
  • A hole is closed only when the stimulus that filled it was also checked.