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
  • Testbench Architecture in SystemVerilog23
    • Testbench Anatomy & Layering
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
      • Monitor Design
      • Scoreboard Architecture
      • Compare and End-of-Test
      • Reference Models and Coverage
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Monitors, Scoreboards & Reference Models
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Monitors, Scoreboards & Reference Models
  5. Compare and End-of-Test

Part 6 · Testbench Architecture · Intermediate

Compare and End-of-Test

Masked compares, first-difference dumps, drain windows, watchdogs and empty-scoreboard guards.

On this page

    Masked compare

    Not every field participates in every compare. Timestamps differ by construction, debug ids are bench bookkeeping, and some payloads are don't-care in specific modes. Encode that as a field mask.

    The next listing is a fragment that uses bus_txn from the scoreboard lesson, so this course's checks do not run it. The mask is chosen by the scoreboard per the spec, never hard-coded in the transaction.

    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: fragment: uses bus_txn from the scoreboard-architecture listing, no top module
    class scoreboard;
      mailbox #(bus_txn) mbx_act;
      bus_txn expected_q[$];
      int unsigned match_count, diff_count;
      int unsigned max_diffs = 10;   // stop the cascade
      bit stop;
    
      function void check_pair(bus_txn exp, bus_txn act, int unsigned mask);
        string desc;
        if (exp.do_compare(act, mask, desc)) begin
          match_count++;
          return;
        end
        diff_count++;
        // First reports carry everything: both transactions, fields, index, time.
        // The format is one string literal inside $sformatf: Icarus 13 accepts a
        // concatenation as a format, but Verilator 5.048 prints it as a number.
        $error("%s", $sformatf(
          "[SCB] DIFFERENCE #%0d at compare index %0d, time %0t\n  fields :%s\n  expect : %s\n  actual : %s",
          diff_count, match_count + diff_count, $time,
          desc, exp.convert2string(), act.convert2string()));
        if (diff_count >= max_diffs)
          $fatal(1, "[SCB] difference threshold (%0d) reached", max_diffs);
      endfunction
    endclass
    
    // Scoreboard call site: payload is don't-care on an error response
    // mask = (exp.resp != 0) ? (bus_txn::F_ALL & ~bus_txn::F_DATA)
    //                        : bus_txn::F_ALL;

    Compare rule

    Reason

    Use !== on 4-state fields

    An X in DUT output must fail, not silently match.

    Never compare timestamps or debug ids

    Metadata differs by construction and always false-alarms.

    Name every differing field with both values

    The report then writes itself.

    Cap reports with a threshold

    The first difference is signal; the cascade is noise.

    Common pitfall

    Printing only “expected X got Y” without the full transactions forces a re-run to debug. No report cap turns one alignment slip into a gigabyte log.

    Check yourself The scoreboard prints 10,000 differences. Where do you start?

    At the first one only. Check stream alignment first via monitor counts, recompute that case against the spec by hand, and open waveforms at its recorded time.

    When streams fall out of step

    One dropped transaction misaligns every later FIFO compare. Triage by what differs, not by how many reports arrived.

    First report shows

    Likely cause

    First check

    All fields differ, queue out of step

    Dropped or duplicated transaction upstream

    Monitor counts on both sides.

    Data only, address and length match

    Datapath bug or model formula bug

    Recompute from the spec by hand.

    X or Z in actual fields

    Uninitialized RTL or sampling race

    Reset coverage and clocking-block use.

    Drain, watchdog, final checks

    A test ends at quiescence, not at the last stimulus. In-flight transactions must flush through the DUT before the verdict.

    The next listing is a fragment that uses classes from earlier listings, so this course's checks do not run it. The env stores the generator, driver, monitor and clock handles the end of test depends on.

    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
    // not-run: fragment: uses generator, driver, bus_monitor, scoreboard and bus_if from earlier listings, no top module
    class env;
      virtual bus_if.MON vif;   // clock the drain waits on
      generator          gen;     // done_flag set after the last put
      driver             drv;     // n_done counts completed handshakes
      bus_monitor        mon_out;
      scoreboard         scb;
    
      task run_test();
        fork : tb_threads
          begin
            // 1. stimulus exhausted: every item generated AND driven
            wait (gen.done_flag && drv.n_done == gen.n);
            drain();              // 2. wait for in-flight activity to flush
            mon_out.stop = 1;     // 3. loops exit at their next iteration
            scb.stop = 1;
            scb.final_check();    // 4. leftovers, undrained mailboxes, zero-compare
            scb.report();
          end
          watchdog();             // global hang protection, whole test
        join_any
        disable tb_threads;
      endtask
    
      // Drain: wait until nothing has moved for 100 cycles
      task drain();
        int idle = 0;
        int unsigned last_seen = mon_out.txn_count;
        while (idle < 100) begin
          @(posedge vif.clk);
          if (mon_out.txn_count != last_seen) begin
            last_seen = mon_out.txn_count;
            idle = 0;             // activity restarts the window
          end else idle++;
        end
      endtask
    
      task watchdog();
        #1ms;
        $fatal(1, "[ENV] WATCHDOG: test exceeded 1ms");
      endtask
    endclass
    
    // Inside scoreboard: final_check fails leftovers, undrained actuals,
    // a zero-compare run, and any recorded difference before report() prints.

    The zero-compare guard is the most valuable line: a run with no compares at all must fail, never pass. Stronger benches also require a per-test minimum compare count.

    Interview

    Two staples live here: “How do you know a test is done?” (quiescence: drain plus leftover checks plus watchdog) and “How could a test pass while checking nothing?” (the zero-compare trap and its counter-check).

    Check yourself Stimulus finished 100 cycles ago and the output monitor is still counting. Is the test done?

    No. Transactions are still in flight. Ending now discards pending compares, possibly the failing one. Wait for the drain window, then check leftovers.

    Testbench

    A compare function and a verdict function, plus a testbench that runs them on a masked compare, a first difference, a leftover, an X, and an empty 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
    module tb_compare_eot;
      timeunit 1ns; timeprecision 1ps;
      reg [31:0] exp_data [0:3];
      reg [31:0] act_data [0:3];
      reg [1:0]  exp_resp [0:3];
      bit        has_actual [0:3];
      int errors = 0;
      int match_count = 0, diffs = 0, leftovers = 0;
      int first_diff = -1;
    
      // Compare engine: payload is don't-care on an error response, and
      // !== makes an X in the actual data a difference.
      function automatic bit pair_differs(input [31:0] e, input [31:0] a,
                                          input [1:0] resp);
        return (resp == 2'd0) && (e !== a);
      endfunction
      // Verdict: something compared, no difference, nothing left over.
      function automatic bit verdict_pass(input int nmatch, input int ndiff,
                                          input int left);
        return (nmatch + ndiff > 0) && (ndiff == 0) && (left == 0);
      endfunction
    
      initial begin
        exp_data[0] = 32'hAAAA; exp_resp[0] = 2'd0;
        act_data[0] = 32'hAAAA; has_actual[0] = 1;      // identical
        exp_data[1] = 32'hBBBB; exp_resp[1] = 2'd2;
        act_data[1] = 32'hBEEF; has_actual[1] = 1;      // error response: masked
        exp_data[2] = 32'hCCCC; exp_resp[2] = 2'd0;
        act_data[2] = 32'hCCD0; has_actual[2] = 1;      // real data difference
        exp_data[3] = 32'hDDDD; exp_resp[3] = 2'd0;
        has_actual[3] = 0;                              // dropped: leftover
        #1;
        for (int i = 0; i < 4; i++) begin
          if (!has_actual[i]) leftovers++;
          else if (pair_differs(exp_data[i], act_data[i], exp_resp[i])) begin
            diffs++;
            if (first_diff == -1) first_diff = i;
          end
          else match_count++;
        end
        if (match_count !== 2) errors++;
        if (diffs !== 1) errors++;
        if (first_diff !== 2) errors++;
        if (leftovers !== 1) errors++;
        if (!pair_differs(32'hCCCC, 32'hxxxx_xxxx, 2'd0)) errors++;  // X must differ
        if (verdict_pass(match_count, diffs, leftovers)) errors++;   // this run fails
        if (verdict_pass(0, 0, 0)) errors++;                         // empty run fails
        if (!verdict_pass(3, 0, 0)) errors++;                        // clean run passes
        if (errors == 0) $display("PASS: masked compare matched 2, first difference at index 2, 1 leftover; X and empty run both fail the verdict");
        else $fatal(1, "FAIL: %0d checks wrong (hit=%0d diffs=%0d first=%0d leftovers=%0d)",
                    errors, match_count, diffs, first_diff, leftovers);
        $finish;
      end
    endmodule

    Related topics

    • Self-checking regressions

    Previous

    177 / 259

    Next: Reference Models and Coverage

    Up next

    Up nextReference Models and Coverage

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Monitor Design