SystemVerilog Functional Coverage: Covergroups & Bins
SystemVerilog functional coverage measures something a line- or toggle-based code-coverage report never can: whether your testbench actually exercised the scenarios that matter. Code coverage tells you a statement executed. Functional coverage tells you that a FIFO really went full while a flush was asserted, or that every opcode was issued with every addressing mode. You describe those intentions once, as a coverage model, and the simulator keeps score while tests run.
The model is built from four constructs: the covergroup container, one or more coverpoint expressions inside it, bins that bucket the values of each coverpoint, and cross relationships between coverpoints. Sampling, triggered either by a clocking event or an explicit sample() call, is what records a hit.
Covergroups and coverpoints
A covergroup is a user-defined type, much like a class. You declare it, then construct an instance with new() before anything can be recorded. Each coverpoint names an expression, usually a signal or a class field, whose values you want to track. If you write no bins, the tool generates automatic bins for you.
bit [3:0] data; bit valid; bit clk; covergroup cov_cg @(posedge clk); // sample automatically each clock coverpoint data; // 16 automatic bins, one per value coverpoint valid; // automatic bins for 0 and 1 endgroup cov_cg cg = new(); // construct it, or nothing is tracked
Here the covergroup samples on every rising clock edge. The data coverpoint receives sixteen automatic bins and valid gets two. A covergroup that is declared but never instantiated contributes nothing to the report, so the new() call is not optional.
Defining bins
Automatic bins are rarely what you want for wide signals: a 32-bit coverpoint would request four billion bins. Explicit bins let you declare only the buckets that are meaningful, give them readable names, and keep illegal or don’t-care values out of the score.
| Syntax | Meaning |
|---|---|
bins a = {5} | One bin that counts the value 5 |
bins r = {[0:15]} | One bin covering the whole range 0 to 15 |
bins s[] = {1,2,4} | A separate bin for each listed value |
bins q[4] = {[0:255]} | Range split evenly into four bins |
bins other = default | Catches values that no named bin matched |
ignore_bins x | Values removed from the coverage total |
illegal_bins e | Sampling one raises a run-time error |
bit [7:0] addr;
covergroup addr_cg;
coverpoint addr {
bins low = {[0:63]}; // one bin for a range
bins mid[4] = {[64:191]}; // split a range into 4 bins
bins high = {192, 224, 255}; // a set of specific values
bins others = default; // everything not matched above
ignore_bins reserved = {[200:210]}; // excluded from the score
illegal_bins bad = {13}; // flagged if ever sampled
}
endgroupCovergroup bins are tallied inside the simulator’s coverage database, which the free Icarus Verilog and Verilator engines do not fully implement. To see the same bookkeeping concretely, the portable model below counts hits per bin in plain SystemVerilog and reports the percentage of bins reached, which is exactly what a coverpoint computes internally. Run it and open the Simulation log tab.
module tb;
int hits [4];
int stimulus [8];
int covered;
initial begin
stimulus = '{0, 1, 2, 1, 3, 0, 2, 1};
foreach (stimulus[i]) hits[stimulus[i]]++;
covered = 0;
foreach (hits[i]) begin
$display("bin opcode==%0d : %0d hits", i, hits[i]);
if (hits[i] > 0) covered++;
end
$display("Functional coverage = %0d/4 bins hit = %0d%%", covered, covered*100/4);
if (covered == 4) $display("Coverage goal reached");
else $display("Coverage goal NOT reached");
$finish;
end
endmodulebin opcode==0 : 2 hits bin opcode==1 : 3 hits bin opcode==2 : 2 hits bin opcode==3 : 1 hits Functional coverage = 4/4 bins hit = 100% Coverage goal reached /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpa1m7cmoz/coverage_bins.sv:16: $finish called at 0 (1s)
Sampling: sample() and events
A coverage model records nothing until something tells it to look. SystemVerilog offers two triggers. Attaching a clocking event to the covergroup header, @(posedge clk), samples automatically. Omitting it and calling cg.sample() from procedural code samples on demand, which is the usual choice when coverage lives in a class-based scoreboard and you want to record a transaction only after it has been checked. Sample too early and you capture reset values; sample too late and you may miss short-lived conditions.
Cross coverage
Individual coverpoints prove each variable took every value. They do not prove the values occurred together. A cross multiplies the bins of two or more coverpoints, so you can require, for example, that every bus mode was seen with the enable both high and low.
bit [1:0] mode; bit enable; covergroup bus_cg; cp_mode: coverpoint mode; // 4 bins cp_enable: coverpoint enable; // 2 bins x_mode_en: cross cp_mode, cp_enable; // 4 x 2 = 8 cross bins endgroup
Because cp_mode has four bins and cp_enable has two, the cross defines eight combinations. Crosses grow multiplicatively, so cross only the coverpoints whose interaction is a genuine verification goal. The clocked model below walks a short stimulus stream, samples the two signals on each rising edge the way an automatic covergroup would, and tallies the four mode/size combinations.
module tb;
bit clk = 0;
int idx = 0;
bit op_vec [6];
bit sz_vec [6];
int cross_hits [2][2];
int combos;
always #5 clk = ~clk;
initial begin
op_vec = '{0, 1, 0, 1, 1, 0};
sz_vec = '{0, 0, 1, 1, 1, 0};
end
always @(posedge clk) begin
if (idx < 6) begin
cross_hits[op_vec[idx]][sz_vec[idx]] = cross_hits[op_vec[idx]][sz_vec[idx]] + 1;
idx = idx + 1;
end
else begin
combos = 0;
for (int o = 0; o < 2; o++)
for (int s = 0; s < 2; s++) begin
$display("cross op=%0d x sz=%0d : %0d hits", o, s, cross_hits[o][s]);
if (cross_hits[o][s] > 0) combos = combos + 1;
end
$display("Cross coverage = %0d/4 combinations = %0d%%", combos, combos*100/4);
$finish;
end
end
endmodulecross op=0 x sz=0 : 2 hits cross op=0 x sz=1 : 1 hits cross op=1 x sz=0 : 1 hits cross op=1 x sz=1 : 2 hits Cross coverage = 4/4 combinations = 100% /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp1ck_0v9p/cross_coverage.sv:29: $finish called at 65 (1s)
Coverage options and goals
Options tune how coverage is counted and when the group is considered done. They are set with option.* for per-instance behavior or type_option.* for the covergroup type as a whole.
| Option | Effect |
|---|---|
option.at_least = n | A bin counts only after n hits |
option.goal = n | Percentage treated as the target (default 100) |
option.weight = n | Relative weight of this point in the group total |
option.per_instance = 1 | Keep a separate score per instance |
option.auto_bin_max = n | Cap on automatic bins created |
type_option.* | Applies to the type, aggregated across instances |
bit [7:0] pkt_len;
bit clk;
covergroup pkt_cg @(posedge clk);
option.per_instance = 1; // track each instance separately
option.goal = 90; // 90% is "covered enough"
option.at_least = 2; // a bin counts only after 2 hits
len: coverpoint pkt_len {
option.weight = 3; // this point weighs 3x in the total
}
endgroupAt run time, cg.get_coverage() returns a real number from 0 to 100 for the whole group, and get_inst_coverage() returns it for one instance. The goal does not change these numbers; it defines the threshold at which the group is reported as satisfied, which lets a regression close out at, say, 90% rather than chasing unreachable corners.
Writing a coverage model that pays off
- Model intent, not implementation. Cover the specification’s named modes and corner cases, not internal flip-flops.
- Name every bin. A report full of auto-generated names is hard to act on; a hole called
fifo_full_during_flushis self-explanatory. - Keep crosses small. Exclude impossible combinations with bin selection so unreachable cross bins never distort the percentage.
- Use
illegal_binsas cheap checks. They turn forbidden values into immediate errors instead of silent misses. - Sample at the right moment. Record after a transaction is validated, so coverage reflects real, checked behavior.
- Read the holes, not the number. 95% coverage with the dangerous 5% unhit is worse than it looks; review which bins remain empty.
Quick check
Why is functional coverage needed when a test already reports 100% code coverage?
Code coverage only shows that lines or branches executed. Functional coverage shows whether the meaningful scenarios and value combinations defined in the verification plan were actually exercised, which 100% code coverage does not guarantee.
What is the difference between automatic bins and explicit bins?
Automatic bins are created by the tool, one per possible value (capped by auto_bin_max), and are impractical for wide signals. Explicit bins let you declare only the named value ranges or sets you care about, keeping the model readable and relevant.
When would you call sample() explicitly instead of using @(posedge clk) on the covergroup?
When coverage lives in class-based code such as a scoreboard and you want to record a transaction at a precise point, typically after it has been received and checked, rather than blindly on every clock edge.
If two coverpoints each hit all of their bins, is the cross between them automatically covered?
No. Individual coverpoints only prove each value occurred at some time. The cross requires that the value combinations occurred together, so cross bins can remain empty even when both coverpoints are fully covered.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses