Skip to content

SystemVerilog · Module 11

Covergroups & Coverpoints

The two coverage primitives — covergroup as a class-like type, coverpoint as its measurable field. Three coverpoint forms, label semantics, the four legal scopes, the built-in method API, and the constructor-omission bug every team ships at least once.

Module 11 · Page 11.2

The Two Coverage Primitives

A covergroup is a user-defined type that encapsulates the coverage for one area of interest. A coverpoint is a tracked variable or expression inside that covergroup, partitioned into bins. Everything else in Module 11 — automatic bins, named bins, wildcard/illegal/ignore bins, transitions, crosses, options, sampling events — is mechanics layered on top of these two primitives.

A useful mental model: the covergroup is a class (a type that is instantiated, that owns per-instance state, that exposes built-in methods); the coverpoint is a member of that class (a tracked field with bins as its categories). Once you carry that picture, the rest of the syntax follows the rules you already know about SystemVerilog classes.

Syntax — Define, Instantiate, Sample, Query

A covergroup goes through four lifecycle phases. Every one of them must be present for coverage to land in the database.

SystemVerilog — covergroup lifecycle, in order
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. Define the covergroup TYPE ─────────────────────────────────
covergroup cg_name @(sampling_event);   // auto-sampled on event
    coverpoint variable_name;
endgroup
 
// Alternate form — manual sampling, no event
covergroup cg_manual;
    coverpoint variable_name;
endgroup
 
// ── 2. Instantiate — create the OBJECT ────────────────────────────
cg_name   cg_inst   = new();
cg_manual cg_manual_inst = new();
 
// ── 3. Sample — record current values into the bins ───────────────
cg_manual_inst.sample();          // manual sampling
// cg_inst is sampled automatically on @(sampling_event)
 
// ── 4. Query — read the percentage at any point in the simulation ─
real pct = cg_manual_inst.get_inst_coverage();
$display("[cg_manual] coverage = %0.2f%%", pct);

What this code does. Declares two covergroups — one auto-sampled on an event, one manually sampled — constructs each into an instance, fires a manual sample, and queries the per-instance percentage at simulation time.

Synthesisability. Simulation-only, every line. Covergroups are an instrument; synthesis tools strip them. Putting a covergroup inside synthesisable RTL is harmless (the synth tool ignores it) but it is a code-organisation smell — coverage belongs in the verification environment, not in the DUT.

A Complete Working Example — APB Transaction Coverage

The pattern below is the canonical class-based coverage shape. A transaction class owns the data being measured, embeds the covergroup that measures it, constructs the covergroup in new(), and exposes a sampling hook the monitor calls.

