Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
    • Covergroups & Coverpoints
    • Bins: Explicit, Auto, Illegal & Ignore
    • Cross Coverage
    • Coverage Options & Methods
      • option.per_instance & Instance Options
      • Weights, Goals & at_least
      • get_coverage(), sample() & Friends
      • Parameterizing Covergroups
      • Simulator Coverage Control
    • Coverage Closure & Sign-Off
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹Coverage Options & Methods
  1. Topics
  2. SystemVerilog
  3. Functional Coverage in SystemVerilog
  4. Coverage Options & Methods
  5. Parameterizing Covergroups

Part 5 · Functional Coverage · Intermediate

Parameterizing Covergroups

Covergroup constructor arguments, ref arguments for sampled variables, and one definition reused per port.

On this page

    Why this matters

    Five ports had five copy-pasted covergroups, and one kept an old legal-length range after a spec change. One definition with constructor arguments would have had one place to fix.

    After this lesson you can: parameterize a covergroup with constructor arguments, use ref arguments for the sampled signal, and build an embedded covergroup per object.

    Constructor arguments

    A covergroup declaration takes a formal argument list like a function. Arguments bind at new time and can shape bins, set options, and name the instance.

    One definition then serves N differently configured instances. The alternative is N copy-pasted variants that drift apart.

    The following listing is not run in this course's checks: the open simulators do not count covergroups.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    // Value arguments shape bins and options per instance
    covergroup addr_cg (string nm, int unsigned lo, int unsigned hi)
                @(posedge clk iff valid);
      option.per_instance = 1;
      option.name = nm;
    
      cp_addr : coverpoint addr {
        bins in_range  = {[lo:hi]};
        bins below     = {[0:lo-1]};
        bins above     = {[hi+1:32'hFFFF_FFFF]};
      }
    endgroup
    
    // Same definition, three differently bounded instances.
    // Keep 0 < lo and hi < 32'hFFFF_FFFF so the edge bins stay correct:
    // at lo = 0, lo-1 wraps to 32'hFFFF_FFFF and below covers the whole
    // space (always hit); at max hi, hi+1 wraps to 0 the same way.
    addr_cg sram_cov = new("sram_cov", 32'h0000_1000, 32'h0000_FFFF);
    addr_cg peri_cov = new("peri_cov", 32'h4000_0000, 32'h4001_FFFF);
    addr_cg ddr_cov  = new("ddr_cov",  32'h8000_0000, 32'hBFFF_FFFF);

    Bounds are frozen per instance at construction. Later changes to the passed variables never retune the bins.

    Value arguments versus ref arguments

    Kind

    Bound when

    Used for

    Value, the default

    Copied at new

    Bin bounds, names, modes, option settings

    ref

    Aliased at new

    The variable to sample, read live at each sampling event

    A ref argument lets each instance watch a different signal. The following listing is not run in this course's checks: the open simulators do not count covergroups.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    // ref argument: the instance samples whichever variable it was bound to
    covergroup len_cg (ref logic [7:0] len_sig, input string nm)
                @(posedge clk iff valid);
      option.per_instance = 1;
      option.name = nm;
      cp_len : coverpoint len_sig {
        bins s = {[1:4]}; bins m = {[5:64]}; bins l = {[65:255]};
      }
    endgroup
    
    logic [7:0] rx_len, tx_len;
    len_cg rx_cov = new(rx_len, "rx_len_cov");
    len_cg tx_cov = new(tx_len, "tx_len_cov");

    One definition watches two signals. The referenced signal must outlive the covergroup instance, or sampling reads a dangling alias.

    One definition across many instances

    In class benches the same idea appears as an embedded covergroup, built once per object with that object's configuration. The following listing is not run in this course's checks: the open simulators do not count covergroups.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    class port_monitor;
      string  mon_name;
      int     max_len;
    
      covergroup cg (string nm, int maxl) with function sample(int len, bit bad);
        option.per_instance = 1;
        option.name = nm;
        cp_len : coverpoint len {
          bins small = {[1:8]};
          bins big   = {[9:maxl]};          // per-instance upper bound
        }
        cp_bad : coverpoint bad { bins ok = {0}; bins seen = {1}; }
        x_lb : cross cp_len, cp_bad;
      endgroup
    
      function new(string name_arg, int max_len_arg);
        mon_name = name_arg;
        max_len = max_len_arg;
        cg = new(mon_name, max_len);   // embedded covergroup: construct once, here
      endfunction
    
      function void observe(int len, bit bad);
        cg.sample(len, bad);
      endfunction
    endclass
    
    // port0 allows long packets, port1 is a narrow control port:
    port_monitor m0 = new("port0_cov", 1500);
    port_monitor m1 = new("port1_cov", 64);
    • An embedded covergroup is constructed in the class constructor, once and only there.

    • Value arguments are a snapshot at new. Expecting bins to track the source variable is the classic bug.

    • Bins built from arguments are fixed at construction. Parameterization is per-instance, not dynamic.

    Common pitfall

    Passing a variable and expecting bins to track it fails: value arguments freeze at construction. Building the embedded group outside the constructor, or twice, loses coverage. Binding ref to a short-lived variable samples a dangling alias.

    Interview

    “How do you cover five ports with different legal length ranges without five covergroups?” wants constructor arguments for bounds and names, ref or a sample signature for data, and per-instance rows. The follow-up probes whether value arguments are bound at construction or sampled live.

    Check yourself A test changes the variable that supplied a bin bound after new. Do the bins change?

    No. Value arguments are copied at construction. Only a ref argument follows a live variable, and it supplies sampled values, not bounds.

    Check yourself Where must an embedded covergroup be constructed?

    Once, in the class constructor. Anywhere else risks tool errors or silently lost coverage.

    Testbench

    The bench gives the bins model a constructor with the lesson's arguments (nm, lo, hi), builds three instances, and changes the variable that supplied sram's upper bound after new. Addresses are 14 bits here so the model stays small, and the constructor has the guards a real covergroup needs: no below bin when lo is 0.

    Predict first Twelve addresses go to all three instances, and hi_bound drops from 255 to 100 after sram was built. How many in-range hits does sram count?

    4: 0, 1, 255 and 7. The bound was copied at new, so 255 still counts. peri counts 3 and ddr counts 3 from the same addresses.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // uses: covmodel
    // One parameterized definition, three instances: covergroup addr_cg (string nm, int lo, int hi).
    // Addresses are 14 bits here so the model's bins stay small.
    class addr_cov;
      cm_point cp_addr;
      function new(string nm, int lo, int hi);          // value arguments: copied now, frozen after
        cp_addr = new(nm);
        cp_addr.add("in_range", lo, hi);
        if (lo > 0)     cp_addr.add("below", 0, lo - 1);         // the guards a real covergroup needs too:
        if (hi < 16383) cp_addr.add("above", hi + 1, 16383);     // lo - 1 and hi + 1 must not wrap
      endfunction
    endclass
    
    module tb_range_args;
      addr_cov sram, peri, ddr;
      initial begin
        int hi_bound = 255;
        sram = new("sram_cov", 0, hi_bound);
        peri = new("peri_cov", 1024, 2047);
        ddr  = new("ddr_cov", 2048, 8191);
        hi_bound = 100;                                 // too late: sram's bins were built with 255
        begin
          int addrs[12] = '{0, 1, 255, 256, 1024, 1500, 2047, 2048, 4096, 8191, 8192, 7};
          foreach (addrs[i]) begin sram.cp_addr.sample(addrs[i]); peri.cp_addr.sample(addrs[i]); ddr.cp_addr.sample(addrs[i]); end
        end
        sram.cp_addr.report(); peri.cp_addr.report(); ddr.cp_addr.report();
        if (sram.cp_addr.bin_list[0].hits != 4 || peri.cp_addr.bin_list[0].hits != 3 || ddr.cp_addr.bin_list[0].hits != 3 ||
            sram.cp_addr.n_bins() != 2) $fatal(1, "FAIL: per-instance bins");
        $display("PASS: one definition counted 4 sram, 3 peri and 3 ddr hits; changing hi_bound after new changed nothing");
        $finish;
      end
    endmodule
    Terminal
      coverpoint sram_cov: 100.00% (2 of 2 bins)
        in_range       4
        above          8
      coverpoint peri_cov: 100.00% (3 of 3 bins)
        in_range       3
        below          5
        above          4
      coverpoint ddr_cov: 100.00% (3 of 3 bins)
        in_range       3
        below          8
        above          1
    PASS: one definition counted 4 sram, 3 peri and 3 ddr hits; changing hi_bound after new changed nothing

    Remember

    • Value arguments are copied at new() and frozen.

    • ref arguments follow a live variable for sampling.

    • Guard edge bins so lo - 1 and hi + 1 cannot wrap.

    Rule of thumb: one definition, many instances, arguments for the differences.

    How this was checked

    Counted with the course's bins model (rich/coverage/cov_model.sv, listed on the Coverage hub) on Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator 5.048 parses covergroups with a COVERIGN warning and ignores their bins, and Icarus 13 has none, so each covergroup listing is marked not run and its bench counts the same bins with the model.

    Three instances counted 4, 3 and 3 in-range hits from the same twelve addresses, and changing the bound variable after new() changed nothing.

    Related topics

    • Instance options

    • Cross coverage

    Previous

    226 / 363

    Next: Simulator Coverage Control

    Up next

    Up nextSimulator Coverage Control

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • option.per_instance & Instance Options
    • Weights, Goals & at_least