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

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs
    • Races and Simulation-Synthesis Mismatches
    • Latches and Combinational Loops
    • CDC Bugs
    • Reset-Domain Crossing Bugs
    • Timing-Only Bugs
    • Protocol Violations
    • Hangs and Deadlocks
    • Starvation and Livelock
    • Performance Bugs
    • Power-Intent Bugs
    • DFT-Mode Bugs
    • Configuration and Register-Map Bugs
    • Testbench Bugs That Look Like Design Bugs
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 5 · Bug Classes
  1. Topics
  2. The Art of Hardware Debug
  3. Part 5 · Bug Classes
  4. Testbench Bugs That Look Like Design Bugs

Part 5 · Bug Classes · Working (W)

Testbench Bugs That Look Like Design Bugs

Bench bugs that look like design bugs, beyond a wrong expected value: a monitor that samples just after the edge, a checker disabled by mistake, and a race between the bench's drive and the design's edge. Their signatures, the first three checks, and real reproductions in one self-checking bench and a two-line race that two simulators resolve differently.

On this page

    Why this matters

    A bench bug either blames a correct design, which costs the designer days, or passes a broken one, which ships the bug. Sampling, races and disabled checkers have signatures of their own, different from a wrong expected value, and each takes minutes to confirm once you know it.

    After this lesson you can: recognise a monitor that samples at the wrong time, a race between bench and design, and a checker that never runs, confirm each by changing only the bench, and add the rules that stop them.

    Note

    Level: Working (W) · Debug-loop step: Localise

    Before this: When the Bug Is in the Test, Differential Debugging

    A stage that drops words only under back-pressure

    A valid/ready register stage fails its stream check: got 4, expected 1. Words seem to come out of order, but only in tests where the sink stalls; with the sink always ready, every test passes. The designer's assertions on the stage are quiet. A second stage, which really does lose words, passes a colleague's regression without a single error. Two benches, two wrong answers. How do you tell which bench to trust?

    What people usually do, and why it fails

    When the bug is in the test taught the method: check the design against the spec at the first wrong cycle, then check the checker. Its examples were wrong expected values: a model that misreads the spec or borrows the design's signals. That habit, reading the scoreboard's logic, misses three other bench bugs, because the scoreboard's logic is fine in all of them. The bug is in when the bench samples, when it drives, or whether the check runs at all.

    The signature

    Bench bug

    How it looks

    How to tell

    Wrong expected value

    a mismatch the designer can explain from the spec

    Part 3: quote the spec at the first wrong cycle

    Monitor samples at the wrong time

    mismatches only under stalls or gaps, one beat off; design-side assertions quiet

    move the sample point to the edge; the failure goes away with the design unchanged

    Race between bench and design

    result differs between simulators, or after an unrelated edit, by one cycle

    run the other simulator; drive on the other edge

    Checker disabled by mistake

    everything passes, including a design with a known bug

    print the checker's comparison count; run it on a mutant

    The monitor bug is easiest to see at one clock edge. The bench drives ready on the falling edge, so at the rising edge both sides of the handshake are stable:

    At a rising edge, a monitor that samples at the edge sees the word that moves. One that samples a moment later sees the next word paired with the old ready, and counts a transfer that did not happen.

    The first three checks

    These are Localise checks on the bench. Each changes only the bench, so a result that moves clears the design:

    1. Does the checker run? Print how many comparisons it made, next to how many it should have made, and run it once against a design with a known bug (a mutant). Zero comparisons, or a clean mutant, means it is blind.

    2. When does the bench sample and drive? Monitors should sample at the design's edge, before the design's registers update (a clocking block does this); drivers should change inputs away from that edge. Look for # delays in monitors and blocking drives on the clock edge.

    3. Does the failure move when only the bench's timing changes? Run the other simulator, move the sample point, or drive on the other edge. If the result moves with the design untouched, the bug is in the bench.

    One real reproduction: one stage, four monitors

    The bench drives two valid/ready stages with sequence numbers 1 to 60 and random back-pressure. One stage is correct; the other accepts a word while stalled and overwrites the one it holds, a real bug. Four copies of one monitor and in-order checker watch them: correct, sampling just after the edge, and with the check disabled by rst_n where !rst_n was meant.

    stage.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
    // stage: a valid/ready register stage. A word moves when valid && ready at a rising edge.
    // DROP = 0 is correct. DROP = 1 is a real design bug: it accepts a new word while stalled and
    // overwrites the one it holds, so words are lost under back-pressure.
    `timescale 1ns/1ns
    module stage #(parameter bit DROP = 0) (
      input  logic       clk, rst_n, in_valid, out_ready,
      input  logic [7:0] in_data,
      output logic       in_ready, out_valid,
      output logic [7:0] out_data
    );
      assign in_ready = DROP ? 1'b1 : (!out_valid || out_ready);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin out_valid <= 1'b0; out_data <= '0; end
        else if (in_valid && in_ready) begin out_valid <= 1'b1; out_data <= in_data; end
        else if (out_ready) out_valid <= 1'b0;
    endmodule
    
    // mon_chk: a monitor and an in-order checker on the stage's output. The words are sequence
    // numbers 1, 2, 3, ... so each accepted word must be one more than the last.
    //   LATE = 1: bench bug, the monitor samples just after the edge instead of at it.
    //   DIS  = 1: bench bug, the check is disabled by "rst_n" where "!rst_n" was meant.
    module mon_chk #(parameter bit LATE = 0, parameter bit DIS = 0) (
      input logic       clk, rst_n, valid, ready,
      input logic [7:0] data
    );
      logic [7:0] last = 8'd0;
      int         checks = 0, errors = 0;
      string      first = "";
      always @(posedge clk) begin
        if (LATE) #1;                              // values after the edge belong to the next cycle
        if (!(DIS ? rst_n : !rst_n) && valid && ready) begin
          checks++;
          if (data !== last + 8'd1) begin
            errors++;
            if (first == "") first = $sformatf("%0t ns: got %0d, expected %0d", $time, data, last + 8'd1);
          end
          last = data;
        end
      end
    endmodule
    
    // src: sends sequence numbers 1 to 60, with gaps. It drives on the falling edge, so nothing the
    // design samples changes at the design's own edge.
    module src (
      input  logic       clk, rst_n, in_ready,
      output logic       in_valid,
      output logic [7:0] in_data
    );
      int unsigned rng = 32'h1234_5678;
      bit took = 1'b0;
      initial begin in_valid = 1'b0; in_data = 8'd0; end
      always @(posedge clk) took = in_valid && in_ready;     // the handshake, sampled at the edge
      always @(negedge clk) if (rst_n) begin
        if (took) in_valid = 1'b0;                           // the word was taken at the last edge
        rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
        if (!in_valid && in_data < 8'd60 && (rng % 100) < 70) begin
          in_data = in_data + 8'd1;
          in_valid = 1'b1;
        end
      end
    endmodule
    
    Terminal
    $ iverilog -g2012 -o tb.vvp tb_bugs.sv && vvp -n tb.vvp
    good design,   monitor at the edge:     60 checks, clean
    good design,   monitor after the edge:  53 checks, 5 errors, first at 86 ns: got 4, expected 1
    broken design, checker on:              38 checks, 15 errors, first at 105 ns: got 5, expected 4
    broken design, checker disabled:        0 checks, clean
    PASS: a late monitor blames a good design; a disabled checker passes a broken one with 0 checks
    ...

    Verilator 5.048 prints the same five lines. Read them as a reviewer of the bench, not of the design.

    • Line 2 blames a correct design. The late monitor made 53 comparisons instead of 60 and reports got 4, expected 1: pairing each ready with the next cycle's word, it skipped words 1 to 3 and counted others at the wrong time, as in the picture above. Line 1, the same design and checker sampling at the edge, is clean with all 60. Only the bench changed, so the design is cleared.

    • Line 3 is a real design bug, and its message has the same shape as line 2: an unexpected number. Shape alone cannot tell a bench bug from a design bug; changing only the bench can.

    • Line 4 passes a broken design with 0 checks. rst_n is 1 for the whole test, so a check guarded by rst_n instead of !rst_n never runs. Without its count, this line is indistinguishable from line 1. The bench's own pass rule treats zero comparisons as a failure.

    A race between the bench and the design

    The third class needs no monitor at all. Here the bench drives the design's input with a blocking assignment on the design's own edge:

    dut.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // bad-example: a race, kept short so it can be run in both simulators (output below)
    // race_tb: the bench drives the DUT's input with a blocking assignment on the DUT's own edge.
    // Whether the flop sees the old or the new value depends on which process the simulator runs first.
    `timescale 1ns/1ps
    module dut (input logic clk, input logic [7:0] d, output logic [7:0] q);
      always_ff @(posedge clk) q <= d;
    endmodule
    
    module race_tb;
      logic clk = 1'b0;
      logic [7:0] d = 8'd0, q;
      always #5 clk = ~clk;
      dut u (.clk, .d, .q);
      initial begin
        repeat (4) begin @(posedge clk); d = d + 1; end   // bad: blocking drive on the DUT's edge
        @(negedge clk) $display("q=%0d after 4 drives", q);
        $finish;
      end
    endmodule
    Terminal
    $ iverilog -g2012 -o race.vvp race.sv && vvp -n race.vvp
    q=3 after 4 drives
    ...
    $ verilator --binary --timing -Wno-fatal --top-module race_tb -Mdir vlr race.sv && ./vlr/Vrace_tb
    q=4 after 4 drives
    ...

    Same code, two answers, one cycle apart. At each rising edge two processes run: the flop reads d, and the bench writes it. The language does not fix their order. Icarus ran the flop first and Verilator the bench first. Moving the drive to @(negedge clk) gives q=4 in both. A nonblocking drive on the rising edge removes the race in the language: the flop reads d before the update, so the standard answer is q=3, which Icarus prints. Verilator 5.048 still printed 4, a difference in how it schedules a process woken by the clock. So the portable rule is to drive away from the sampling edge, or through a clocking block, and not to rely on any one simulator's ordering.

    The fix pattern

    • Sampling: sample at the design's edge, before its registers update: in an always @(posedge clk) without delays, or with a clocking block's input skew. Never add a # delay to a monitor to "see the new value".

    • Races: drive inputs on the other edge or through a clocking block, and release reset between edges, never on one the bench also acts on.

    • Disabled checkers: checks on by default, a disable that is explicit and logged, and conditions written in the same polarity as the reset they name.

    Preventing it

    • Count every check and fail on zero. Every checker prints its comparison count at the end; the test fails if a checker that should have run made none. The bench above does this.

    • Keep a few mutants in the regression, each expected to fail, as Part 3 recommends. A disabled or late checker turns one of them green the day it breaks.

    • Run part of the regression in a second simulator. A race shows up as a difference before it shows up as a bug report.

    • The review questions: on which edge does each monitor sample and each driver drive, and how many comparisons did this checker make in this test? See UVM's false-pass playbook for the UVM forms (objections, report catchers, disabled checks).

    Traps

    Common pitfall

    Racing at reset release. A first draft of this page's bench released reset from an initial block on a falling edge, while the source and sink also acted on falling edges. Icarus and Verilator then disagreed by one cycle (first at 86 ns against 76 ns) with the design unchanged. Releasing reset between edges (#22) made them agree.

    Common pitfall

    A source that samples its own handshake late. The source must decide that a word was taken from valid && ready at the rising edge, not from ready half a cycle later. A source that uses the late ready sends some words twice: with it, the correct monitor on the correct design reported 66 checks and 6 errors. The took flag in src is the fix.

    Why this matters

    Senior lens. When a bench and a design disagree, make the bench prove itself first with two cheap runs: the checker against a mutant, and the failing test in a second simulator. Both take minutes and clear or convict the bench before anyone reads RTL.

    Ask for comparison counts in every regression summary. A checker whose count drops to zero after a refactor is the most dangerous kind of green, and the count is the only line that shows it. UVM's debug lessons cover the testbench-side tooling.

    Interview

    Asked: "A scoreboard reports mismatches only when the sink applies back-pressure. With ready always high, everything passes. Design bug or bench bug?"

    Weak answer: It must be a design bug in the back-pressure path, because that is the only time it fails. I would look at the ready logic in the RTL.

    Strong answer: It could be either, and back-pressure is also exactly when a monitor's sampling point matters, because valid, data and ready stop moving together. I would check when the monitor samples: at the edge before the design updates, or after it. Then I would change only the bench, for example sample at the edge or run the other simulator, and see whether the failure moves. If design-side assertions on valid/ready are quiet and the failure disappears with the design unchanged, it is the bench. I would also check the checker's comparison count against the number of words sent.

    Follow-ups: (1) How would a clocking block change this? (2) How would you catch a checker that has silently stopped checking?

    Check yourself A test passes in Icarus and fails in Verilator, one cycle late, after a colleague moved a driver from the falling edge to the rising edge. Design or bench, and how do you check?

    Most likely a bench race: a driver that changes the design's input on the edge the design samples on gives an order the language leaves open, as race.sv showed. Move the drive back to the falling edge (or into a clocking block) with the design unchanged; if both simulators then agree and the spec's latency is met, it was the bench.

    Check yourself Every test in a regression passes, including one written last week against a known design bug. What do you look at first?

    The checker, not the design: a test that should fail and passes means the check is not running or not seeing the bug. Print its comparison count and its enable condition; a disable written with the wrong polarity, as in line 4, makes zero comparisons and reports clean. Then rerun the known-bug test and make sure it fails.

    Remember

    • Mismatches that appear only under back-pressure or gaps, off by one beat, while design-side assertions stay quiet: check when the monitor samples.

    • A result that changes with the simulator or an unrelated edit, by exactly one cycle, is a race. Drive away from the edge the design samples on.

    • A checker that made zero comparisons has not passed anything. Every checker reports its count, and zero is a failure.

    Rule of thumb: if the failure moves when you change only the bench's timing, the bug is in the bench.

    How this was checked

    tb_bugs.sv as shown, 2026-10-18: iverilog -g2012 -o tb.vvp tb_bugs.sv && vvp -n tb.vvp (Icarus Verilog 13.0) and verilator --binary --timing --assert -Wno-fatal --top-module tb_bugs -Mdir vl tb_bugs.sv && ./vl/Vtb_bugs (Verilator 5.048) print the same five lines. An earlier draft that released reset from an initial block on a falling edge printed first at 86 ns in Icarus and first at 76 ns in Verilator for the late monitor. A draft whose source tested in_valid && in_ready on the falling edge made the correct monitor report 66 checks, 6 errors on the correct design in both simulators.

    race.sv as shown: iverilog -g2012 -o race.vvp race.sv && vvp -n race.vvp prints q=3, verilator --binary --timing -Wno-fatal --top-module race_tb -Mdir vlr race.sv && ./vlr/Vrace_tb prints q=4. With the drive moved to @(negedge clk) both print q=4. With d <= d + 1 on the rising edge, Icarus prints 3 (the answer the language's scheduling rules give) and Verilator 4.

    Limits: small benches with plain modules, not UVM; clocking blocks are named as a cure but not run here. The blocking-drive outcome is what these two tool versions did; the language leaves that order open, so another version may differ.

    Keep going

    • Practice: Monitor sampling races

    • Practice: Monitor and scoreboard timestamp drift: false mismatches

    • Practice: Race conditions without clocking blocks

    • Practice: Report catcher abuse: swallowing real errors

    • Code lab: Clocking block

    • Code lab: Passive monitor only

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • When the bug is in the test: a wrong scoreboard

    • UVM: debugging the testbench

    • UVM: the mismatch playbook

    • SystemVerilog: anatomy of a race

    • SystemVerilog: the clocking-block cure

    • Part 10: case studies, including the testbench that was wrong

    Previous

    55 / 107

    Next: Part 6 · Debug by Stage

    Up next

    Up nextPart 6 · Debug by Stage

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs