Functional coverage in SystemVerilog: a covergroup with two coverpoints, their bins and their cross, with bins hit, not hit and ignored
|

Functional Coverage in SystemVerilog: Covergroups, Bins, Cross

In short: Functional coverage tells you which parts of the spec your tests actually exercised. In SystemVerilog you write it as a covergroup: coverpoints pick the values to watch, bins sort them into buckets, and crosses check that two things happened together. This guide builds each piece with small examples and real Synopsys VCS output, so you can see exactly how the coverage number moves.

A test that passes only proves the cases it ran. Functional coverage answers the next question: which cases did it run? Code coverage is collected for you from the RTL. Functional coverage you write yourself, from the spec and the verification plan, so it can tell you a feature was never tried even when every line of RTL was executed. More on the difference in types of coverage metrics.

Every example below comes from one small file, cov_demo.sv, run on Synopsys VCS X-2025.06-SP1. The numbers come straight from get_coverage() and get_inst_coverage(), so you do not need a report tool to follow along. The full file is at the end.

The big picture: a tally sheet for your spec

Think of a traffic survey. Someone stands at a junction with a clipboard and makes a mark each time a vehicle passes. The clipboard is the covergroup, each column (cars, buses, bikes) is a coverpoint, each box you tick is a bin, and a grid of “vehicle type by hour” is a cross.

SurveySystemVerilogJob
The clipboardcovergroupHolds everything, created with new()
Looking up and making a marksample() or a clock eventDecides when something counts
A column, such as vehicle typecoverpointDecides what is watched
A box in that columnbinsOne thing you want to see at least once
The type-by-hour gridcrossTwo things seen together
Boxes crossed out: no buses after midnightignore_binsImpossible cases that should not count
Tanks on the road: call the policeillegal_binsMust never happen; error if it does

The picture at the top of this page puts it all together using Example 4 below. Coverage % is simply bins hit divided by bins that count.

Covergroups and sampling

A covergroup only counts what it samples, so when it samples matters as much as what it watches.

  • What it is: a type, like a class. Declare it once, create it with new(), then call sample() or give it a clock event.
  • Analogy: a tally sheet on a clipboard. It does nothing until someone looks at the road and makes a mark.
  • Where it lives: a module, interface, class or package. In UVM it usually sits in a subscriber fed by the monitor; see UVM testbench architecture.
Way to sampleCodeGood forTrap
Clock eventcovergroup cg @(posedge clk);Signals in a module or interfaceSamples every clock, idle cycles too
Explicit callcg.sample();Transactions in a class or monitorForget to call it, or call it before new(), and you get 0% or a null-handle crash

Example 5: the idle-cycle trap

Ten clocks, only two real transfers (cmd 3 and 5). The bus sits at cmd = 0 when idle.

covergroup cg_clocked @(posedge clk);                        // samples EVERY clock
  cp_cmd: coverpoint cmd { bins c[] = {[0:7]}; }
endgroup
covergroup cg_valid @(posedge clk);
  cp_cmd: coverpoint cmd iff (valid) { bins c[] = {[0:7]}; }  // only real transfers
endgroup
--- 5. clocked vs explicit sampling
every clock   37.50%  (counts the idle cmd 0 as covered)
iff (valid)   25.00%  (only the 2 real transfers)

The first number looks better, and it is wrong: cmd = 0 was never actually sent. Gate the coverpoint with iff (valid), or call sample() only on real transfers.

Example 7: sample() with an argument

When the value lives in a transaction rather than a variable the covergroup can see, pass it in:

covergroup cg_len with function sample(int unsigned len);
  option.at_least = 2;                                       // a bin needs 2 hits to count
  cp_len: coverpoint len { bins short_len = {[1:64]}; bins long_len = {[65:1500]}; }
endgroup
...
c7.sample(40); c7.sample(1000);   // each bin hit once, needs 2
c7.sample(12);                    // short_len now has 2 hits
--- 7. at_least = 2, sample() with an argument
after 40, 1000      type   0.00%  inst   0.00%
after 40, 1000, 12  type  50.00%  inst   0.00%

The type number does what at_least = 2 promises. The instance number stays at 0% because this covergroup does not turn on option.per_instance; Example 6 explains why.

Bins: the buckets that make the number

The bins you write decide what 100% means.

Bin kindSyntaxWhat it makesAnalogy
Autocoverpoint wide;Up to 64 bins (auto_bin_max), values shared out evenlyA shelf pre-split into equal boxes
Singlebins med = {[4:12]};1 bin, hit by any value in the setOne box for medium
Arraybins low[] = {[0:3]};1 bin per value (here 4)One box per item
Transitionbins up = (0 => 1 => 2);Hit when values arrive in that orderA route, not a stop
Repetitionbins hold3 = (3[*3]);Hit when 3 is seen on 3 samples in a rowStanding still at one stop
Wildcardwildcard bins hi = {4'b11??};? matches 0 or 1A postcode prefix
Ignoreignore_bins r = {6, 7};Removes values; they never countCrossing items off the list
Illegalillegal_bins bad = {[8:15]};Error if hit; never countsAn alarm on the door

Repetition is only legal inside a transition bin, so it is written (3[*3]) with parentheses, never {3[*3]}.

Example 1: how many bins?

bit [3:0] var_x;  bit [7:0] wide;
covergroup cg_bins;
  cp_x:    coverpoint var_x { bins low_bins[] = {[0:3]};    // 4 bins: 0,1,2,3
                              bins med_bins   = {[4:12]}; } // 1 bin for 4..12
  cp_wide: coverpoint wide;                                 // auto bins
endgroup

Sampled var_x = 0, 1, 5, 6 and wide = 0 to 15:

--- 1. bins count
cp_x     60.00%  (expect 3 of 5 bins: 0, 1 and med)
cp_wide   6.25%  (values 0..15 hit; how many auto bins?)
  • cp_x is 3 of 5 bins. Values 5 and 6 land in the same bin, so together they add only one.
  • cp_wide has 256 values in 64 auto bins of 4 values each. Sixteen values fill 4 bins: 4 / 64 = 6.25%.

Example 2: ignore vs illegal

cp_op: coverpoint opcode { bins         ops[]    = {[0:7]};
                           ignore_bins  reserved = {6, 7};
                           illegal_bins bad      = {[8:15]}; }

After sampling 0 to 7, cp_op reads 100.00%: ops[] loses 6 and 7 and keeps 6 bins, all hit. Then the test samples 9:

Error-[FCIBH] Illegal bin hit
  VERIFICATION ERROR (FUNCTIONAL COVERAGE) : At time 100 ns, Illegal state bin
  bad of coverpoint cp_op in covergroup cov_demo::cg_ops got hit with value
  0x9
  Covergroup Instance: c2

On VCS this stopped the simulation (exit code 1). The line after sample() never printed. Put illegal bins only on values that must truly never happen, and expect the run to end when one is hit.

Example 3: transitions and wildcard

cp_st: coverpoint state { bins up        = (0 => 1 => 2);
                          bins hold3     = (3[*3]);
                          bins to_zero[] = (1, 2, 3 => 0); }   // 3 bins: 1=>0, 2=>0, 3=>0
cp_ad: coverpoint addr  { wildcard bins top_quarter = {4'b11??}; }
--- 3. transitions + wildcard
cp_st    80.00%  (sequence 0 1 2 3 3 3 0 1 0)
cp_ad   100.00%  (addr 4'b1101 vs wildcard 11??)

Four of five bins: up, hold3, 3=>0 and 1=>0. The sequence never goes 2=>0.

Cross coverage

A cross checks that two things happened together. Bins multiply, so trim the impossible ones.

  • Analogy: a timetable grid. Every route times every hour; some cells are “no service” and should not count against you.
  • Rule: you can only cross coverpoints (or variables) of the same covergroup, never single bins.
  • Trim with: ignore_bins using binsof(...) intersect {...}, or, since IEEE 1800-2012, a with expression on the cross.

Example 4

bit [1:0] a, b;
covergroup cg_cross;
  cp_a: coverpoint a;                                       // 4 auto bins
  cp_b: coverpoint b;                                       // 4 auto bins
  ab:   cross cp_a, cp_b {
    ignore_bins no_b1 = binsof(cp_b) intersect {1} &&
                        binsof(cp_a) intersect {[1:3]};     // drop 3 of the 16
  }
endgroup

Every a (0 to 3) with b = 0 and 1:

--- 4. cross
cp_a 100.00%  cp_b  50.00%  ab  38.46%  (a 0..3, b 0..1)
--- total for cg_cross (type):  62.82%
  • ab: 16 cells minus 3 ignored = 13 that count. Hit: (0,0), (1,0), (2,0), (3,0), (0,1). 5 / 13 = 38.46%.
  • The covergroup total is the average of its three items: (100 + 50 + 38.46) / 3 = 62.82%. Each coverpoint and cross has weight 1 by default.

Both coverpoints look healthy on their own. Only the cross shows that 8 of the 13 combinations were never tried.

Options, and reading the number right

The same samples can give different numbers depending on whether you ask about the covergroup type or one instance.

OptionWhat it does
option.per_instance = 1Keep and report each instance separately
type_option.merge_instances = 1Type coverage = union of all instances’ bins; 0 means weighted average, per IEEE 1800
option.at_least = NA bin needs N hits to count (default 1)
option.auto_bin_max = NCap on auto bins (default 64)
option.weight, option.goalHow much this group counts toward the total, and its target %
option.commentA label in the report

All options with examples: functional coverage options in SystemVerilog.

Example 6: type vs instance, the forum puzzle

A Verification Academy user had two instances, one sampling (0,0) and the other (1,1). get_coverage() said 100% while the report showed 50% per instance, and the thread never settled why. The test file repeats that setup three ways:

covergroup cg_default;                      // options left at default
  cp_x: coverpoint x; cp_y: coverpoint y;
endgroup
covergroup cg_inst;
  option.per_instance = 1;
  cp_x: coverpoint x; cp_y: coverpoint y;
endgroup
covergroup cg_merge;
  option.per_instance = 1;
  type_option.merge_instances = 1;
  cp_x: coverpoint x; cp_y: coverpoint y;
endgroup
...
p0.hit(0, 0);   p1.hit(1, 1);
// get_coverage() = the TYPE, get_inst_coverage() = this instance
--- 6. per instance vs per type
default : type 100.00%  p0 inst   0.00%  p1 inst   0.00%
per_inst: type 100.00%  p0 inst  50.00%  p1 inst  50.00%
merge   : type 100.00%  p0 inst 100.00%  p1 inst 100.00%
get_coverage versus get_inst_coverage on VCS: two instances each hit 2 of 4 bins; type coverage reads 100% in all three option settings, instance coverage reads 0%, 50% or 100%
Example 6 on VCS: what each option setting reports.
  • Defaults: get_inst_coverage() returns 0%. VCS keeps no per-instance data unless you ask for it.
  • per_instance = 1: each instance shows its own 50%, the number the forum user saw in the report.
  • Type number: 100% in all three, the union. IEEE 1800 says that with merge_instances = 0 (the default) it should be the weighted average, 50% here, so another simulator may show 50%. That gap is the forum puzzle.
  • merge_instances = 1: VCS also returned 100% for each instance, so the instance numbers stop telling instances apart.

The rule: to judge one instance, set option.per_instance = 1 and read get_inst_coverage(). Use get_coverage() for the total, and check on your own tool what it merges.

One more VCS note: cg_cross::get_coverage(), which IEEE 1800 allows, fails on VCS X-2025.06 with “Accessing covergroup members using scope resolution operator is not yet supported”. Call get_coverage() on an instance instead.

Coverage you can trust

  • Map every bin to the plan. A bin nobody asked for is noise; a plan item with no bin is a hole.
  • Turn coverage on when the design is ready. Sampling during reset or bring-up fills bins with junk.
  • Only merge passing runs. A failing test still hits bins, and that coverage proves nothing.
  • Sample real transfers only. Gate with iff or call sample() from the monitor (Example 5).
  • Name everything. cp_len.short_len reads better in a report than auto[0:63].
  • Close holes on purpose. For each empty bin, decide: new test, new constraint, or a reviewed ignore_bins.

The longer version of this list is in functional coverage guidelines for implementers.

Common mistakes, from real forum threads

Retold in our own words, each with more than one fix.

  • 0% even though sample() is called. A user’s covergroup stayed at 0% once sample() sat inside an if in write(); without the if it reached 100% (thread).
    • Fix A: print the sampled value right next to sample(). If the value never matches a bin, the bins are wrong, not the call.
    • Fix B: check what the bin compares against. Bin values are fixed when the covergroup is constructed, not at sample time.
  • Sampling in the wrong place. Someone sampled from the scoreboard and never saw b = 0 (thread).
    • Fix A: sample from the monitor, or a subscriber fed by it, so you cover what really happened on the bus.
    • Fix B: look at the waves. In that thread b was X.
  • Crash at sample(). A fatal bad-handle error pointed at a coverpoint line (thread).
    • Fix A: build the covergroup with new() before the first sample().
    • Fix B: check the handle you pass in. In that thread the real culprit was a null transaction sent through write().
  • Auto cross bins will not go away. Named cross bins plus ignore_bins joined with && left the auto bins in place (thread).
    • Fix A: use || when you mean “either of these”.
    • Fix B: split the bins you want into their own coverpoints and cross those.
    • Fix C: since 1800-2012, ignore_bins x = cross_name with (...) (another thread).
  • Crossing single bins. Not allowed; only coverpoints can be crossed (thread).
    • Fix A: cross the full coverpoints and ignore what you do not need.
    • Fix B: make a coverpoint that holds only the wanted bins, and cross that.
  • Reading type coverage as instance coverage. Example 6 above (thread).
    • Fix A: use get_inst_coverage() with option.per_instance = 1 when you care about one instance.
    • Fix B: set type_option.merge_instances on purpose, so the type number means what you think.
  • Counting idle cycles. A clocked covergroup samples every edge (Example 5).
    • Fix A: iff (valid) on the coverpoint.
    • Fix B: drop the clock event and call sample() on real transfers only.

Frequently asked questions

What is functional coverage in SystemVerilog?

A measure of which spec features your tests exercised. You write it yourself as covergroups, coverpoints, bins and crosses, and the simulator counts what was hit.

What is the difference between code coverage and functional coverage?

Code coverage is collected automatically from the RTL and shows which lines, branches and toggles ran. Functional coverage comes from the spec and the plan. You need both: high code coverage with low functional coverage means features were never tried.

How many bins does bins b[] = {[0:3]} create?

Four, one per value. Without the [] it is one bin that any of the four values can hit.

What is the difference between ignore_bins and illegal_bins?

Ignored values are removed and never count. Illegal values never count either, and hitting one is an error; on VCS it stopped the simulation.

What is the difference between a covergroup and a cover property?

A covergroup counts values and combinations. A cover property counts a sequence of events over time, written with the same syntax as an assertion.

Why is my cross coverage so low?

Bins multiply. A 4 by 4 cross has 16 bins. Ignore the impossible combinations so the number shows real gaps, not cells that can never be hit.

What is the difference between get_coverage and get_inst_coverage?

get_coverage() reports the covergroup type across all instances. get_inst_coverage() reports one instance, and on VCS it reads 0% unless option.per_instance = 1.

When should a covergroup sample?

On real transfers only: from the monitor, or with iff (valid) on a clocked covergroup.

Full code

The whole test file, all seven examples in one run:

// cov_demo.sv : functional coverage, one small example per idea
// Run: vcs -full64 -sverilog cov_demo.sv -R
// Every number below comes from get_coverage() / get_inst_coverage(), no report tool needed.
`timescale 1ns/1ns
module cov_demo;

  // ---------------------------------------------------------------- 1. bins count
  bit [3:0] var_x;
  bit [7:0] wide;
  covergroup cg_bins;
    cp_x:    coverpoint var_x { bins low_bins[] = {[0:3]};    // 4 bins: 0,1,2,3
                                bins med_bins   = {[4:12]}; } // 1 bin for 4..12
    cp_wide: coverpoint wide;                                 // auto bins
  endgroup

  // ---------------------------------------------------------------- 2. ignore vs illegal
  bit [3:0] opcode;
  covergroup cg_ops;
    cp_op: coverpoint opcode { bins         ops[]    = {[0:7]};
                               ignore_bins  reserved = {6, 7};
                               illegal_bins bad      = {[8:15]}; }
  endgroup

  // ---------------------------------------------------------------- 3. transitions + wildcard
  bit [1:0] state;
  bit [3:0] addr;
  covergroup cg_trans;
    cp_st: coverpoint state { bins up        = (0 => 1 => 2);
                              bins hold3     = (3[*3]);        // 3 on three samples in a row
                              bins to_zero[] = (1, 2, 3 => 0); }
    cp_ad: coverpoint addr  { wildcard bins top_quarter = {4'b11??}; }
  endgroup

  // ---------------------------------------------------------------- 4. cross
  bit [1:0] a, b;
  covergroup cg_cross;
    cp_a: coverpoint a;                                       // 4 auto bins
    cp_b: coverpoint b;                                       // 4 auto bins
    ab:   cross cp_a, cp_b {
      ignore_bins no_b1 = binsof(cp_b) intersect {1} &&
                          binsof(cp_a) intersect {[1:3]};     // drop 3 of the 16
    }
  endgroup

  // ---------------------------------------------------------------- 5. clocked vs explicit sampling
  bit clk, valid;
  bit [2:0] cmd;
  covergroup cg_clocked @(posedge clk);                       // samples EVERY clock
    cp_cmd: coverpoint cmd { bins c[] = {[0:7]}; }
  endgroup
  covergroup cg_valid @(posedge clk);
    cp_cmd: coverpoint cmd iff (valid) { bins c[] = {[0:7]}; } // only real transfers
  endgroup

  // ---------------------------------------------------------------- 6. per instance vs per type
  class pkt_cov;
    bit x, y;
    covergroup cg_default;                                    // all options left at default
      cp_x: coverpoint x; cp_y: coverpoint y;
    endgroup
    covergroup cg_inst;
      option.per_instance = 1;
      cp_x: coverpoint x; cp_y: coverpoint y;
    endgroup
    covergroup cg_merge;
      option.per_instance = 1;
      type_option.merge_instances = 1;
      cp_x: coverpoint x; cp_y: coverpoint y;
    endgroup
    function new(); cg_default = new(); cg_inst = new(); cg_merge = new(); endfunction
    function void hit(bit vx, bit vy);
      x = vx; y = vy; cg_default.sample(); cg_inst.sample(); cg_merge.sample();
    endfunction
  endclass

  // ---------------------------------------------------------------- 7. at_least + sample() with arguments
  covergroup cg_len with function sample(int unsigned len);
    option.at_least = 2;                                      // a bin needs 2 hits to count
    cp_len: coverpoint len { bins short_len = {[1:64]}; bins long_len = {[65:1500]}; }
  endgroup

  // ================================================================ run
  cg_bins c1 = new(); cg_ops c2 = new(); cg_trans c3 = new(); cg_cross c4 = new();
  cg_clocked c5 = new(); cg_valid c6 = new(); cg_len c7 = new();
  pkt_cov p0, p1;
  int vals_x[]  = '{0, 1, 5, 6};
  int vals_st[] = '{0, 1, 2, 3, 3, 3, 0, 1, 0};

  initial begin
    $display("\n--- 1. bins count");
    foreach (vals_x[i]) begin var_x = vals_x[i]; wide = i; c1.sample(); end
    for (int v = 4; v < 16; v++) begin wide = v; c1.sample(); end  // wide got 0..15
    $display("cp_x    %6.2f%%  (expect 3 of 5 bins: 0, 1 and med)", c1.cp_x.get_coverage());
    $display("cp_wide %6.2f%%  (values 0..15 hit; how many auto bins?)", c1.cp_wide.get_coverage());

    $display("\n--- 2. ignore vs illegal");
    for (int v = 0; v < 8; v++) begin opcode = v; c2.sample(); end  // 6 and 7 are ignored
    $display("cp_op   %6.2f%%  after 0..7", c2.cp_op.get_coverage());
    // the illegal value is sampled at the very end: on VCS it stops the run

    $display("\n--- 3. transitions + wildcard");
    foreach (vals_st[i]) begin state = vals_st[i]; addr = 4'b1101; c3.sample(); end
    $display("cp_st   %6.2f%%  (sequence 0 1 2 3 3 3 0 1 0)", c3.cp_st.get_coverage());
    $display("cp_ad   %6.2f%%  (addr 4'b1101 vs wildcard 11??)", c3.cp_ad.get_coverage());

    $display("\n--- 4. cross");
    for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) begin a = i; b = j; c4.sample(); end
    $display("cp_a %6.2f%%  cp_b %6.2f%%  ab %6.2f%%  (a 0..3, b 0..1)",
             c4.cp_a.get_coverage(), c4.cp_b.get_coverage(), c4.ab.get_coverage());

    $display("\n--- 5. clocked vs explicit sampling");
    // 10 clocks; only 2 are real transfers (cmd 3 and 5). Bus idles with cmd = 0.
    for (int t = 0; t < 10; t++) begin
      valid = (t == 3 || t == 7);
      cmd   = (t == 3) ? 3 : (t == 7) ? 5 : 0;
      #5 clk = 1; #5 clk = 0;
    end
    $display("every clock  %6.2f%%  (counts the idle cmd 0 as covered)", c5.cp_cmd.get_coverage());
    $display("iff (valid)  %6.2f%%  (only the 2 real transfers)", c6.cp_cmd.get_coverage());

    $display("\n--- 6. per instance vs per type");
    p0 = new(); p1 = new();
    p0.hit(0, 0);                                     // p0 sees x=0, y=0
    p1.hit(1, 1);                                     // p1 sees x=1, y=1
    // get_coverage() = coverage of the TYPE, get_inst_coverage() = this instance
    $display("default : type %6.2f%%  p0 inst %6.2f%%  p1 inst %6.2f%%",
      p0.cg_default.get_coverage(), p0.cg_default.get_inst_coverage(), p1.cg_default.get_inst_coverage());
    $display("per_inst: type %6.2f%%  p0 inst %6.2f%%  p1 inst %6.2f%%",
      p0.cg_inst.get_coverage(), p0.cg_inst.get_inst_coverage(), p1.cg_inst.get_inst_coverage());
    $display("merge   : type %6.2f%%  p0 inst %6.2f%%  p1 inst %6.2f%%",
      p0.cg_merge.get_coverage(), p0.cg_merge.get_inst_coverage(), p1.cg_merge.get_inst_coverage());

    $display("\n--- 7. at_least = 2, sample() with an argument");
    c7.sample(40); c7.sample(1000);
    $display("after 40, 1000      type %6.2f%%  inst %6.2f%%", c7.get_coverage(), c7.get_inst_coverage());
    c7.sample(12);
    $display("after 40, 1000, 12  type %6.2f%%  inst %6.2f%%", c7.get_coverage(), c7.get_inst_coverage());

    $display("\n--- total for cg_cross (type): %6.2f%%", c4.get_coverage());

    $display("\n--- 2b. now sample the illegal opcode 9");
    opcode = 9; c2.sample();                                          // illegal
    $display("still running after the illegal bin, cp_op %6.2f%%", c2.cp_op.get_coverage());
    $finish;
  end
endmodule
vcs -full64 -sverilog cov_demo.sv -R

References

Read next

2 Comments

Comments are closed.