SystemVerilog — APB transaction with embedded covergroup
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class apb_txn;
    // ── Transaction fields ────────────────────────────────────────
    rand bit [31:0] paddr;
    rand bit         pwrite;         // 1 = write, 0 = read
    rand bit [31:0] pwdata;
    rand bit [ 3:0] pstrb;           // byte-strobe (writes only)
 
    // ── Covergroup definition (a TYPE — no storage yet) ───────────
    covergroup cg_apb;
        cp_addr  : coverpoint paddr {
            bins  csr_region  = {[32'h0000_0000 : 32'h0000_00FF]};
            bins  ram_region  = {[32'h1000_0000 : 32'h1FFF_FFFF]};
            bins  reg_corners = {32'h0000_0000, 32'h0000_00FF,
                                  32'h1000_0000, 32'h1FFF_FFFF};
        }
        cp_dir   : coverpoint pwrite {
            bins  read  = {1'b0};
            bins  write = {1'b1};
        }
        cp_strb  : coverpoint pstrb {
            bins  full_word   = {4'b1111};
            bins  half_word[] = {4'b0011, 4'b1100};
            bins  byte_only[] = {4'b0001, 4'b0010, 4'b0100, 4'b1000};
            bins  unaligned   = default;       // catch-all
        }
    endgroup
 
    // ── Constructor — instantiate the covergroup ──────────────────
    function new();
        cg_apb = new();                        // ← THE LINE THAT IS ALWAYS FORGOTTEN
    endfunction
 
    // ── Sampling hook for the monitor ─────────────────────────────
    function void sample_cov();
        cg_apb.sample();
    endfunction
endclass
 
// ── Driver / monitor loop (one transaction per iteration) ─────────
apb_txn t = new();
repeat (1000) begin
    assert (t.randomize());
    drive_dut(t);                              // user-supplied
    @(posedge vif.pclk iff vif.pready);        // wait for commit
    t.sample_cov();                            // sample at the right moment
end

What this code does. Models an APB transaction with address, direction, write-strobe, and embedded coverage. The covergroup tracks three coverpoints with explicit bin partitions appropriate to the spec (memory regions, R/W split, strobe shapes). The constructor calls new() to instantiate the covergroup. After each randomised transaction is driven and the slave commits, the monitor's loop samples coverage.

How to simulate it. Compile under any IEEE 1800-compatible simulator with coverage enabled — vcs -sverilog -cm cond+fsm+tgl+branch+cov for VCS, vsim -coverage for Questa, xrun -coverage all for Xcelium. Run a test that exercises the loop, then dump coverage with urg, imc, or the tool's UCDB reporter.

Expected output (truncated urg-style coverage report).

Functional coverage report — apb_txn::cg_apb
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Group   cg_apb                              coverage   71.4%
  cp_addr   csr_region                       100.0%   ( 348 hits )
            ram_region                       100.0%   ( 421 hits )
            reg_corners[0] 32'h0000_0000     100.0%   (  14 hits )
            reg_corners[1] 32'h0000_00FF     100.0%   (  11 hits )
            reg_corners[2] 32'h1000_0000       0.0%   (   0 hits )   ← gap
            reg_corners[3] 32'h1FFF_FFFF       0.0%   (   0 hits )   ← gap
  cp_dir    read                             100.0%   ( 489 hits )
            write                            100.0%   ( 511 hits )
  cp_strb   full_word                        100.0%   ( 412 hits )
            half_word[0] 4'b0011             100.0%   (  61 hits )
            half_word[1] 4'b1100             100.0%   (  58 hits )
            byte_only[0] 4'b0001             100.0%   (  31 hits )
            byte_only[1] 4'b0010              0.0%   (   0 hits )   ← gap
            byte_only[2] 4'b0100             100.0%   (  29 hits )
            byte_only[3] 4'b1000             100.0%   (  33 hits )
            unaligned                        100.0%   (  19 hits )

What to read out of this. Three real gaps: two address corners (32'h1000_0000, 32'h1FFF_FFFF) the random distribution missed, and one strobe shape (4'b0010) the constraint solver biased against. The coverage report is the next directed test. Without the explicit reg_corners and byte_only[] bins, the coverpoints would have looked healthy on aggregate while leaving those three scenarios unverified — a real bug class.

The Three Coverpoint Forms

A coverpoint can take three syntactic shapes. Each is appropriate for a different intent.

Form 1 — Simple coverpoint with automatic bins

The lightest form: name a variable, let the simulator pick the bin structure. For small types (≤ 64 distinct values by default, controlled by option.auto_bin_max), each distinct value becomes its own bin. For wider types the value space is bucketed automatically. Automatic binning gives you a quick first-pass coverage model with zero up-front design work — useful early in a project, often replaced later with explicit bins matched to the spec.

SystemVerilog — simple (auto-binned) coverpoints
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_simple;
    coverpoint state;            // unnamed — label defaults to "state"
    cp_opcode: coverpoint opcode;        // named for cross / option use
    cp_addr  : coverpoint txn.addr;      // expression with field access
endgroup

Form 2 — Coverpoint with explicit bins

The verification-plan form: divide the value space into named buckets that match the scenarios you actually care about. This is the correct form for any coverpoint whose interesting partition is not 1-bin-per-value — addresses split by memory region, opcodes split by class, payloads split by length category.

SystemVerilog — coverpoint with explicit bins
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_addr_regions;
    cp_addr: coverpoint addr {
        bins  csr     = {[32'h0000_0000 : 32'h0000_00FF]};   // 256 values, 1 bin
        bins  ram     = {[32'h1000_0000 : 32'h1FFF_FFFF]};   // 256 M values, 1 bin
        bins  mmio    = {[32'h2000_0000 : 32'h2FFF_FFFF]};
        bins  corners = {32'h0000_0000, 32'h0000_00FF,
                          32'h1000_0000, 32'h1FFF_FFFF};       // explicit corners
        // Anything not matching the above falls into the auto "default" bin
    }
endgroup
 
// Coverpoint reports 100% only after EVERY listed bin is hit at least once
// (or option.at_least times, when set).

Form 3 — Coverpoint on an expression

Coverpoints accept any integral expression — not just a plain variable. This lets you track derived values (sum of two operands, top bits of an address, equality between two byte lanes) without adding a real signal to the DUT or a field to the transaction.

SystemVerilog — expression coverpoints
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_expr;
    // The arithmetic result of an ALU operation
    cp_sum: coverpoint (a + b) {
        bins low  = {[0:15]};
        bins high = {[16:31]};
    }
 
    // The top nibble of a 32-bit address (region selector)
    cp_region: coverpoint addr[31:28] {
        bins reg0    = {4'h0};
        bins reg1    = {4'h1};
        bins reg4    = {4'h4};
        bins others  = {[4'h2 : 4'h3], [4'h5 : 4'hF]};
    }
 
    // A boolean — are the low and high bytes of a 16-bit word identical?
    cp_uniform: coverpoint (data[7:0] == data[15:8]);
endgroup

Coverpoint Labels — Why They Matter

A coverpoint can be named with a leading label: label: coverpoint expr. Labels are syntactically optional and substantively mandatory in any non-trivial coverage model.

Without a labelWith a label
Cannot be used in cross (no name to reference)First-class participant in any cross
Per-coverpoint option.* settings not addressableoption.weight, option.at_least, option.goal settable per coverpoint
Reports display tool-generated names (coverpoint_0, coverpoint_1)Reports display the engineer-chosen name — readable, traceable to the plan
Refactoring breaks silently when fields are renamedThe label is the contract; the underlying expression can change without touching the model
SystemVerilog — labels enable cross + per-cp options
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_alu;
    cp_opcode: coverpoint opcode {
        option.weight   = 5;       // critical coverage — high weight
        option.at_least = 20;      // 20 hits per bin before "covered"
        bins add = {3'h0};
        bins sub = {3'h1};
        bins mul = {3'h2};
        bins div = {3'h3};
    }
 
    cp_mode: coverpoint mode {
        bins normal   = {2'b00};
        bins saturate = {2'b01};
        bins overflow = {2'b10};
    }
 
    // Cross requires both operands to be LABELLED coverpoints.
    cr_op_mode: cross cp_opcode, cp_mode;
endgroup

The discipline rule that follows: label every coverpoint you ship. The cost is zero (one word of typing); the benefit is every downstream activity (cross coverage, option tuning, report reading, refactoring) becomes legible.

Unlike constraint, which is locked to classes, covergroups can be defined in four different scopes. Each scope matches a different layer of the verification environment.

ScopeTypical useSampling triggerExample
ClassTransaction-level, scoreboard, monitor coverageManual sample() from post_randomize() or a monitor hookapb_txn::cg_apb above
ModuleProtocol-level coverage inside a TB moduleClock edge, named event, or manualcg_apb_bus @(posedge pclk iff pready)
InterfaceSignal-level coverage close to the DUT pinsClock edge inside the interfacecg_handshake @(posedge clk) inside an APB interface
ProgramTest-level scenario coverage from a program blockManual sample() driven by the testRare in modern UVM, common in pre-UVM SV

The scope choice is not just a syntactic detail — it determines who owns the coverage. Class-scope is owned by the transaction (the spec witness travels with the data). Interface-scope is owned by the DUT-TB boundary (the signal-level promise the DUT must keep). Module and program scopes are owned by the test infrastructure. A mature verification environment has covergroups in all four scopes, each measuring a different layer.

Built-in Methods — The Covergroup API

Every covergroup instance ships with a small built-in API. Most of it you never call directly — the tooling reads the data. But knowing what is available makes the model controllable.

MethodReturnsUse
sample()voidForce a coverage sample now — records values of every coverpoint into bins
get_inst_coverage()real (0.0 – 100.0)Per-instance percentage for this specific object
get_coverage()real (0.0 – 100.0)Type-level percentage merged across every instance of this covergroup type
start()void(Re-)enable sampling — useful after stop()
stop()voidPause sampling without losing accumulated hits — useful during reset or init
set_inst_name(string)voidRename this instance in reports — useful when several instances coexist
SystemVerilog — querying coverage at simulation time
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
apb_txn t = new();
real inst_pct;
real type_pct;
 
repeat (500) begin
    assert (t.randomize());
    t.sample_cov();
end
 
inst_pct = t.cg_apb.get_inst_coverage();
type_pct = apb_txn::cg_apb::get_coverage();   // class-scope :: type-scope
 
$display("[apb_txn] this instance:    %0.2f%%", inst_pct);
$display("[apb_txn] all instances:    %0.2f%%", type_pct);
 
// Pause coverage during a known-uninteresting phase
t.cg_apb.stop();
run_reset_sequence();
t.cg_apb.start();

Common Pitfalls — The Coverage-Killers

Most covergroup bugs are not in the model — they are in the plumbing around the model. The covergroup is correct; the sample never lands.

Pitfall 1 — Forgetting to construct the covergroup
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class my_txn;
    covergroup cg;
        coverpoint data;
    endgroup
    // ── BUG: no constructor → cg is null at runtime ──
endclass
 
// At simulation time:
//   my_txn t = new();
//   t.cg.sample();   // null-handle dereference (VCS/Questa)
//                     // or silent no-op — coverage stays at 0%
Pitfall 2 — Sampling without a label, then trying to cross
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg;
    coverpoint a;                 // ← unnamed — has no addressable label
    coverpoint b;
    cross a, b;                   // ELABORATION ERROR — no coverpoint named "a"
endgroup
Pitfall 3 — Defining the covergroup AFTER the sampling event
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_late @(posedge clk);   // event in the type
    coverpoint data;
endgroup
 
initial begin
    @(posedge clk);                  // a clock edge has already happened
    cg_late cg = new();              // ← constructed AFTER first edge
    // Coverage will start landing on the NEXT @(posedge clk), not this one.
end
Pitfall 4 — Multiple instances, but no per_instance tracking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class dual_port;
    covergroup cg;
        coverpoint data;
    endgroup
    cg port_a_cov, port_b_cov;
 
    function new();
        port_a_cov = new();
        port_b_cov = new();
    endfunction
endclass
 
// Reports show ONE merged number for both ports — fine if that's the intent.
// If you wanted per-port breakdown, you need option.per_instance = 1 on the
// covergroup; otherwise the per-instance view is unavailable.

The underlying shape is consistent: the covergroup compiled, but the sample never landed in the bin you expected. The fix is always at the construction or sampling site — never in the bin definitions themselves.

Debug Lab — "Coverage Compiled, the Report Says 0%"

The single most common new-engineer experience with coverage. The covergroup is syntactically correct, the simulation runs, the report comes back at 0%. Three independent failure modes converge on the same symptom.

Buggy code
SystemVerilog — three independent bugs, one zero-coverage report
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class fifo_txn;
    rand bit [7:0] data;
    rand bit       wr;
 
    covergroup cg @(posedge clk);             // ── BUG A: event-form covergroup
        cp_data: coverpoint data {
            bins all_vals[] = {[0:255]};
        }
        cp_wr  : coverpoint wr;
    endgroup
    // ── BUG B: no constructor for cg ──
endclass
 
class env;
    fifo_txn t;
 
    task run();
        t = new();                            // class is constructed
        repeat (1000) begin
            assert (t.randomize());           // BUG C: clk is never toggled
            // no manual sample, and clk never advances in this thread
        end
    endtask
endclass
Symptom

The class compiles cleanly. The test runs. randomize() succeeds 1000 times. The regression script reports PASS. The coverage tool reports cg 0.0% (0/258 bins hit). Some simulators emit a warning at high verbosity ("covergroup not constructed") — many do not. The verification lead is told "coverage is at 0% across the board" and the team spends two days hunting a bug in the bin definitions that is not there.

Root cause

Three compounding mistakes:

  • Bug A ties the covergroup to @(posedge clk), but the class is constructed in a context where clk is never toggled. The sampling event never fires.
  • Bug B never constructs the covergroup (cg = new() is missing from the constructor), so even if the event fired, cg is a null handle.
  • Bug C is conceptual: the test layer has no path that drives a clock or calls sample() explicitly. Bug A only matters in the presence of a real clock; Bug B only matters in the presence of a real sample; in their absence, every coverage model in this environment will look identical.

Any one of these alone would zero the report. Together, they make the cause harder to find because each bug masks the others.

Fix

Make the sampling rule explicit, construct the covergroup, and pick the form that matches the environment. For a transaction class with no clock, manual sampling is the correct choice:

SystemVerilog — the three-line fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class fifo_txn;
    rand bit [7:0] data;
    rand bit       wr;
 
    covergroup cg;                            // ── Fix A: drop the event
        cp_data: coverpoint data {
            bins all_vals[] = {[0:255]};
        }
        cp_wr  : coverpoint wr;
    endgroup
 
    function new();
        cg = new();                           // ── Fix B: construct the cg
    endfunction
endclass
 
class env;
    fifo_txn t;
 
    task run();
        t = new();
        repeat (1000) begin
            assert (t.randomize());
            t.cg.sample();                    // ── Fix C: sample explicitly
        end
    endtask
endclass

The discipline rule that prevents recurrence: every covergroup declaration is paired with its construction line in the same commit, and every covergroup has a documented sampling moment. Code review rejects the covergroup that has neither, the way it rejects a class … endclass with no new().

Industry Context — Where This Pattern Ships

The class-embeds-covergroup pattern shown above is the structural backbone of every modern UVM-based environment.

  • AMBA AXI / AHB / APB VIP ships transaction classes (uvm_sequence_item derivatives) with embedded covergroups for address ranges, transfer sizes, burst types, response codes, and the cross of every interesting pair. ARM's official AMBA VIPs publish the coverage plan alongside the agent — the covergroup is the spec contract.
  • Ethernet MAC / PCS / PMA verification uses covergroups on frame-size buckets (64B, 65–127B, …, jumbo), VLAN tag presence, error injections, and the cross of frame-size × error-type — directly traceable to IEEE 802.3 conformance test categories.
  • Memory controller verification (DDR3/4/5, LPDDR, HBM) uses covergroups on rank/bank, read/write/refresh interleaving, training step transitions, and back-to-back command pairs.
  • UVC libraries (Cadence VIPC, Synopsys VC VIP, Mentor Questa VIP) all expose covergroups in their transaction classes as the primary surface for the integrator to extend with project-specific bins.

If you can read and modify a covergroup-on-a-transaction-class fluently, you can read and modify the coverage layer of any commercial VIP — the pattern is invariant across vendors.

Interview Q&A — Ten Questions You Will Be Asked

A covergroup is the container — a user-defined type that encapsulates one area of coverage. A coverpoint is an individual tracked variable or expression inside a covergroup, partitioned into bins. One covergroup, many coverpoints; one coverpoint, many bins. The covergroup carries the sampling rule and the built-in methods; the coverpoint carries the measurement and the bin partition.

Coding Guidelines — Seven Rules for Covergroup Hygiene

  1. Every covergroup is constructed in new(), in the same commit as its declaration. Code review rejects either alone. The construction line is not a stylistic detail — it is the difference between a working coverage model and a 0% report.
  2. Label every coverpoint. The cost is one word; the payoff is cross coverage, per-coverpoint options, and readable reports. Unlabelled coverpoints survive in tutorials and disappear from production code.
  3. Use explicit bins for any coverpoint that ships. Auto bins are a prototyping aid; the verification plan deserves explicit, named bins that map to its rows. The bin name is the plan-row scoreboard.
  4. Match the sampling moment to the spec moment. Sampling before the transaction commits, before the slave responds, or before the fields are valid produces "high coverage" that is measurement noise. The sample must align with the moment the spec says the scenario happened.
  5. Pick one form of sampling per environment and stay consistent. Event-form covergroups belong at the bus/interface layer; manual sample() belongs at the transaction layer. Mixing them inside one model is rare and usually a sign the design has not been thought through.
  6. Use option.per_instance = 1 whenever multiple instances need separate visibility. Without it, the tool merges instance numbers and the per-instance view disappears from the report — a debugging trap when the per-port (or per-channel) breakdown is exactly what you needed.
  7. Treat the covergroup as documentation. Comments above each coverpoint should name the verification-plan row it implements. The covergroup is the executable spec contract; the comments are the spec citation. A reviewer should be able to read the covergroup and reconstruct the relevant plan section.

Summary

The covergroup is a class-like type that owns the coverage model; the coverpoint is its measurable field. Define the covergroup, instantiate it in the constructor, choose the sampling form (event or manual) that matches the layer, label every coverpoint, prefer explicit bins over the auto-bin default, and verify the sample is firing on the values you expect. Everything else in Module 11 — automatic bins, named bins, illegal and ignore bins, transitions, crosses, options, sampling events, instance-vs-type tracking — is a refinement of these primitives.