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.
| Survey | SystemVerilog | Job |
|---|---|---|
| The clipboard | covergroup | Holds everything, created with new() |
| Looking up and making a mark | sample() or a clock event | Decides when something counts |
| A column, such as vehicle type | coverpoint | Decides what is watched |
| A box in that column | bins | One thing you want to see at least once |
| The type-by-hour grid | cross | Two things seen together |
| Boxes crossed out: no buses after midnight | ignore_bins | Impossible cases that should not count |
| Tanks on the road: call the police | illegal_bins | Must 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 callsample()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 sample | Code | Good for | Trap |
|---|---|---|---|
| Clock event | covergroup cg @(posedge clk); | Signals in a module or interface | Samples every clock, idle cycles too |
| Explicit call | cg.sample(); | Transactions in a class or monitor | Forget 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 kind | Syntax | What it makes | Analogy |
|---|---|---|---|
| Auto | coverpoint wide; | Up to 64 bins (auto_bin_max), values shared out evenly | A shelf pre-split into equal boxes |
| Single | bins med = {[4:12]}; | 1 bin, hit by any value in the set | One box for medium |
| Array | bins low[] = {[0:3]}; | 1 bin per value (here 4) | One box per item |
| Transition | bins up = (0 => 1 => 2); | Hit when values arrive in that order | A route, not a stop |
| Repetition | bins hold3 = (3[*3]); | Hit when 3 is seen on 3 samples in a row | Standing still at one stop |
| Wildcard | wildcard bins hi = {4'b11??}; | ? matches 0 or 1 | A postcode prefix |
| Ignore | ignore_bins r = {6, 7}; | Removes values; they never count | Crossing items off the list |
| Illegal | illegal_bins bad = {[8:15]}; | Error if hit; never counts | An 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_xis 3 of 5 bins. Values 5 and 6 land in the same bin, so together they add only one.cp_widehas 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_binsusingbinsof(...) intersect {...}, or, since IEEE 1800-2012, awithexpression 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.
| Option | What it does |
|---|---|
option.per_instance = 1 | Keep and report each instance separately |
type_option.merge_instances = 1 | Type coverage = union of all instances’ bins; 0 means weighted average, per IEEE 1800 |
option.at_least = N | A bin needs N hits to count (default 1) |
option.auto_bin_max = N | Cap on auto bins (default 64) |
option.weight, option.goal | How much this group counts toward the total, and its target % |
option.comment | A 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%
- 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
iffor callsample()from the monitor (Example 5). - Name everything.
cp_len.short_lenreads better in a report thanauto[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 anifinwrite(); without theifit 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.
- Fix A: print the sampled value right next to
- 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
bwas X.
- Crash at sample(). A fatal bad-handle error pointed at a coverpoint line (thread).
- Fix A: build the covergroup with
new()before the firstsample(). - Fix B: check the handle you pass in. In that thread the real culprit was a null transaction sent through
write().
- Fix A: build the covergroup with
- Auto cross bins will not go away. Named cross bins plus
ignore_binsjoined 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).
- Fix A: use
- 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()withoption.per_instance = 1when you care about one instance. - Fix B: set
type_option.merge_instanceson purpose, so the type number means what you think.
- Fix A: use
- 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.
- Fix A:
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
- IEEE Std 1800-2017, clause 19: functional coverage
- Unraveling the Complexities of Functional Coverage, Thomas Ellis and Rohit Jain, Verification Academy
- Verification Academy forum threads linked in the mistakes section
Read next
- AI-written vs hand-written UVM testbench: a green log that hid 2 bugs, and the coverpoint that would have shown them
- Types of coverage metrics: code vs functional coverage in depth
- Functional coverage options in SystemVerilog
- Functional coverage guidelines for implementers
- UVM testbench architecture: where the coverage subscriber sits
- SoC verification flow: coverage closure as a sign-off gate
Part of the learning path Functional coverage

nicely explained. Good luck!!
Thanks Hitesh 🙂