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.

SyntaxMeaning
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 = defaultCatches values that no named bin matched
ignore_bins xValues removed from the coverage total
illegal_bins eSampling 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
  }
endgroup

Covergroup 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
endmodule
bin 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
endmodule
cross 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.

OptionEffect
option.at_least = nA bin counts only after n hits
option.goal = nPercentage treated as the target (default 100)
option.weight = nRelative weight of this point in the group total
option.per_instance = 1Keep a separate score per instance
option.auto_bin_max = nCap 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
  }
endgroup

At 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_flush is self-explanatory.
  • Keep crosses small. Exclude impossible combinations with bin selection so unreachable cross bins never distort the percentage.
  • Use illegal_bins as 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.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD