Part 5 · Functional Coverage · Intermediate

option.per_instance & Instance Options

Type-level vs instance-level accumulation, option.name and option.comment, and when per-instance coverage matters.

Two accumulation models

By default, every instance of a covergroup type pours its hits into one shared type-level bucket . Construct the same covergroup in four port monitors and the report shows a single number — the union of everything any port saw. Setting option.per_instance = 1 makes each instance keep its own hit counts and appear as its own row in the report, alongside the type-level rollup.

diagram
TYPE-LEVEL (default)                 PER-INSTANCE (per_instance = 1)

  mon[0].cg ─┐                          mon[0].cg ──► port0_cov : 97%
  mon[1].cg ─┼──► one merged           mon[1].cg ──► port1_cov : 95%
  mon[2].cg ─┤    type bucket          mon[2].cg ──► port2_cov : 96%
  mon[3].cg ─┘    "my_cg : 98%"        mon[3].cg ──► port3_cov :  3%  ◄── !!
                                                          │
  port3 broken/idle? INVISIBLE.         type rollup still reported too.
  Other ports' hits mask it.            The dead port is one glance away.

The diagram is the whole argument: with type-level accumulation, three healthy ports hide a dead one. Per-instance coverage exists to make per-unit health visible.


Naming instances so reports are readable

With per-instance mode on, the report needs to tell instances apart. option.name sets the per-instance report label (otherwise you get tool-generated hierarchy strings), and option.comment attaches free-text that shows up next to the entry — typically the plan section the group covers.

systemverilog
covergroup port_cg (string inst_name) @(posedge clk iff valid);
  option.per_instance = 1;
  option.name    = inst_name;             // report row label
  option.comment = "vplan 3.2 per-port traffic shapes";

  cp_kind : coverpoint pkt_kind { bins data = {DATA}; bins ctrl = {CTRL}; }
  cp_size : coverpoint pkt_size { bins s = {[1:64]}; bins m = {[65:512]};
                                  bins l = {[513:1500]}; }
  x_ks : cross cp_kind, cp_size;
endgroup

// One definition, four named instances:
port_cg cov0 = new("port0_cov");
port_cg cov1 = new("port1_cov");
port_cg cov2 = new("port2_cov");
port_cg cov3 = new("port3_cov");

Instance options you set most often

  • option.per_instance = 1 — keep per-instance hit counts and report rows; must be set, conceptually, before sampling begins.

  • option.name — human-readable instance label; pass it through a constructor argument so each instance differs.

  • option.comment — free text for the report; use it for plan traceability references.

  • option.weight / option.goal — also settable per instance; covered in the next sub-lesson.


When per-instance matters — and when it does not

Decision guide

  • Per-port / per-channel monitors — YES. Each unit must individually prove traffic coverage; symmetry bugs hide in the merge.

  • Same covergroup on N identical DMA channels — YES if channels can be configured differently; arguably type-level if they are provably symmetric and stimulus is round-robin.

  • One covergroup instantiated exactly once — irrelevant; type and instance views coincide (though option.name still tidies the report).

  • Hundreds of instances (one per packet flow) — be careful: per-instance multiplies database size and report rows; coarser grouping may serve the plan better.

systemverilog
// Reading both views at runtime:
real type_pct = cov0.get_coverage();        // merged across ALL instances
real inst_pct = cov0.get_inst_coverage();   // this instance only

initial begin
  // end-of-test per-port health check
  if (cov3.get_inst_coverage() < 50.0)
    $display("WARNING: port3 coverage %0.1f%% — dead port?",
             cov3.get_inst_coverage());
end

Interview angle: "You have four identical ports and the covergroup reports 98% — are you done?" is a classic. The expected answer names the trap (type-level merge hides an idle port), the fix (option.per_instance = 1 plus option.name per port), and the check (compare get_inst_coverage across instances). Bonus points for noting the database-size cost of per-instance mode at large instance counts.

Key takeaways

  • Default accumulation is type-level: all instances merge into one number, which hides per-unit failures.

  • option.per_instance = 1 gives each instance its own counts and report row; option.name makes the rows readable.

  • get_coverage() reads the type-level merge; get_inst_coverage() reads one instance — different questions, different numbers.

  • Use per-instance for per-port/per-channel symmetry checking; skip it when instances are provably equivalent.

Common pitfalls

  • Trusting a type-level 98% across N monitors — one dead monitor is invisible in the merge.

  • Forgetting option.name with per_instance — the report fills with unreadable auto-generated hierarchy paths.

  • Enabling per_instance on hundreds of instances without need — database bloat and report noise.

  • Calling get_coverage() expecting this instance's number — that is get_inst_coverage(); the two diverge exactly when per-instance matters.