Part 5 · Functional Coverage · Intermediate

Embedded vs Standalone Covergroups

Class-embedded covergroups with per-object instances and option.per_instance, module-level covergroups, passing arguments, and the wrapper-class reuse pattern.

Two homes, two lifetimes

An embedded covergroup is declared inside a class; every object of that class can own its own covergroup instance, created in the constructor. A standalone covergroup is declared at module, interface, or package scope as a type, then instantiated explicitly. Embedded suits transaction coverage that travels with testbench components (one collector per agent); standalone suits white-box coverage bolted next to the RTL signals it watches.

diagram
EMBEDDED (in class)                 STANDALONE (in module/interface)

  class cov_collector;                module dut_cov(input clk, ...);
    covergroup cg ...                   covergroup cg @(posedge clk);
    function new();                       ...
      cg = new();   ──┐                 endgroup
    endfunction       │                 cg cg_inst = new();
  endclass            │               endmodule
                      │
  one cg instance     │               one instance per module
  PER OBJECT ◄────────┘               instantiation (usually 1)

  cpu_agent.cov ──► cg instance A     bound or instantiated next
  dma_agent.cov ──► cg instance B     to the signals it watches

Per-instance coverage and option.per_instance

By default, simulators report covergroup coverage merged across all instances of the same covergroup type — useful sometimes, misleading often. With two agents sharing a collector class, the merged number can read 100% while the DMA agent alone sits at 40%. Setting option.per_instance = 1 (plus a distinct option.name per instance) makes each instance track and report its own coverage.

systemverilog
class port_coverage;
  covergroup cg (string inst_name) with function sample(bit [7:0] len);
    option.per_instance = 1;
    option.name = inst_name;        // distinct report row per instance
    cp_len : coverpoint len {
      bins single = {1};
      bins burst  = {[2:255]};
    }
  endgroup

  function new(string inst_name);
    cg = new(inst_name);            // covergroup new() takes arguments!
  endfunction
endclass

// In the environment:
// cpu_cov = new("cpu_port");  → report row "cpu_port"
// dma_cov = new("dma_port");  → report row "dma_port"

Passing references and arguments to covergroups

Covergroup new() accepts formal arguments declared in the covergroup header — strings for naming, integers for parameterizing bins, and (most powerfully) ref arguments that let one covergroup type watch different variables per instance.

systemverilog
covergroup cg_byte (ref bit [7:0] watched, input int max_val)
                                                  @(posedge clk);
  cp_val : coverpoint watched {
    bins lo  = {[0:max_val/2]};
    bins hi  = {[max_val/2 + 1:max_val]};
  }
endgroup

bit [7:0] tx_len, rx_len;
cg_byte tx_cov = new(tx_len, 128);   // instance watches tx_len
cg_byte rx_cov = new(rx_len, 255);   // same type, watches rx_len

The wrapper-class pattern for reusable coverage

Covergroups cannot be extended, cannot live in packages as constructible units the way classes can, and cannot be passed around by themselves. The standard solution: wrap the covergroup in a class. The wrapper gives you construction control, a clean sample API, multiple independent instances, and a unit you can place into any environment.

systemverilog
// Reusable, environment-agnostic coverage unit
class axi_len_coverage;
  covergroup cg with function sample(bit [7:0] awlen, bit [2:0] awsize);
    option.per_instance = 1;
    cp_len  : coverpoint awlen {
      bins single  = {0};
      bins short_b = {[1:3]};
      bins long_b  = {[4:255]};
    }
    cp_size : coverpoint awsize;
    x_len_size : cross cp_len, cp_size;
  endgroup

  function new(string name = "axi_len_cov");
    cg = new();
    cg.option.name = name;
  endfunction

  function void sample_txn(bit [7:0] awlen, bit [2:0] awsize);
    cg.sample(awlen, awsize);
  endfunction

  function real get_pct();
    return cg.get_inst_coverage();
  endfunction
endclass
  • The wrapper class is reusable across testbenches; a module-level covergroup is welded to its module.

  • Per-object instances make multi-port / multi-agent coverage natural — one wrapper object per port.

  • The wrapper can expose query helpers (get_pct) and gating logic (skip during reset) around the raw covergroup.

  • Interview angle: “why wrap a covergroup in a class?” — construction control, reuse, per-instance tracking, and a stable sample API.

Key takeaways

  • Embedded covergroups give one instance per object — natural for per-agent, per-port coverage collectors.

  • option.per_instance = 1 plus distinct option.name prevents merged-across-instances numbers from hiding a weak port.

  • Covergroup new() takes arguments, including ref variables — one covergroup type can watch different signals per instance.

  • The wrapper-class pattern is the standard way to make covergroups reusable and portable.

Common pitfalls

  • Relying on type-merged coverage with multiple instances — one strong instance masks another at 40%.

  • Forgetting cg = new() in the wrapper constructor — the embedded-covergroup silent-zero bug again.

  • Declaring a covergroup inside a class and trying to instantiate it from outside — embedded covergroups are constructible only within their class.

  • Module-level covergroups for transaction coverage — no per-object instances, awkward sampling from class-based testbenches.