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
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
    • The Off-by-One That Fails at Wrap
    • The Reset Value That Changed
    • The X That Hid for Thousands of Cycles
    • The Race Two Simulators Disagree On
    • The Bus Value That Was Never Sent
    • The Response to the Wrong Master
    • The Deadlock Under Back-Pressure
    • The Credit Leak That Hangs After an Hour
    • The 2x Throughput Regression
    • The Missing Isolation
    • The Test-Mode Leak
    • The Testbench That Was Wrong
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 10 · Case Studies
  1. Topics
  2. The Art of Hardware Debug
  3. Part 10 · Case Studies
  4. The Testbench That Was Wrong

Part 10 · Case Studies · Working (W)

The Testbench That Was Wrong

A checksum block fails one seed in four, always one below the reference model, and the bug is filed against the RTL. The hunt asks who is right first: a receiver check from the spec, a three-word packet worked by hand and a 100-seed sweep all side with the design. The model was wrong, and the fixed bench is proven against a deliberately broken design.

On this page

    Why this matters

    A wrong reference model fails a test exactly like a design bug, and the usual response is to change the design until the two agree. Asking who is right first, with a referee that shares no code with either side, settles it in minutes and stops a correct design being broken to match a broken model.

    After this lesson you can: decide whether the design or the bench is wrong with an independent referee, a minimal case checked by hand and a sweep, fix the bench, and prove the fixed bench still catches a real design bug.

    Note

    Level: Working (W) · Debug-loop step: The whole loop (won at Localise: deciding which side is wrong)

    Before this: When the Bug Is in the Test, The Debug Loop

    The report

    A small block, csum16, computes the 16-bit ones'-complement checksum of a packet, one word per cycle: add the words with end-around carry (a carry out of bit 15 goes back into bit 0) and output the complement of the sum. It has been in the nightly random regression for weeks. This week someone lengthened the packets to 32 words and made half the words ffff padding, and the nightly turned red:

    Terminal
    $ for s in $(seq 11 20); do vvp -n c0.vvp +SEED=$s | grep -E "^(MISMATCH|PASS)"; done
    PASS: seed 11, 2000 packets, dut matches model
    PASS: seed 12, 2000 packets, dut matches model
    MISMATCH seed=13 packet=387 words=32: csum dut=ffea model=ffeb
    PASS: seed 14, 2000 packets, dut matches model
    PASS: seed 15, 2000 packets, dut matches model
    MISMATCH seed=16 packet=118 words=32: csum dut=fffb model=fffc
    PASS: seed 17, 2000 packets, dut matches model
    MISMATCH seed=18 packet=1618 words=9: csum dut=fffa model=fffb
    MISMATCH seed=19 packet=459 words=19: csum dut=fff8 model=fff9
    PASS: seed 20, 2000 packets, dut matches model

    The bug report says: csum16 is one off in rare packets; looks like the end-around carry is added twice. Please fix the RTL. Here is the RTL:

    csum16.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // csum16: the 16-bit ones'-complement checksum of a packet of 16-bit words, one word per cycle.
    // Spec: add the words with end-around carry (a carry out of bit 15 is added back into bit 0),
    // and output the complement of the sum with the last word.
    `timescale 1ns/1ps
    module csum16 (
      input  logic        clk, rst_n,
      input  logic        in_valid, in_last,
      input  logic [15:0] in_data,
      output logic        out_valid,
      output logic [15:0] out_csum
    );
      logic [15:0] acc_q, acc_n;
      logic [16:0] s;
      assign s     = {1'b0, acc_q} + {1'b0, in_data};
      assign acc_n = s[15:0] + {15'd0, s[16]};            // end-around carry
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin acc_q <= '0; out_valid <= 1'b0; out_csum <= '0; end
        else begin
          out_valid <= in_valid && in_last;
          if (in_valid && in_last) begin out_csum <= ~acc_n; acc_q <= '0; end
          else if (in_valid)       acc_q <= acc_n;
        end
    endmodule

    And the bench's reference model, which computes the expected checksum for each packet the bench sends:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: the bench's model as it was (FIX = 0 in the full listing at the end of the page)
      // The reference model: sum in 32 bits, fold the carries back into 16 bits, complement.
      function automatic logic [15:0] model_csum();
        logic [31:0] sum = 0;
        foreach (pkt[i]) sum += {16'h0, pkt[i]};
        sum = {16'h0, sum[15:0]} + {16'h0, sum[31:16]};
        return ~sum[15:0];
      endfunction

    Stop here and try it. Four seeds in ten fail, and in every failure the design's checksum is exactly one below the model's. Is the design adding a carry it should not, or is something else going on? Before you open the RTL's carry line, decide how you would know.

    Check yourself Want a hint before you read the hunt?

    Look at the failing checksums: ffea, fffb, fffa, fff8. All are close to ffff, so the folded sums were all tiny. What does a 32-bit sum have to look like for its folded value to be tiny, and can one fold leave a carry behind?

    The hunt

    This hunt is won at Localise, by deciding which side of the comparison is wrong before debugging either, and closed at Prove with a mutant design.

    Reproduce: same packet, both simulators

    Terminal
    $ vvp -n c0.vvp +SEED=18
    MISMATCH seed=18 packet=1618 words=9: csum dut=fffa model=fffb
      packet: 1926 5fbb ffff 468f 8274 be1f ffff ffff ffff
    FATAL: csum_tb.sv:112: seed 18: 1 of 2000 packets mismatch
    ...
    $ verilator --binary --timing --assert -Wno-fatal --top-module csum_tb -GFIX=0 -Mdir vl0 csum_tb.sv > /dev/null 2>&1; vl0/Vcsum_tb +SEED=18
    MISMATCH seed=18 packet=1618 words=9: csum dut=fffa model=fffb
      packet: 1926 5fbb ffff 468f 8274 be1f ffff ffff ffff
    ...

    Seed 18 fails on packet 1,618 of 2,000, nine words long, the same in both simulators. Not a race, not X: two deterministic computations disagree. Collect what the report already says before forming any theory:

    • Always exactly one apart, and always in the same direction: design = model − 1.

    • Rare: one packet in a few thousand, on about one seed in four.

    • Always near ffff: every failing checksum is ffxx, so every failing sum folded to a small number.

    • It started when the packets got longer and fuller of `ffff`, which makes the 32-bit sum bigger, with more carries to fold.

    What usually happens next, and why it is a trap

    The report already blames the RTL, and the RTL has an obvious suspect: the end-around carry line. An afternoon goes into reading it, then into "fixing" it so that the design matches the model, perhaps by folding the carry differently. That afternoon assumes the answer to the only question that matters: of the two things that disagree, which one is wrong? The model is code too, written by a person on a deadline, and it was never tested against anything but this design.

    The method, which when the bug is in the test states in general: check the design against the spec at the failing case, then check the checker. Here, find a referee that shares no code with either side.

    Localise, referee 1: a check the spec gives for free

    A ones'-complement checksum has a property that needs no model at all: a receiver that adds every word and the checksum with end-around carry must get ffff. That is how the checksum is checked in use. Written one word at a time, it shares no code with the model's 32-bit sum-then-fold, and none with the RTL:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet: the receiver check added to the bench (full listing at the end of the page)
      function automatic bit receiver_ok(input logic [15:0] csum);
        logic [16:0] a = 0;
        foreach (pkt[i]) begin a = a[15:0] + pkt[i]; a = a[15:0] + a[16]; end
        a = a[15:0] + csum; a = a[15:0] + a[16];
        return a[15:0] == 16'hffff;
      endfunction
    Terminal
    $ vvp -n c0.vvp +SEED=18 +VERIFY
    MISMATCH seed=18 packet=1618 words=9: csum dut=fffa model=fffb
      packet: 1926 5fbb ffff 468f 8274 be1f ffff ffff ffff
      receiver check: dut ok, model BAD
    receiver check: dut checksum wrong in 0 of 2000 packets
    FATAL: csum_tb.sv:112: seed 18: 1 of 2000 packets mismatch
    ...

    On the failing packet, the design's checksum passes the receiver check and the model's fails it. And over all 2,000 packets of the seed, the design's checksum never fails. One referee has voted for the design.

    Localise, referee 2: work the failing packet by hand

    Nine words are few enough to add on paper. The model's way first: sum in 32 bits, then fold the upper half into the lower half once.

    Step

    Value

    Note

    32-bit sum of the nine words

    0005_ffff

    five carries out of bit 15

    Fold once: ffff + 0005

    1_0004

    17 bits: the fold itself carried

    The model keeps 16 bits

    0004

    the new carry is dropped; checksum ~0004 = fffb

    Fold again: 0004 + 1

    0005

    no carry left; checksum ~0005 = fffa, the design's answer

    There it is: the one fold produced a carry of its own, and the model threw it away. That can only happen when the low half of the sum plus the number of carries reaches 1_0000, so the folded value is tiny and the checksum is near ffff, which explains the third clue. The smallest packet that does it has three words. The bench's +CORNER mode sends it:

    Terminal
    $ vvp -n c0.vvp +CORNER +VERIFY
    MISMATCH seed=1 packet=1 words=3: csum dut=fffe model=ffff
      packet: ffff ffff 0001
      receiver check: dut ok, model BAD
    receiver check: dut checksum wrong in 0 of 3 packets
    FATAL: csum_tb.sv:112: seed 1: 1 of 3 packets mismatch
    ...

    By hand, as the spec says, one word at a time: ffff + ffff = 1_fffe, wrap the carry, ffff; ffff + 0001 = 1_0000, wrap the carry, 0001; checksum fffe. The design says fffe. The model's sum is 0001_ffff, one fold gives 1_0000, it keeps 0000 and says ffff. Second referee, same vote.

    Localise, referee 3: is it always the model?

    Two packets could be luck. A sweep over 100 seeds, counting both checks:

    Terminal
    $ for s in $(seq 1 100); do vvp -n c0.vvp +SEED=$s +VERIFY; done > sweep0.log 2>&1
    $ grep -c '^PASS' sweep0.log; grep -c '^MISMATCH' sweep0.log
    74
    26
    $ grep '^  receiver' sweep0.log | sort | uniq -c
      26   receiver check: dut ok, model BAD
    $ grep '^receiver' sweep0.log | sort | uniq -c
     100 receiver check: dut checksum wrong in 0 of 2000 packets

    26 seeds mismatch. In all 26 the receiver check sides with the design, and across 200,000 packets the design's checksum never fails it. The bug is in the bench, in one line of the model: it folds once where the spec's arithmetic needs a fold until no carry is left.

    Hypothesise and test: does the explanation predict the pattern?

    A cause is only confirmed when it explains every clue, not just the one you were looking at. This one does:

    Clue

    Explained by the single fold

    Always one apart, design = model − 1

    the dropped carry is worth exactly 1 in the sum, so the model's complement is 1 higher

    Always near ffff

    the fold only carries when the folded value wraps past ffff to a tiny number

    Rare, about one seed in four

    the low half plus the carry count must reach 1_0000, which few sums do

    Started with longer packets full of ffff

    bigger sums have more carries to fold, so more sums reach that boundary

    The fix and the lesson

    Fix: in the bench, not in the design

    design.sv
    // snippet: the fix, in model_csum()
        while (sum[31:16] != 0) sum = {16'h0, sum[15:0]} + {16'h0, sum[31:16]};

    Fold until nothing is left above bit 15. Here is the full bench as it stands after the hunt, with the receiver check behind +VERIFY, the corner packets behind +CORNER, and FIX choosing the old model (0) or the fixed one (1, the default). csum16 is repeated so the listing runs on its own:

    csum16.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // csum16: the 16-bit ones'-complement checksum of a packet of 16-bit words, one word per cycle.
    // Spec: add the words with end-around carry (a carry out of bit 15 is added back into bit 0),
    // and output the complement of the sum with the last word.
    `timescale 1ns/1ps
    module csum16 (
      input  logic        clk, rst_n,
      input  logic        in_valid, in_last,
      input  logic [15:0] in_data,
      output logic        out_valid,
      output logic [15:0] out_csum
    );
      logic [15:0] acc_q, acc_n;
      logic [16:0] s;
      assign s     = {1'b0, acc_q} + {1'b0, in_data};
      assign acc_n = s[15:0] + {15'd0, s[16]};            // end-around carry
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin acc_q <= '0; out_valid <= 1'b0; out_csum <= '0; end
        else begin
          out_valid <= in_valid && in_last;
          if (in_valid && in_last) begin out_csum <= ~acc_n; acc_q <= '0; end
          else if (in_valid)       acc_q <= acc_n;
        end
    endmodule
    

    Prove: the sweep, then a design that really is broken

    Terminal
    $ iverilog -g2012 -o c1.vvp csum_tb.sv            # FIX = 1, the default
    $ for s in $(seq 1 100); do vvp -n c1.vvp +SEED=$s +VERIFY; done > sweep1.log 2>&1
    $ grep -c '^PASS' sweep1.log; grep -c '^MISMATCH' sweep1.log
    100
    0
    $ vvp -n c1.vvp +CORNER +VERIFY
    receiver check: dut checksum wrong in 0 of 3 packets
    PASS: seed 1, 3 packets, dut matches model
    ...

    Every seed passes. That proves less than it looks: a bench that compares nothing would pass too. A fixed checker must still fail on a real bug, so break the design on purpose. A copy drops the end-around carry from csum16 and runs the fixed bench:

    Terminal
    $ sed '15s|.*|  assign acc_n = s[15:0];                    // mutant: end-around carry dropped|' csum_tb.sv > mutant.sv
    $ iverilog -g2012 -o m.vvp mutant.sv && vvp -n m.vvp +SEED=1 +VERIFY
    MISMATCH seed=1 packet=1 words=2: csum dut=dec7 model=dec6
      packet: 2139 ffff
      receiver check: dut BAD, model ok
    receiver check: dut checksum wrong in 1936 of 2000 packets
    FATAL: mutant.sv:112: seed 1: 1936 of 2000 packets mismatch
    ...
    $ vvp -n m.vvp +CORNER +VERIFY
    MISMATCH seed=1 packet=1 words=3: csum dut=0000 model=fffe
      packet: ffff ffff 0001
      receiver check: dut BAD, model ok
    receiver check: dut checksum wrong in 2 of 3 packets
    FATAL: mutant.sv:112: seed 1: 2 of 3 packets mismatch
    ...

    The fixed model catches the mutant on the very first packet, and this time the receiver check votes against the design, as it should: the two checks now agree with each other, and both disagree with a design that is really wrong. On the corner packets the mutant fails two of three; the one it gets right is the single word ffff, which never carries.

    Prevent: test the checker, not only the design

    • Keep the independent check on. The receiver property costs a few lines and shares no code with the model. Run it on every packet, every night; a disagreement between the model and the property is a bench bug by definition.

    • Give the model its own known-answer tests. The three +CORNER packets are worked by hand; run them against the model alone before every regression, so a model change cannot quietly break them.

    • Mutate the design when you change a checker. One deliberate bug per checker change, kept as a script, proves the checker still bites.

    • The review question for every reference model: what happens at the edges of its own arithmetic: widths, carries, wrap, rounding? The design was reviewed for its carry; the model never was.

    Which step won? Localise, by asking which side is wrong before asking what is wrong. Three referees that share no code with the model, the spec's own property, a hand calculation and a sweep, turned a bug report against the RTL into a one-line bench fix in under an hour. Prove closed it: a fixed checker that cannot fail is not a fix.

    Common pitfall

    Fixing the design to match the model. It is the fastest way to turn red into green: make the RTL drop the second carry too, and every seed passes. The chip then sends checksums that receivers reject, on about one packet in several thousand, in the field. If the only evidence that the design is wrong is that it disagrees with the model, you do not yet know which one to fix.

    Common pitfall

    A referee that shares code with the model. Copying the model's fold into a "second check" gives two votes for the same mistake. The receiver check is useful because it is written a different way, from a different sentence of the spec.

    Why this matters

    Senior lens. Reference models are written faster and reviewed less than RTL, and they are trusted more. On a new block, budget for the model to be wrong about as often as the design in the first weeks, and keep a short list of the model's known-answer tests next to the design's directed tests.

    When a bug is filed against the RTL from a scoreboard mismatch, the first comment should be the evidence for which side is wrong, not the fix. That one habit removes most of the "fixed the design, broke the chip" stories.

    Interview

    Asked: "Your scoreboard reports a mismatch on one packet in a few thousand. How do you decide whether the design or the testbench is wrong?"

    Weak answer: I would look at the design's waveform for that packet and find where the value goes wrong, since the scoreboard is what we trust.

    Strong answer: I would not assume either side. First I reproduce the failing packet and collect the pattern: how far apart the values are, which direction, which packets. Then I find a referee that shares no code with the model or the RTL: a property from the spec the output must satisfy, a hand calculation on the smallest failing case, or a second implementation written differently. If the referees side with the design, the bug is in the bench, and I check that the explanation predicts every clue, not just the one I was looking at. After fixing the model, I run a sweep and then deliberately break the design to make sure the fixed checker still fails.

    Follow-ups: (1) What makes a good independent check for a block that has no neat output property? (2) Why is a passing sweep after a bench fix weak evidence on its own?

    Check yourself Before anyone found the bench bug, a designer proposed changing csum16 to sum in 32 bits and fold once at the end, "like the model". Would the regression have caught that change?

    No. The design would then compute exactly what the model computes, including the dropped carry, so every seed would pass. The receiver check would catch it: on the failing packets the new design's checksum would fail the property, just as the model's does. That is why the independent check matters: it is the only part of the bench that does not move when the model is wrong.

    Check yourself With the fixed model, the 100-seed sweep passes. Why is the mutant run still needed, and what would it mean if the mutant also passed?

    A passing sweep shows the model and the design agree; it does not show the bench can disagree. A model edited into something that always returns the design's value, or a compare that never runs, would also pass. If the mutant without the end-around carry passed, the checker would be blind to the very bug class the block exists to get right, and the fix would not be proven.

    Remember

    • A mismatch says two things disagree, not which one is wrong. Find a referee that shares no code with either side.

    • The spec often gives a check cheaper than a model: a property every output must satisfy. Run it as a second, independent checker.

    • After fixing a bench, break the design on purpose and make sure the bench still fails.

    Rule of thumb: before you debug the design, prove the checker is right on the failing case.

    How this was checked

    csum_tb.sv as shown, 2026-10-18. iverilog -g2012 -P csum_tb.FIX=0 -o c0.vvp csum_tb.sv and vvp -n c0.vvp +SEED=<n> [+VERIFY] [+CORNER] (Icarus Verilog 13.0): seeds 13, 16, 18 and 19 of 11 to 20 mismatch; seeds 1 to 100 with +VERIFY: 26 mismatch, and in all 26 the receiver check passes the design's checksum and fails the model's; 0 of 2000 design checksums fail it on every seed. verilator --binary --timing --assert -Wno-fatal --top-module csum_tb -GFIX=0 (Verilator 5.048) prints the same seed-18 lines.

    With the fix (FIX = 1, the default, which the course checker runs): seeds 1 to 100 with +VERIFY all pass, +CORNER +VERIFY passes. Mutant: a copy made with sed '15s|.*| assign acc_n = s[15:0]; ...|' (end-around carry dropped) fails seed 1 with 1936 of 2000 packets and +CORNER with 2 of 3. The sums by hand are checked with Python 3.14.3.

    Limits: one small design and its own bench; the receiver check is a necessary condition from the spec, not a full model (two different checksums cannot both pass it, but a wrong packet with a matching wrong checksum would).

    Keep going

    • Practice: Scoreboard mismatch with no errors

    • Practice: Scoreboard mismatch, but the monitor shows correct data

    • Practice: 16-bit ones'-complement header checksum

    • Code lab: Reference model class

    • Code lab: Scoreboard compare

    • Interview: run a mock interview on this topic

    Related topics

    • Part 10 · Case Studies

    • When the bug is in the test: check the design, then the checker

    • Testbench bugs that look like design bugs

    • Confirmation bias and blaming the tool

    • Differential debugging: references and their limits

    • UVM: the mismatch playbook

    Previous

    97 / 107

    Next: Part 11 · Debug as a Team

    Up next

    Up nextPart 11 · Debug as a Team

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Off-by-One That Fails at Wrap
    • The Reset Value That Changed
    • The X That Hid for Thousands of Cycles