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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here5
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive39
  • SystemVerilog Assertions — Interview-Critical Deep Dive41
  • Functional Coverage in SystemVerilog25
    • Covergroups & Coverpoints
      • Covergroup Anatomy
      • Sampling: Clocked, Event & sample()
      • Coverpoint Expressions & Types
      • Your First Coverage Model: Three Bins and a Report
      • An ALU Coverage Plan as a Covergroup
      • Sign-Off Needs Both: Code and Functional Coverage
      • The Bins Model: Counting Covergroups on Open Tools
    • Bins: Explicit, Auto, Illegal & Ignore
    • Cross Coverage
    • Coverage Options & Methods
    • Coverage Closure & Sign-Off
  • Testbench Architecture in SystemVerilog24
  • Advanced Language & Integration26
  • Senior & Interview Preparation23
  • Principal-Level Verification34
Loading lesson

←Covergroups & Coverpoints
  1. Topics
  2. SystemVerilog
  3. Functional Coverage in SystemVerilog
  4. Covergroups & Coverpoints
  5. The Bins Model: Counting Covergroups on Open Tools

Part 5 of 9, lesson 7 of 25Part 5 of 9/Functional Coverage/Lesson 7 of 25Intermediate

The Bins Model: Counting Covergroups on Open Tools

The plain-class model the coverage lessons count with, how each method maps to covergroup syntax, and one covergroup counted both ways.

A learner pasted a covergroup into the Code Lab, saw 0.0% however much traffic ran, and concluded the testbench was broken. The simulator ignores the bins; nothing was wrong with the test.

After this lesson you can: use the bins model to count a covergroup on open tools and map each method to the covergroup syntax it stands for.

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
Terminal
[cov] cp_op: illegal bin rsvd hit by 3
covergroup alu_cov: 75.00% (goal 100)
  coverpoint cp_op: 100.00% (3 of 3 bins)
    add            2
    sub            1
    mul            1
    rsvd           1     (illegal)
  coverpoint cp_len: 100.00% (3 of 3 bins)
    zero           1     (ignore)
    len[0]         1
    len[1]         1
    len[2]         2
  cross op_x_len: 25.00% (2 of 8 bins)
    hole: <add, len[1]>
    hole: <add, len[2]>
    hole: <sub, len[0]>
    hole: <sub, len[2]>
    hole: <mul, len[0]>
    hole: <mul, len[1]>
PASS: the model counted the covergroup's bins, its illegal and ignored values, and the cross

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.

Check yourself Why does this course count coverage with a model instead of running covergroups?

Verilator 5.048 ignores covergroup bins and Icarus 13 has none, so a covergroup run there reads 0 however much traffic ran. The model applies the same rules and prints numbers you can check.

  • Open simulators ignore covergroup bins; the model counts them.

  • Each model method is named after a syntax form.

  • Paste benches below the model listing, in one file.

Rule of thumb: count with the model on open tools, and with the covergroup at work.

How this was checked

Counted with the course's bins model (rich/coverage/cov_model.sv, listed on The Bins Model page) 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.

The demo counted a covergroup with an illegal value, an ignored value and a cross: cross at 2 of 8 bins, the illegal hit reported, the ignored value never counted.

Related topics

  • Your first coverage model

Previous

150 / 275

Next: Bins: Explicit, Auto, Illegal & Ignore

Up next

Up nextBins: Explicit, Auto, Illegal & Ignore

Sign in to save progress

Practice this lessonPrevious lessonReport an error

See also

  • Covergroup Anatomy
  • Sampling: Clocked, Event & sample()
  • Coverpoint Expressions & Types