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 Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
    • Covergroups & Coverpoints
    • Bins: Explicit, Auto, Illegal & Ignore
    • Cross Coverage
    • Coverage Options & Methods
    • Coverage Closure & Sign-Off
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹SystemVerilog
  1. Topics
  2. SystemVerilog
  3. Functional Coverage in SystemVerilog

Part 5 · Functional Coverage · Intermediate

Functional Coverage in SystemVerilog

Covergroups, coverpoints, bins, crosses, options, and the closure workflow — measuring what your testbench actually exercised.

  • 5topics
  • 22lessons
On this page

    Functional coverage measures scenarios from the specification, while code coverage measures structure from the RTL. This part builds covergroups, bins, crosses, and options, then closes the loop from holes to sign-off.

    Learning path through the coverage topics.
    Suggested order. Covergroups first, so every later bin and cross has a structure to fill.

    The five topics

    Topic

    Lessons

    What it covers

    Time

    Covergroups & Coverpoints

    4

    Covergroups bundle coverpoints and bins; lessons cover structure, sampling, expressions and a first model.

    30 min

    Bins: Explicit, Auto, Illegal & Ignore

    4

    Bins turn sampled values into named scenarios, violations and exclusions.

    30 min

    Cross Coverage

    5

    Cross coverage from product bins to spec-driven patterns, with pruning and explosion control.

    40 min

    Coverage Options & Methods

    4

    How per-instance options, weights, goals, sampling methods, and covergroup arguments shape coverage.

    30 min

    Coverage Closure & Sign-Off

    5

    Close the coverage loop: triage holes, steer stimulus, waive the unreachable, and sign off with evidence.

    40 min

    Running the coverage lessons on open tools

    The open simulators do not count covergroups. Verilator 5.048 parses them with a COVERIGN warning and ignores the bins, so get_inst_coverage() reads 0.0 however much traffic ran; Icarus 13 has no covergroups at all. Code you run in the Code Lab will compile and report nothing.

    So the lessons teach the real covergroup syntax, the code to write at work and in interviews, and count with a small bins model written in plain classes. The model applies the same rules: named bins, ranges, [] and [N] arrays, wildcard and transition bins, default, ignore_bins, illegal_bins, crosses with binsof selections, named cross bins and exclusions, at_least, weights and goals. It prints a report shaped like a simulator's.

    Covergroup syntax

    Bins model

    bins b = {[lo:hi]};

    cp.add("b", lo, hi);

    bins b[] = {[lo:hi]}; and bins b[N] = {[lo:hi]};

    cp.add_each("b", lo, hi); and cp.add_split("b", lo, hi, N);

    ignore_bins, illegal_bins

    cp.add(..., CM_IGNORE), cp.add(..., CM_ILLEGAL)

    wildcard bins b = {4'b1??0};

    cp.add_wild("b", 4'b1000, 4'b1001);

    bins t = (0 => 1 => 2);

    cp.add_trans("t", '{0, 1, 2});

    x : cross a, b;

    x = new("x", a, b);, then x.sample() after sampling a and b

    binsof(a.p), binsof(b.q), binsof(a) intersect {...}

    x.sel("p", ""), x.sel("", "q"), x.sel_values('{...})

    &&, ||, ! on selections

    x.sel_and(s1, s2), x.sel_or(s1, s2), x.sel_not(s)

    bins n = <selection>;, ignore_bins, illegal_bins in a cross

    x.add_bin("n", s), x.ignore(s), x.illegal(s)

    option.at_least, option.weight, option.goal

    the at_least, weight and goal fields

    sample(), get_coverage(), start(), stop()

    sample(v), get_coverage(), the enabled flag

    Here is the model. Benches in this part start with // uses: covmodel: paste them into the Code Lab below this listing, in one file. The course's own checks do the same, so every number on these pages came from a run.

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    // The course's bins model: what a covergroup counts, written as plain classes.
    // The open simulators do not count covergroups (Verilator 5.048 parses them and
    // ignores the bins; Icarus 13 has none), so the coverage lessons count with these
    // classes and print real numbers.
    // Each method is named after the covergroup syntax it stands for.
    
    typedef enum {CM_BIN, CM_IGNORE, CM_ILLEGAL, CM_DEFAULT} cm_kind_e;
    
    class cm_bin;
      string     name;
      cm_kind_e  kind;
      int        vals[$];          // the values that land here (ranges expanded)
      int        seq[$];           // transition alternatives, oldest value first, one after another
      int        alt_len[$];       // the length of each alternative in seq
      int        goto_v, goto_n, goto_then;   // (goto_v [->goto_n] => goto_then)
      bit [31:0] wval, wmask;      // wildcard: (v & wmask) == wval
      bit        wild;
      int        hits;
      function new(string n, cm_kind_e k);
        name = n; kind = k;
      endfunction
      function bit is_trans();
        return alt_len.size() != 0 || goto_n != 0;
      endfunction
      function bit has(int v);
        if (wild) return (v & wmask) == wval;
        foreach (vals[i]) if (vals[i] == v) return 1;
        return 0;
      endfunction
    endclass
    
    class cm_point;
      string name;
      cm_bin bin_list[$];
      int    at_least = 1, weight = 1, goal = 100;
      bit    enabled = 1;              // start() and stop()
      int    hist[$];                  // recent samples, for transition bins
      int    last_bin = -1;            // value bin hit by the last sample, for crosses
      int    illegal_hits;
      function new(string n);
        name = n;
      endfunction
      // bins n = {[lo:hi]};  ignore_bins and illegal_bins take a kind
      function void add(string n, int lo, int hi, cm_kind_e k = CM_BIN);
        cm_bin b = new(n, k);
        for (int v = lo; v <= hi; v++) b.vals.push_back(v);
        bin_list.push_back(b);
      endfunction
      // bins n = {v0, v1, ...};
      function void add_values(string n, int v[$], cm_kind_e k = CM_BIN);
        cm_bin b = new(n, k);
        b.vals = v;
        bin_list.push_back(b);
      endfunction
      // bins n[] = {[lo:hi]};  one bin per value
      function void add_each(string n, int lo, int hi);
        for (int v = lo; v <= hi; v++) add($sformatf("%s[%0d]", n, v), v, v);
      endfunction
      // bins n[count] = {[lo:hi]};  the values dealt into count bins, the last one taking any remainder
      function void add_split(string n, int lo, int hi, int count);
        int per = (hi - lo + 1) / count;
        for (int i = 0; i < count; i++)
          add($sformatf("%s[%0d]", n, i), lo + i * per, (i == count - 1) ? hi : lo + (i + 1) * per - 1);
      endfunction
      // wildcard bins n = {pattern};  mask bits set where the pattern is not ?
      function void add_wild(string n, bit [31:0] val, bit [31:0] mask, cm_kind_e k = CM_BIN);
        cm_bin b = new(n, k);
        b.wild = 1; b.wval = val & mask; b.wmask = mask;
        bin_list.push_back(b);
      endfunction
      // bins n = (v0 => v1 => ...);
      function void add_trans(string n, int s[$], cm_kind_e k = CM_BIN);
        cm_bin b = new(n, k);
        b.seq = s; b.alt_len.push_back(s.size());
        bin_list.push_back(b);
      endfunction
      // bins n = (...), (...);  another transition that also hits the last bin added
      function void also(int s[$]);
        cm_bin b = bin_list[bin_list.size() - 1];
        foreach (s[i]) b.seq.push_back(s[i]);
        b.alt_len.push_back(s.size());
      endfunction
      // bins n = (v [->count] => then_v);  count v's, gaps allowed, the last one followed by then_v
      function void add_goto(string n, int v, int count, int then_v, cm_kind_e k = CM_BIN);
        cm_bin b = new(n, k);
        b.goto_v = v; b.goto_n = count; b.goto_then = then_v;
        bin_list.push_back(b);
      endfunction
      // bins n = default;  counts samples no other bin took, and never counts toward coverage
      function void add_default(string n);
        cm_bin b = new(n, CM_DEFAULT);
        bin_list.push_back(b);
      endfunction
    
      function bit seq_hit(cm_bin b);
        int start = 0;
        if (b.goto_n != 0) begin
          int seen = 0;
          if (hist.size() < 2 || hist[hist.size() - 1] != b.goto_then || hist[hist.size() - 2] != b.goto_v) return 0;
          foreach (hist[i]) if (i < hist.size() - 1 && hist[i] == b.goto_v) seen++;
          return seen >= b.goto_n;
        end
        foreach (b.alt_len[a]) begin
          bit ok = hist.size() >= b.alt_len[a];
          for (int i = 0; ok && i < b.alt_len[a]; i++)
            if (hist[hist.size() - b.alt_len[a] + i] != b.seq[start + i]) ok = 0;
          if (ok) return 1;
          start += b.alt_len[a];
        end
        return 0;
      endfunction
    
      function void sample(int v);
        bit taken = 0;
        last_bin = -1;
        if (!enabled) return;
        hist.push_back(v);
        if (hist.size() > 16) void'(hist.pop_front());
        foreach (bin_list[i]) if (bin_list[i].kind == CM_IGNORE && (bin_list[i].is_trans() ? seq_hit(bin_list[i]) : bin_list[i].has(v))) begin
          bin_list[i].hits++;
          return;                                         // ignore_bins win over everything
        end
        foreach (bin_list[i]) if (bin_list[i].kind == CM_ILLEGAL && (bin_list[i].is_trans() ? seq_hit(bin_list[i]) : bin_list[i].has(v))) begin
          bin_list[i].hits++; illegal_hits++;
          $display("  [cov] %s: illegal bin %s hit by %0d", name, bin_list[i].name, v);
          return;
        end
        foreach (bin_list[i]) if (bin_list[i].kind == CM_BIN) begin
          if (bin_list[i].is_trans() ? seq_hit(bin_list[i]) : bin_list[i].has(v)) begin
            bin_list[i].hits++; taken = 1;
            if (last_bin < 0 && !bin_list[i].is_trans()) last_bin = i;
          end
        end
        if (!taken) foreach (bin_list[i]) if (bin_list[i].kind == CM_DEFAULT) bin_list[i].hits++;
      endfunction
    
      function int n_bins();   int n = 0; foreach (bin_list[i]) if (bin_list[i].kind == CM_BIN) n++; return n; endfunction
      function int n_covered(); int n = 0; foreach (bin_list[i]) if (bin_list[i].kind == CM_BIN && bin_list[i].hits >= at_least) n++; return n; endfunction
      function real get_coverage();
        return n_bins() == 0 ? 100.0 : 100.0 * n_covered() / n_bins();
      endfunction
      function void report();
        $display("  coverpoint %s: %0.2f%% (%0d of %0d bins)%s", name, get_coverage(), n_covered(), n_bins(),
                 get_coverage() >= goal ? "" : $sformatf("  below goal %0d", goal));
        foreach (bin_list[i]) begin
          string tag = bin_list[i].kind == CM_IGNORE ? "  (ignore)" : bin_list[i].kind == CM_ILLEGAL ? "  (illegal)" :
                       bin_list[i].kind == CM_DEFAULT ? "  (default)" : bin_list[i].hits < at_least ? "  <- hole" : "";
          $display("    %-14s %4d%s", bin_list[i].name, bin_list[i].hits, tag);
        end
      endfunction
    endclass
    
    typedef bit cm_cells[$];             // a selection of cross cells, one bit per (a bin, b bin) pair
    
    class cm_cross;
      string    name;
      cm_point  a, b;
      int       hits[$];               // one counter per (a bin, b bin) pair
      bit       skip[$];               // ignored or illegal pairs: not counted toward coverage
      bit       bad[$];                // illegal pairs: a hit is reported
      bit       taken[$];              // pairs a named bin absorbed: no longer auto bins
      string    nb_name[$];            // named cross bins
      cm_cells  nb_cells[$];
      int       nb_hits[$];
      int       at_least = 1, weight = 1;
      int       illegal_hits;
      function new(string n, cm_point pa, cm_point pb);
        name = n; a = pa; b = pb;
        repeat (a.bin_list.size() * b.bin_list.size()) begin hits.push_back(0); skip.push_back(0); bad.push_back(0); taken.push_back(0); end
      endfunction
      function int idx(int i, int j); return i * b.bin_list.size() + j; endfunction
      function bit real_pair(int i, int j);
        return a.bin_list[i].kind == CM_BIN && !a.bin_list[i].is_trans() && b.bin_list[j].kind == CM_BIN && !b.bin_list[j].is_trans();
      endfunction
    
      // Selections.  binsof(a.an) && binsof(b.bn): "" means any bin of that point
      function cm_cells sel(string an, string bn);
        cm_cells c;
        foreach (a.bin_list[i]) foreach (b.bin_list[j])
          c.push_back((an == "" || a.bin_list[i].name == an) && (bn == "" || b.bin_list[j].name == bn));
        return c;
      endfunction
      // binsof(a) intersect {values}: every a bin holding one of the values; binsof(b) intersect for b_side = 1
      function cm_cells sel_values(int v[$], bit b_side = 0);
        cm_cells c;
        foreach (a.bin_list[i]) foreach (b.bin_list[j]) begin
          bit hit = 0;
          foreach (v[k]) if (b_side ? b.bin_list[j].has(v[k]) : a.bin_list[i].has(v[k])) hit = 1;
          c.push_back(hit);
        end
        return c;
      endfunction
      function cm_cells sel_and(cm_cells x, cm_cells y); cm_cells c; foreach (x[k]) c.push_back(x[k] && y[k]); return c; endfunction
      function cm_cells sel_or(cm_cells x, cm_cells y);  cm_cells c; foreach (x[k]) c.push_back(x[k] || y[k]); return c; endfunction
      function cm_cells sel_not(cm_cells x);             cm_cells c; foreach (x[k]) c.push_back(!x[k]);        return c; endfunction
      function int count(cm_cells x); int n = 0; foreach (x[k]) n += x[k]; return n; endfunction
    
      // bins n = <selection>;  one bin, hit by a sample in any selected cell; it absorbs those auto cells
      function void add_bin(string n, cm_cells c);
        nb_name.push_back(n); nb_cells.push_back(c); nb_hits.push_back(0);
        foreach (c[k]) if (c[k]) taken[k] = 1;
      endfunction
      // ignore_bins n = <selection>;  illegal_bins n = <selection>;
      function void ignore(cm_cells c);  foreach (c[k]) if (c[k]) skip[k] = 1; endfunction
      function void illegal(cm_cells c); foreach (c[k]) if (c[k]) begin skip[k] = 1; bad[k] = 1; end endfunction
      // The short forms: ignore_bins ... = binsof(a.an) && binsof(b.bn);  and  binsof(a) intersect {values}
      function void exclude(string an, string bn); ignore(sel(an, bn)); endfunction
      function void exclude_a_values(int v[$]);    ignore(sel_values(v));   endfunction
    
      function void sample();
        int k;
        if (a.last_bin < 0 || b.last_bin < 0) return;
        k = idx(a.last_bin, b.last_bin);
        if (bad[k]) begin
          illegal_hits++;
          $display("  [cov] %s: illegal cross bin <%s, %s> hit", name, a.bin_list[a.last_bin].name, b.bin_list[b.last_bin].name);
          return;
        end
        if (skip[k]) return;
        hits[k]++;
        foreach (nb_cells[n]) if (nb_cells[n][k]) nb_hits[n]++;
      endfunction
      // The goal: every named bin with a cell left in it, plus the auto cells no named bin absorbed.
      function bit named_live(int n);
        foreach (nb_cells[n][k]) if (nb_cells[n][k] && !skip[k]) return 1;
        return 0;
      endfunction
      function bit auto_cell(int i, int j);
        return real_pair(i, j) && !skip[idx(i, j)] && !taken[idx(i, j)];
      endfunction
      function int n_bins();
        int n = 0;
        foreach (nb_name[x]) n += named_live(x);
        foreach (a.bin_list[i]) foreach (b.bin_list[j]) n += auto_cell(i, j);
        return n;
      endfunction
      function int n_covered();
        int n = 0;
        foreach (nb_name[x]) n += named_live(x) && nb_hits[x] >= at_least;
        foreach (a.bin_list[i]) foreach (b.bin_list[j]) n += auto_cell(i, j) && hits[idx(i, j)] >= at_least;
        return n;
      endfunction
      function real get_coverage();
        return n_bins() == 0 ? 100.0 : 100.0 * n_covered() / n_bins();
      endfunction
      function void report();
        $display("  cross %s: %0.2f%% (%0d of %0d bins)", name, get_coverage(), n_covered(), n_bins());
        foreach (nb_name[x]) if (named_live(x))
          $display("    %-14s %4d%s", nb_name[x], nb_hits[x], nb_hits[x] < at_least ? "  <- hole" : "");
        foreach (a.bin_list[i]) foreach (b.bin_list[j])
          if (auto_cell(i, j) && hits[idx(i, j)] < at_least)
            $display("    hole: <%s, %s>", a.bin_list[i].name, b.bin_list[j].name);
      endfunction
    endclass
    
    class cm_group;
      string   name;
      cm_point points[$];
      cm_cross crosses[$];
      int      goal = 100;
      function new(string n);
        name = n;
      endfunction
      // Weighted average over every coverpoint and cross, as the standard defines it.
      function real get_coverage();
        real sum = 0; int w = 0;
        foreach (points[i])  begin sum += points[i].get_coverage() * points[i].weight;   w += points[i].weight;  end
        foreach (crosses[i]) begin sum += crosses[i].get_coverage() * crosses[i].weight; w += crosses[i].weight; end
        return w == 0 ? 0.0 : sum / w;
      endfunction
      function void report();
        $display("covergroup %s: %0.2f%% (goal %0d)", name, get_coverage(), goal);
        foreach (points[i])  points[i].report();
        foreach (crosses[i]) crosses[i].report();
      endfunction
    endclass

    A first covergroup and the same coverage counted with the model:

    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 bench below counts it
    covergroup alu_cov with function sample(bit [1:0] op, int len);
      cp_op: coverpoint op {
        bins add = {0}; bins sub = {1}; bins mul = {2};
        illegal_bins rsvd = {3};
      }
      cp_len: coverpoint len {
        ignore_bins zero = {0};
        bins len[3] = {[1:12]};
      }
      op_x_len: cross cp_op, cp_len {
        ignore_bins mul_long = binsof(cp_op.mul) && binsof(cp_len) intersect {[9:12]};
      }
    endgroup
    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
    // uses: covmodel
    // The covergroup above, counted with the bins model.
    module tb_cm_demo;
      cm_group g = new("alu_cov");
      cm_point cp_op = new("cp_op"), cp_len = new("cp_len");
      cm_cross op_x_len;
      initial begin
        // coverpoint op  { bins add = {0}; bins sub = {1}; bins mul = {2}; illegal_bins rsvd = {3}; }
        cp_op.add("add", 0, 0); cp_op.add("sub", 1, 1); cp_op.add("mul", 2, 2);
        cp_op.add("rsvd", 3, 3, CM_ILLEGAL);
        // coverpoint len { ignore_bins zero = {0}; bins len[3] = {[1:12]}; }
        cp_len.add("zero", 0, 0, CM_IGNORE); cp_len.add_split("len", 1, 12, 3);
        // cross cp_op, cp_len { ignore_bins mul_long = binsof(cp_op.mul) && binsof(cp_len) intersect {[9:12]}; }
        op_x_len = new("op_x_len", cp_op, cp_len);
        op_x_len.exclude("mul", "len[2]");
        g.points.push_back(cp_op); g.points.push_back(cp_len); g.crosses.push_back(op_x_len);
        // sample(): five transactions
        begin
          int ops[5] = '{0, 1, 2, 0, 3};
          int lens[5] = '{1, 5, 9, 0, 12};
          for (int i = 0; i < 5; i++) begin cp_op.sample(ops[i]); cp_len.sample(lens[i]); op_x_len.sample(); end
        end
        g.report();
        if (cp_op.get_coverage() != 100.0 || op_x_len.n_covered() != 2 || op_x_len.n_bins() != 8) $fatal(1, "FAIL: model numbers");
        $display("PASS: the model counted the covergroup's bins, its illegal and ignored values, and the cross");
        $finish;
      end
    endmodule

    Read the report as a simulator's: the two coverpoints are full, the illegal value was flagged as it happened, the ignored length never counted, and the cross is at 2 of 8 because only two operation-length pairs ever ran. The holes are listed by name. That is the shape every coverage report in this part takes.

    Tip

    Define bins from the verification plan before writing stimulus, and sample observed transactions rather than driven inputs so the numbers report what was checked.

    Check yourself Why can a suite report full code coverage yet miss a required scenario?

    Code coverage records which lines, toggles, and branches executed. Only covergroups with planned bins record which specified scenarios occurred, so a required scenario with no bin stays invisible.

    Interview

    Typical interview tasks from this part: explain code versus functional coverage, write a coverpoint with explicit bins, prune a cross with ignore_bins, and describe how a coverage hole becomes a new test.

    Related topics

    • Previous: SVA

    • Next: Testbench

    • Back to the course overview

    PreviousNext: Covergroups & Coverpoints

    Up next

    Up nextCovergroups & Coverpoints

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Start Here
    • SystemVerilog Language Foundations
    • Object-Oriented SystemVerilog for Verification