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.
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 watchesPer-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.
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.
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_lenThe 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.
// 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
endclassThe 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.