SystemVerilog · Module 11
Instance vs Type Coverage
The distinction between per-instance and per-type coverage. The get_inst_coverage / get_coverage API, the per_instance option, cross-regression UCDB merge semantics, the multi-instance environments where per-instance breakdown is mandatory, and how the choice shapes the sign-off dashboard.
Module 11 · Page 11.10
The Closer for Module 11
Module 11 has built the structural vocabulary (covergroups, coverpoints, bins, crosses, transitions), the option vocabulary (weight, at_least, goal, comment), and the sampling-event vocabulary (event-driven vs manual, with function sample). This page closes the module by answering the question every multi-instance environment must answer: when do we report coverage per-instance, and when do we collapse to a single type-level number?
A covergroup is a type. A covergroup instance is the object you get from cg = new();. A class with one covergroup field, instantiated four times, produces four covergroup instances of one covergroup type. The question of how those four instances' bin counts are reported and merged at sign-off has a small API answer (get_inst_coverage vs get_coverage, option.per_instance) and a large architectural answer (which environments need the per-instance breakdown, and which can collapse without losing audit fidelity).
The Coverage Query API
Every covergroup instance exposes two methods to query its coverage at simulation time. They answer different questions.
class apb_txn;
rand bit [31:0] paddr;
rand bit pwrite;
covergroup cg_apb;
cp_addr: coverpoint paddr;
cp_dir : coverpoint pwrite;
endgroup
function new();
cg_apb = new();
endfunction
endclass
apb_txn port_a = new();
apb_txn port_b = new();
// ... drive port_a with 1000 transactions, port_b with 200 ...
real port_a_inst_pct = port_a.cg_apb.get_inst_coverage();
real port_b_inst_pct = port_b.cg_apb.get_inst_coverage();
// Type-level — aggregate across BOTH port_a and port_b instances
real type_pct = apb_txn::cg_apb::get_coverage();
$display("port_a per-instance: %0.2f%%", port_a_inst_pct);
$display("port_b per-instance: %0.2f%%", port_b_inst_pct);
$display("type-level merged: %0.2f%%", type_pct);What each method returns.
| Method | Returns | Question it answers |
|---|---|---|
inst.cg.get_inst_coverage() | per-instance % for this object | "Did this port hit every documented bin?" |
inst.cg.get_coverage() | per-instance % (LRM uses this name pre-2017; tools may alias) | (treat as inst-level; check tool docs) |
class_t::cg_t::get_coverage() | type-level merged % | "Across every instance of this type, did we hit every bin?" |
The pattern that scales: use get_inst_coverage() for per-instance diagnostics ("which port has the gap?") and the type-scope class::cg::get_coverage() for the headline sign-off number. The two answer different verification-plan questions and both belong in a real dashboard.
option.per_instance — Preserving the Per-Instance View
By default, the simulator merges covergroup instances within a single simulation run before writing the UCDB — the per-instance breakdown disappears from the report and from cross-regression merging. option.per_instance = 1 preserves the per-instance view.
class dma_channel;
covergroup cg;
option.per_instance = 1; // ← preserve per-instance view
option.comment = "vplan §8 — per-channel DMA coverage";
cp_addr: coverpoint addr;
cp_dir : coverpoint dir;
endgroup
cg ch_cov;
function new(string name);
ch_cov = new();
ch_cov.set_inst_name(name); // readable instance label
endfunction
endclass
class dma_engine;
dma_channel ch[4];
function new();
foreach (ch[i]) ch[i] = new($sformatf("dma.ch[%0d]", i));
endfunction
endclassThe report shape with per_instance = 1:
Group cg [dma.ch[0]] coverage 100.0%
cp_addr ... 100.0%
cp_dir ... 100.0%
Group cg [dma.ch[1]] coverage 75.0%
cp_addr ... 100.0%
cp_dir ... 50.0% ( 1/2 bins )
Group cg [dma.ch[2]] coverage 100.0%
...
Group cg [dma.ch[3]] coverage 100.0%
...
Group cg (TYPE — merged across all 4 instances) coverage 93.8%
cp_addr ... 100.0%
cp_dir ... 100.0% ( merged across instances )Without per_instance = 1, the per-channel breakdown disappears entirely. The report shows one type-level number; the dashboard cannot tell you that channel 1 has a 50% gap on cp_dir. For DMA / cache / multi-port memory environments, this is almost always the wrong setting — the per-channel gap is the most actionable information the report can produce.
When to Use Per-Instance vs Type Coverage
Match the choice to the verification-plan's question for each verification-plan row that touches multi-instance state.
| Environment | Per-instance? | Why |
|---|---|---|
| Multi-port memory (each port has its own contract) | Yes | Each port is independently verified; per-port gap is the actionable signal |
| Multi-channel DMA (each channel runs different traffic) | Yes | Per-channel report identifies which channel is under-exercised |
| Per-domain SoC PMU (each power domain has its own state machine) | Yes | Per-domain coverage is the per-domain verification-plan row |
| Cache (per-way coverage of MESI states) | Yes | Per-way gap signals coherence-protocol gap in one specific way |
| N identical bus monitors sharing one coverage contract | No | All instances measure the same metric against the same contract; merged number is the answer |
| Multiple instances of the same UVM agent in different test contexts | Both | Per-instance for per-context audit; merged for the sign-off headline |
The decision rule that works in practice: if the verification-plan row distinguishes instances, use per_instance = 1. If the row treats instances as interchangeable, the default per_instance = 0 is fine. Most verification plans for non-trivial designs distinguish instances at least somewhere; production environments default per_instance = 1 for any covergroup with more than one instance.
set_inst_name() — Readable Instance Labels
The default instance name in reports is tool-generated (cg_1, cg_2, cg_3). For environments with more than two instances, set_inst_name() gives each one a readable label.
class cache_way;
covergroup cg_way;
option.per_instance = 1;
cp_state: coverpoint state {
bins mesi[] = {[0:3]}; // M, E, S, I states
}
endgroup
cg_way w_cov;
function new(int way_index);
w_cov = new();
w_cov.set_inst_name($sformatf("cache.way[%0d]", way_index));
endfunction
endclass
class cache;
cache_way ways[4];
function new();
foreach (ways[i]) ways[i] = new(i);
endfunction
endclass
// Reports now display:
// cg_way [cache.way[0]] coverage 100.0%
// cg_way [cache.way[1]] coverage 75.0% ← actionable per-way gap
// cg_way [cache.way[2]] coverage 100.0%
// cg_way [cache.way[3]] coverage 100.0%
// cg_way [merged] coverage 93.8%The $sformatf pattern (with a structural path like cache.way[%0d]) maps each instance label to its position in the hierarchy. The report becomes self-explanatory: a 75% line attached to cache.way[1] localises the gap to that specific way, and the next directed test is "exercise way 1 in MESI states it has not yet reached."
A Complete Working Example — Dual-Port Memory Coverage
The pattern below shows the production shape: a transaction-class covergroup, instantiated per port, with per-instance preservation and readable labels.
class apb_txn;
rand bit [31:0] paddr;
rand bit pwrite;
rand bit [ 3:0] pstrb;
covergroup cg_apb;
option.per_instance = 1;
option.comment = "vplan §5 — per-port APB transaction coverage";
cp_addr: coverpoint paddr {
bins csr = {[32'h0000_0000 : 32'h0000_00FF]};
bins ram = {[32'h1000_0000 : 32'h1FFF_FFFF]};
bins mmio = {[32'h2000_0000 : 32'h2FFF_FFFF]};
}
cp_dir: coverpoint pwrite {
bins read = {1'b0};
bins write = {1'b1};
}
cp_strb: coverpoint pstrb {
bins full = {4'b1111};
bins half_lo = {4'b0011};
bins half_hi = {4'b1100};
bins byte_only = {4'b0001, 4'b0010, 4'b0100, 4'b1000};
}
cr_addr_dir: cross cp_addr, cp_dir {
option.comment = "vplan §5.4 — region × direction per port";
}
endgroup
function new(string instance_path);
cg_apb = new();
cg_apb.set_inst_name(instance_path);
endfunction
function void post_randomize();
cg_apb.sample();
endfunction
endclass
class dual_port_mem_env;
apb_txn port_a;
apb_txn port_b;
function new();
port_a = new("mem.port_a");
port_b = new("mem.port_b");
endfunction
// Per-instance and type-level closure dashboard
function void print_closure();
real pa = port_a.cg_apb.get_inst_coverage();
real pb = port_b.cg_apb.get_inst_coverage();
real ty = apb_txn::cg_apb::get_coverage();
$display("[mem.port_a] per-instance coverage: %0.2f%%", pa);
$display("[mem.port_b] per-instance coverage: %0.2f%%", pb);
$display("[apb_txn type] merged coverage: %0.2f%%", ty);
endfunction
endclassWhat this code does. The apb_txn class carries a covergroup with three coverpoints and one cross. option.per_instance = 1 preserves per-instance counts. set_inst_name() is called from the constructor with the hierarchical path, so each port's report row has a readable label. The dual-port environment instantiates two apb_txn objects — one for each port — and exposes a closure-print function that surfaces both per-port and merged numbers.
Expected dashboard print (after a regression).
[mem.port_a] per-instance coverage: 87.5%
[mem.port_b] per-instance coverage: 62.5% ← actionable gap on port_b
[apb_txn type] merged coverage: 87.5%What to read out of this. Type coverage at 87.5% looks tolerable on the headline. But the per-instance breakdown shows port_b is at 62.5% — meaning the merged number is being pulled up by port_a's contribution. The verification gap lives on port_b specifically; the next directed test targets port_b traffic. Without per_instance = 1, the dashboard would have shown only the merged 87.5% and the team would not know which port to target.
Cross-Regression Merging — What per_instance Buys at Sign-Off
For multi-night regression flows that merge UCDBs across runs, the per_instance setting determines what survives the merge.
Without per_instance = 1:
- Each nightly regression writes a UCDB containing one type-level row per covergroup type.
- Merging two UCDBs adds the type-level counts — but the per-instance contributions to each are lost.
- After a month of merging, the dashboard shows merged type-level numbers; per-instance evidence has been collapsed at each write.
- Sign-off audit can verify "every documented bin was hit somewhere" but cannot verify "every instance hit every documented bin."
With per_instance = 1:
- Each nightly regression writes a UCDB containing per-instance rows alongside the merged type-level row.
- Merging two UCDBs adds per-instance counts pairwise (instance 0 + instance 0, instance 1 + instance 1, …) — the per-instance contributions accumulate across regressions.
- After a month of merging, the dashboard shows both per-instance closure per nightly's contribution and the across-month merged numbers.
- Sign-off audit can verify per-instance coverage rules and per-instance audit-trail citations.
The setting is load-bearing for multi-week sign-off flows. Production environments enable it once at type scope (type_option.per_instance = 1 on the relevant covergroup types) and document the choice in a coverage-policy README. Per-instance coverage discipline is not a per-run decision; it is a multi-month verification-plan commitment.
Debug Lab — "The Per-Port Dashboard Disappeared After a Merge Regression"
A failure mode that catches teams when the verification dashboard transitions from single-run to merged-regression reporting.
class apb_port;
covergroup cg;
// ── BUG: per_instance is NOT set — defaults to 0 ──────────
// option.per_instance = 1; // omitted
cp_addr: coverpoint addr;
cp_dir : coverpoint dir;
endgroup
cg port_cov;
function new(string name);
port_cov = new();
port_cov.set_inst_name(name);
endfunction
endclass
class env;
apb_port port_a, port_b;
function new();
port_a = new("mem.port_a");
port_b = new("mem.port_b");
endfunction
endclass
// Run 1 — single regression report:
// cg [mem.port_a] 100% ← per-instance row VISIBLE here
// (some simulators show per-inst in single
// run even when per_instance=0)
// cg [mem.port_b] 75%
// cg (merged) 87.5%
// Merge 30 nightly UCDBs and re-report:
// cg (merged across 30 runs and 2 instances) 95.0%
// ← per-instance rows GONE — collapse-on-write lost the per-instance
// data at each nightly's UCDB writeA single-regression report shows per-instance rows. The team launches a 30-night merge regression to drive sign-off coverage. The merged dashboard shows only the type-level number — no per-port breakdown. The verification lead asks "how is port_b doing?" and there is no answer the dashboard can give; the per-port view was collapsed during each nightly's UCDB write and cannot be reconstructed at merge time.
The covergroup omitted option.per_instance = 1. The default per_instance = 0 allows the simulator to show per-instance rows in a single-run report (some simulators do, some don't — tool-dependent), but it always collapses instances before UCDB write. Once collapsed, the per-instance contribution to each bin is lost; merging across runs can only operate on the collapsed type-level rows. The single-run dashboard misled the team into thinking per-instance reporting was set up correctly; it wasn't, and the merge surfaced the gap.
The structural mistake is treating per-instance reporting as a runtime visualisation rather than a UCDB-persistence setting. The merged dashboard is the sign-off dashboard; whatever isn't preserved at UCDB write is gone from the sign-off audit trail.
Set option.per_instance = 1 (or type_option.per_instance = 1 at type scope) on every covergroup that has multiple instances. Re-run the regression — both new runs and merged historical UCDBs need to be regenerated since the historical UCDBs lost their per-instance contribution.
class apb_port;
covergroup cg;
type_option.per_instance = 1; // ← preserved across UCDB merges
option.comment = "Per-port coverage — preserved per-instance at "
+ "every UCDB write for multi-night merge support";
cp_addr: coverpoint addr;
cp_dir : coverpoint dir;
endgroup
cg port_cov;
function new(string name);
port_cov = new();
port_cov.set_inst_name(name);
endfunction
endclassThe discipline rule that prevents recurrence: every covergroup type with multiple instances sets type_option.per_instance = 1 from day one. Code review treats the omission as a sign-off-readiness defect, not a stylistic preference. Some teams add a static-analysis check that scans for covergroup types with multiple known instantiation sites and verifies per_instance = 1 is set.
The complementary discipline: regenerate UCDBs whenever the per-instance setting changes. Historical UCDBs written under per_instance = 0 cannot be retroactively given per-instance data — the contribution was discarded at write time. The merge process has to be re-run from sources for the per-instance view to populate.
Industry Context — Where the Distinction Matters Most
- Multi-port memory controllers. DDR controllers expose multiple read/write ports — each port has its own command queue, its own response routing, and (in spec terms) its own contract. Per-instance coverage is the only way to verify each port's contract independently.
- N-way cache verification. A 4-way associative cache runs MESI/MOESI state machines per way. Per-way coverage surfaces "way 2 never reaches the Owned state" — invisible from merged-only reporting.
- Multi-channel DMA / multi-domain GPIO / multi-region MPU. Each channel/domain/region has its own bin-counters, its own audit-trail row in the verification plan. Per-instance coverage is the structural pattern.
- AMBA bus matrix verification. Multiple masters and slaves connect through a bus matrix; each master-slave pair is a separate verification target. Per-instance coverage on the per-pair covergroups is the canonical pattern in ARM AMBA VIPs.
- UVM environments with replicated agents. When the same agent type is instantiated multiple times (e.g. four AXI agents for four AXI ports),
per_instance = 1keeps each agent's coverage view distinct. Without it, the four agents' contributions blur into one type-level number and per-agent debugging stalls. - DO-254 / ISO 26262 / ARP4754A traceability flows. Per-instance coverage rows map directly to per-instance verification-plan rows in the traceability matrix. Certification audits read per-instance coverage as primary evidence; merged-only coverage is insufficient for instance-specific certification claims.
The pattern across these uses: per-instance coverage is the structural answer when the verification plan distinguishes instances. The default per_instance = 0 was designed for environments where instances are interchangeable; in modern multi-instance designs, that's the exception rather than the rule.
Interview Q&A — Ten Questions You Will Be Asked
Instance coverage is the per-object coverage percentage — each covergroup instance has its own bin counters and its own closure number. Type coverage is the aggregate percentage across every instance of the covergroup type — the bins are merged into one set of counters. get_inst_coverage() returns the instance number; class_t::cg_t::get_coverage() returns the type number. They answer different verification-plan questions and both belong in a real dashboard.
Coding Guidelines — Seven Rules for Instance-vs-Type Discipline
- Set
type_option.per_instance = 1on every covergroup type with more than one instance. Defaultper_instance = 0loses the per-instance view at UCDB write time, which destroys the sign-off audit trail for multi-instance environments. - Use
set_inst_name()from every instance's constructor. The default tool-generated names are unreadable; the structural-path convention (mem.port_a,cache.way[0],dma.ch[3]) makes the report navigable. - Print both per-instance and type-level coverage in the closure dashboard. The per-instance numbers identify which instances drove a gap; the type-level number is the sign-off headline. Reporting only one is incomplete.
- Match the per-instance/type choice to the verification-plan row's question. Per-instance for plan rows that distinguish instances (per-port, per-channel, per-domain). Type-level merged when instances are interchangeable. Most non-trivial plans distinguish at some level; default to
per_instance = 1. - Regenerate UCDBs after changing
per_instance. Historical UCDBs written underper_instance = 0cannot be retroactively given per-instance data — the contribution was discarded at write time. The merge process must run from sources. - Verify
per_instanceis correct before launching a long regression. Run an overnight, run the merge command, inspect the merged-only report. Per-instance rows present → setting is correct. Per-instance rows missing → fix the setting before the multi-night run starts. The cost of catching it at hour 12 is one re-run. - Document the per-instance setting choice in a coverage-policy README. The discipline is a multi-month commitment; new engineers joining the project need to know the convention before adding new covergroup types. Per-instance vs type is not a per-run decision; it is a verification-plan-level architecture.
Summary
The instance-vs-type coverage distinction governs how multi-instance environments are reported and merged at sign-off. Instance coverage answers "is each instance individually verified?"; type coverage answers "across every instance, was every documented bin hit at least once?" option.per_instance = 1 (best at type_option scope) preserves per-instance counts in the UCDB so the per-instance view survives merging — load-bearing for multi-week sign-off flows. set_inst_name() makes the report navigable. The default per_instance = 0 is appropriate only when instances are genuinely interchangeable; in modern multi-instance designs that is the exception. Set the option deliberately from day one, print both numbers in the dashboard, and treat any historical UCDB written under the wrong setting as needing regeneration from sources.
Closing — What Module 11 Built
Across ten tutorials, Module 11 has built the complete functional-coverage discipline. The structural vocabulary: covergroups and coverpoints, the three bin partitions (automatic, named, wildcard/illegal/ignore), transition coverage, and cross coverage. The measurement vocabulary: coverage options and sampling events. The reporting vocabulary: the instance-vs-type model in this page. The introduction framed coverage as the verification-plan witness — the only honest answer to "have we verified what the spec requires?"; every page since has been a vocabulary the discipline uses to keep that promise.
The next module (Module 12 — SystemVerilog Assertions) builds the temporal contract layer that pairs with coverage. Where coverage measures whether scenarios were exercised, SVA enforces invariants — the properties that must hold whenever any scenario runs. The two layers together are how a non-trivial RTL verification environment ships to silicon.