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
      • Covergroup Anatomy
      • Sampling: Clocked, Event & sample()
      • Coverpoint Expressions & Types
      • Building Your First Coverage Model
    • 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

‹Covergroups & Coverpoints
  1. Topics
  2. SystemVerilog
  3. Functional Coverage in SystemVerilog
  4. Covergroups & Coverpoints
  5. Building Your First Coverage Model

Part 5 · Functional Coverage · Intermediate

Building Your First Coverage Model

From ALU spec to coverage plan to covergroup, wrapper class, report holes and sign-off.

On this page

    Why this matters

    A block signed off at 100% line coverage with a FIFO corner never exercised: every line ran, but never with the FIFO full and a write arriving together.

    After this lesson you can: turn a spec into a small coverage plan, write the covergroup that mirrors it, close its holes with directed cases, and explain why code coverage is not enough.

    From spec to plan

    A coverage model starts in the spec, not in code. Scenarios move from the spec into a numbered plan, and only then into covergroups whose bins map one-to-one onto plan items. The worked example is a small ALU.

    Item

    Scenario

    CP-1

    Every opcode executed at least once

    CP-2

    Zero flag seen asserted and deasserted

    CP-3

    Carry seen asserted for ADD and for SUB

    CP-4

    Operands a and b each seen at 0 and at 255

    CP-5

    Every opcode executed with a zero result

    CP-6

    Shift by 0 and shift by 7 for SHL

    Each plan item is a sentence a non-programmer can check off. The covergroup makes every item mechanically measurable by name.

    A covergroup that mirrors the plan

    The next listing is not run in this course's checks: the open simulators do not count covergroups. The bench at the end of the lesson counts the same bins with the course's bins model.

    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
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    typedef enum logic [2:0] { ADD, SUB, AND_, OR_, XOR_, SHL } op_e;
    
    class alu_coverage;
      covergroup cg with function sample(op_e op, bit [7:0] a, b, y,
                                         bit zero, bit carry);
        option.per_instance = 1;
        option.name = "alu_cov";
    
        // CP-1: enum gives one named bin per opcode, automatically
        cp_op : coverpoint op;
    
        // CP-2
        cp_zero : coverpoint zero {
          bins asserted   = {1};
          bins deasserted = {0};
        }
    
        // CP-3: carry is meaningful only for ADD and SUB
        cp_carry : coverpoint carry iff (op inside {ADD, SUB}) {
          bins carry_set   = {1};
          bins carry_clear = {0};
        }
    
        // CP-4: operand extremes
        cp_a_ext : coverpoint a { bins min_v = {0}; bins max_v = {255}; }
        cp_b_ext : coverpoint b { bins min_v = {0}; bins max_v = {255}; }
    
        // CP-5: every opcode with a zero result
        x_op_zero : cross cp_op, cp_zero {
          ignore_bins nz = binsof(cp_zero.deasserted);
        }
    
        // CP-6: shift amount extremes (b[2:0] is the shift count)
        cp_shamt : coverpoint b[2:0] iff (op == SHL) {
          bins sh0 = {0};
          bins sh7 = {7};
        }
      endgroup
    
      function new();
        cg = new();
      endfunction
    endclass

    One coverpoint or cross per plan item, with bins named after the item. The iff guards keep the model measuring only meaningful combinations.

    The filtered cross holds 6 bins: 6 opcodes times the asserted half of cp_zero. Only lower-case and, or and xor are keywords, so AND would also be legal; the underscore in AND_ is just a readability choice.

    The wrapper that carries it

    Before IEEE 1800-2023 a covergroup could not be extended; 2023 adds covergroup extends, but tool support varies. A covergroup also travels badly on its own. The standard reuse unit is a wrapper class: it owns construction, names each instance, and exposes a stable sampling API.

    The next listing is not run in this course's checks: the open simulators do not count covergroups. The bench at the end of the lesson counts the same bins with the course's bins model.

    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
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    class alu_cov_collector;
      covergroup cg (string inst_name) with function sample(op_e op, bit zero);
        option.per_instance = 1;
        option.name = inst_name;      // one report row per instance
        cp_op : coverpoint op;        // 6 named bins, one per opcode
        cp_zero : coverpoint zero {
          bins asserted   = {1};
          bins deasserted = {0};
        }
      endgroup
    
      function new(string inst_name);
        cg = new(inst_name);          // covergroup new() takes arguments
      endfunction
    
      function void sample_txn(op_e op, bit zero);
        cg.sample(op, zero);
      endfunction
    
      function real get_pct();
        return cg.get_inst_coverage();
      endfunction
    endclass
    
    // Merged reporting hides weak instances; per-instance reporting does not:
    // cpu_cov = new("cpu_port");   -> report row "cpu_port"
    // dma_cov = new("dma_port");   -> report row "dma_port"

    Each collector object owns one covergroup instance with its own report row. One strong port can no longer mask a weak one.

    • The wrapper gives construction control, reuse across testbenches, and a stable sample_txn API.

    • Per-instance reporting plus distinct names keeps multi-port coverage honest.

    • A covergroup type may also be declared at package scope. The sampling context is what limits reuse, which is why the wrapper class still wins.

    • Argument plumbing, value versus ref formals, and instance merging are detailed in the Options topic.

    Related topics

    • Per-instance options

    • Covergroup arguments

    Testbench

    The next listing is not run in this course's checks: the open simulators do not count covergroups. It is the complete self-checking testbench to write on a commercial simulator; the bench after it counts the same plan with the bins model.

    tb.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
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    typedef enum logic [2:0] { ADD, SUB, AND_, OR_, XOR_, SHL } op_e;
    
    task automatic alu_model(input op_e op, input bit [7:0] a, b,
                             output bit [7:0] y, output bit carry);
      carry = 1'b0;
      case (op)
        ADD:  {carry, y} = {1'b0, a} + {1'b0, b};
        SUB:  {carry, y} = {1'b0, a} - {1'b0, b};
        AND_: y = a & b;
        OR_:  y = a | b;
        XOR_: y = a ^ b;
        SHL:  y = a << b[2:0];
      endcase
    endtask
    
    class alu_coverage;
      covergroup cg with function sample(op_e op, bit [7:0] a, b, y,
                                         bit zero, bit carry);
        option.per_instance = 1;
        option.name = "alu_cov";
        cp_op : coverpoint op;
        cp_zero : coverpoint zero {
          bins asserted   = {1};
          bins deasserted = {0};
        }
        cp_carry : coverpoint carry iff (op inside {ADD, SUB}) {
          bins carry_set   = {1};
          bins carry_clear = {0};
        }
        cp_a_ext : coverpoint a { bins min_v = {0}; bins max_v = {255}; }
        cp_b_ext : coverpoint b { bins min_v = {0}; bins max_v = {255}; }
        x_op_zero : cross cp_op, cp_zero {
          ignore_bins nz = binsof(cp_zero.deasserted);
        }
        cp_shamt : coverpoint b[2:0] iff (op == SHL) {
          bins sh0 = {0};
          bins sh7 = {7};
        }
      endgroup
      function new();
        cg = new();
      endfunction
    endclass
    
    module tb;
      alu_coverage cov = new();
      op_e       op;
      bit [7:0]  a, b, y;
      bit        zero, carry;
      real       cov_pct;
    
      task automatic directed(op_e o, bit [7:0] x, bit [7:0] z);
        op = o; a = x; b = z;
        alu_model(op, a, b, y, carry);
        zero = (y == 0);
        cov.cg.sample(op, a, b, y, zero, carry);
      endtask
    
      initial begin
        repeat (2000) begin
          assert(std::randomize(op, a, b));
          alu_model(op, a, b, y, carry);
          zero = (y == 0);
          cov.cg.sample(op, a, b, y, zero, carry);
        end
        // Directed top-up for the CP-5 cells uniform random rarely hits:
        directed(OR_,  8'h00, 8'h00);   // OR is zero only for a == b == 0 (1 in 65536)
        directed(ADD,  8'h01, 8'hFF);   // ADD wraps to zero (1 in 256); also b == 255
        directed(SUB,  8'hFF, 8'hFF);   // SUB of equal values (1 in 256); also a == 255
        directed(XOR_, 8'h5A, 8'h5A);   // XOR of equal values (1 in 256)
        cov_pct = cov.cg.get_inst_coverage();
        if (cov_pct >= 99.9)
          $display("PASS: ALU coverage closes at %0.1f%%", cov_pct);
        else
          $fatal(1, "FAIL: ALU coverage stalls at %0.1f%%", cov_pct);
        $finish;
      end
    endmodule

    The same plan counted with the bins model, from 2000 random transactions and then a directed top-up:

    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
    // uses: covmodel
    // The ALU plan counted with the bins model: random stimulus first, then the directed top-up.
    typedef enum int {ADD, SUB, AND_, OR_, XOR_, SHL} op_e;
    
    class alu_coverage;
      cm_group cg = new("alu_cov");
      cm_point cp_op = new("cp_op"), cp_zero = new("cp_zero"), cp_carry = new("cp_carry");
      cm_point cp_a_ext = new("cp_a_ext"), cp_b_ext = new("cp_b_ext"), cp_shamt = new("cp_shamt");
      cm_cross x_op_zero;
      function new();
        cp_op.add("add", ADD, ADD); cp_op.add("sub", SUB, SUB); cp_op.add("and_", AND_, AND_);
        cp_op.add("or_", OR_, OR_); cp_op.add("xor_", XOR_, XOR_); cp_op.add("shl", SHL, SHL);   // CP-1
        cp_zero.add("deasserted", 0, 0); cp_zero.add("asserted", 1, 1);                          // CP-2
        cp_carry.add("carry_clear", 0, 0); cp_carry.add("carry_set", 1, 1);                      // CP-3
        cp_a_ext.add("min_v", 0, 0); cp_a_ext.add("max_v", 255, 255);                            // CP-4
        cp_b_ext.add("min_v", 0, 0); cp_b_ext.add("max_v", 255, 255);
        cp_shamt.add("sh0", 0, 0); cp_shamt.add("sh7", 7, 7);                                    // CP-6
        x_op_zero = new("x_op_zero", cp_op, cp_zero);                                            // CP-5
        x_op_zero.exclude("", "deasserted");                                                     // ignore_bins nz
        cg.points = '{cp_op, cp_zero, cp_carry, cp_a_ext, cp_b_ext, cp_shamt};
        cg.crosses.push_back(x_op_zero);
      endfunction
      function void sample(op_e op, bit [7:0] a, b, bit zero, carry);
        cp_op.sample(op); cp_zero.sample(zero); x_op_zero.sample();
        if (op inside {ADD, SUB}) cp_carry.sample(carry);                                        // iff (op inside {ADD, SUB})
        cp_a_ext.sample(a); cp_b_ext.sample(b);
        if (op == SHL) cp_shamt.sample(b[2:0]);                                                  // iff (op == SHL)
      endfunction
    endclass
    
    Terminal
    after 2000 random transactions:
    covergroup alu_cov: 97.62% (goal 100)
      coverpoint cp_op: 100.00% (6 of 6 bins)
        add            339
        sub            316
        and_           371
        or_            343
        xor_           315
        shl            316
      coverpoint cp_zero: 100.00% (2 of 2 bins)
    ... 6 more lines ...
        min_v          5
        max_v          7
      coverpoint cp_b_ext: 100.00% (2 of 2 bins)
        min_v          8
        max_v          6
      coverpoint cp_shamt: 100.00% (2 of 2 bins)
        sh0            39
        sh7            47
      cross x_op_zero: 83.33% (5 of 6 bins)
        hole: <or_, asserted>
    after the directed top-up: 100.00%
    PASS: random stimulus left named holes, and six directed cases closed the model at 100%

    The report reads like the plan: every coverpoint is full, and the one hole is a named cross bin, <or_, asserted>. OR gives zero only when both operands are zero, 1 in 65,536 for uniform operands, so 2000 random transactions almost never produce it. The group sits at 97.62%, the mean of its seven members. Six directed cases close it.

    1. Extract scenarios from the spec into a numbered, reviewable plan.

    2. Implement one coverpoint or cross per plan item, with bins named after the item.

    3. Run random stimulus, then query get_inst_coverage() and dump the database.

    4. Read holes as missing scenarios, not missing percentage.

    5. Close holes with constraint tweaks or small directed cases, then rerun.

    Sign-off needs both

    Code coverage is extracted automatically from RTL structure: lines, branches, toggles. It answers whether the implementation ran. Functional coverage is written from the spec. It answers whether required scenarios happened.

    Code coverage

    Functional coverage

    Source

    RTL structure

    The specification

    Written

    Automatically, by the tool

    By the verification engineer

    Measures

    Implementation exercised

    Scenarios that occurred

    100% means

    All written code ran

    All planned scenarios hit

    Blind to

    Missing code, scenario meaning

    Anything not in the coverage model

    Effort

    A switch

    A plan and covergroups

    The FIFO corner below is the classic proof. Both lines run, yet the killing combination never occurs.

    The next listing is not run in this course's checks: the open simulators do not count covergroups. The bench at the end of the lesson counts the same bins with the course's bins model.

    fifo_wr.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
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
    module fifo_wr (input logic clk, input logic rst_n,
                    input logic push, pop, input logic full, input logic empty,
                    input logic [7:0] wdata, input logic [2:0] wr_ptr,
                    output logic [2:0] rd_ptr);
      logic [7:0] mem [8];
    
      // Line A runs on any normal push; line B on any normal pop.
      // Pushes until full plus pops until empty give 100% line,
      // toggle and branch coverage: yet push && pop && full NEVER OCCURS.
      // BUG: simultaneous push && pop while FULL corrupts the count
      // elsewhere in the design.
      always_ff @(posedge clk) begin
        if (push && !full)
          mem[wr_ptr] <= wdata;
        if (pop && !empty)
          rd_ptr <= rd_ptr + 1;
      end
    
      // The functional coverage that names the gap:
      covergroup cg_fifo @(posedge clk);
        cp_corner : coverpoint {push, pop, full} iff (rst_n) {
          bins push_pop_full = {3'b111};   // unhit means the scenario was never tried
        }
      endgroup
    
      cg_fifo cg_inst = new();
    endmodule

    The unhit bin points at the exact stimulus gap. Line coverage had no vocabulary to even ask the question.

    Verilator measures code coverage for real, so the claim can be checked. The bench fills a small FIFO, drains it, and samples the corner bin with the bins model:

    fifo4.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
    // uses: covmodel
    // A 4-deep FIFO, a test that fills it and drains it, and the one corner both miss.
    module fifo4 (
      input  logic clk, rst_n, push, pop,
      input  logic [7:0] wdata,
      output logic [7:0] rdata,
      output logic full, empty
    );
      logic [7:0] mem [4];
      logic [1:0] wp, rp;
      logic [2:0] count;
      assign full  = (count == 4);
      assign empty = (count == 0);
      assign rdata = mem[rp];
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin wp <= 0; rp <= 0; count <= 0; end
        else begin
          if (push && !full) begin mem[wp] <= wdata; wp <= wp + 1; end
          if (pop && !empty) rp <= rp + 1;
          count <= count + (push && !full) - (pop && !empty);
        end
    endmodule
    
    module tb_fifo_codecov;
      logic clk = 0, rst_n = 0, push = 0, pop = 0;
      logic [7:0] wdata = 0, rdata;
      logic full, empty;
      always #5 clk = ~clk;
      fifo4 dut (.*);
      cm_point cp_corner = new("cp_corner {push, pop, full}");
      always @(posedge clk) if (rst_n) cp_corner.sample({push, pop, full});
      initial begin
        cp_corner.add("push_pop_full", 3'b111, 3'b111);
        repeat (2) @(negedge clk);
        rst_n = 1;
        repeat (5) begin @(negedge clk) push = 1; wdata = wdata + 1; end   // fill, and one push while full
        @(negedge clk) push = 0;
        repeat (5) begin @(negedge clk) pop = 1; end                        // drain, and one pop while empty
        @(negedge clk) pop = 0;
        repeat (2) @(negedge clk);
        cp_corner.report();
        $display("PASS: the fill-and-drain test finished; compare the corner bin with the line coverage of fifo4");
        $finish;
      end
    endmodule
    Terminal
      coverpoint cp_corner {push, pop, full}: 0.00% (0 of 1 bins)  below goal 100
        push_pop_full  0     <- hole
    PASS: the fill-and-drain test finished; compare the corner bin with the line coverage of fifo4

    Built a second time with line coverage on (verilator --binary --coverage-line, with the FIFO in its own file fifo4.sv), the same test writes coverage.dat, and verilator_coverage --annotate-min 1 --annotate-all --annotate annot coverage.dat annotates the FIFO. Every line ran, both branches of every if included, while the corner bin above reads 0:

    Terminal
    //      // verilator_coverage annotation
            module fifo4 (
              input  logic clk, rst_n, push, pop,
              input  logic [7:0] wdata,
              output logic [7:0] rdata,
              output logic full, empty
            );
              logic [7:0] mem [4];
              logic [1:0] wp, rp;
              logic [2:0] count;
              assign full  = (count == 4);
              assign empty = (count == 0);
              assign rdata = mem[rp];
     000016   always_ff @(posedge clk or negedge rst_n)
     000014     if (!rst_n) begin wp <= 0; rp <= 0; count <= 0; end
     000014     else begin
     000010       if (push && !full) begin mem[wp] <= wdata; wp <= wp + 1; end
     000010       if (pop && !empty) rp <= rp + 1;
     000014       count <= count + (push && !full) - (pop && !empty);
                end
            endmodule

    Recorded with Verilator 5.048 on 2026-10-18. The counts at the left margin are how many times each line ran; a line that never ran would show %000000. Line coverage is complete and the FIFO's one dangerous scenario was never tried. That is the whole argument for functional coverage, measured on one design.

    Functional coverage low

    Functional coverage high

    Code coverage high

    Stimulus wanders; scenarios never formed

    Sign-off candidate: review waivers, then close

    Code coverage low

    Early bring-up: keep testing

    Coverage model too thin, or dead RTL: investigate

    An assertion counter is a different construct: SystemVerilog cover property counts assertion matches, not covergroup bins. Both appear in reports, so name which one each number came from.

    Common pitfall

    Reporting full line coverage as verification complete is the classic trap. Missing code has no line to cover, and covered lines never promise that their combinations happened.

    Interview

    “Do code and functional coverage agree at sign-off?”

    Define each in one sentence, give the FIFO miss as the example, and close with both metrics at goal plus reviewed waivers.

    Check yourself Random stimulus hits every opcode and both zero outcomes, yet x_op_zero sits at 83.3 percent. What is missing?

    One opcode never produced a zero result: five of six asserted bins hit. Add the directed case the hole names.

    Check yourself Code coverage reads high and functional coverage reads low. What does the quadrant say?

    Stimulus wanders without forming planned scenarios. Steer it with constraints or directed cases instead of running longer.

    Remember

    • Plan rows first, then one bin per row.

    • Random stimulus leaves named holes; directed cases close them.

    • Full line coverage can sit beside a 0% functional corner.

    Rule of thumb: sign off on functional and code coverage together.

    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.

    Random stimulus left named holes, and six directed cases closed the model at 100%.

    The FIFO test was also built with verilator --binary --coverage-line and annotated with verilator_coverage on 2026-10-18: every line and both sides of every branch ran while the corner bin read 0.

    Related topics

    • Coverage plan

    • Sign-off and metrics

    Previous

    144 / 259

    Next: Bins: Explicit, Auto, Illegal & Ignore

    Up next

    Up nextBins: Explicit, Auto, Illegal & Ignore

    Sign in to save progress

    Practice this lessonReport an error

    See also

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