Day 6 · Verification Methodology

Self-Checking Testbenches

Video 2 of 4 · ~12 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

AnatomySelf-CheckingTasksFile-Driven

🌍 Where This Lives

In Industry

Regression suites at Intel/NVIDIA/Apple run thousands of self-checking testbenches overnight. CI/CD pipelines for RTL exist. A failing testbench emits a precise error log that developers can triage in minutes. Waveform inspection is reserved for root-causing; it is never how tests report status.

In This Course

Every lab from here forward uses a self-checking testbench. Your make sim either prints “0 failed” or it doesn't. Labs are graded on the self-check output. Video 3 organizes this pattern with tasks; Video 4 scales it with external test vectors.

⚠️ Eyeballing Waveforms Does Not Scale

❌ Wrong Model

“I'll run the simulation, open GTKWave, check that the outputs look right. If I add more tests later I'll just re-check by eye.”

✓ Right Model

Manual inspection does not scale, does not regress, and does not communicate. A self-checking testbench codifies expected behavior, compares it against actual behavior, and emits a structured PASS/FAIL report. When a student hands you a repo and says “it works,” you run make sim and trust the exit code, not their screenshot.

The receipt: If your testbench doesn't print “=== N passed, 0 failed ===”, it isn't done. No exceptions.

👁️ I Do — The Self-Checking Pattern

integer tests_run = 0, tests_failed = 0;

task check_eq(input [31:0] actual, input [31:0] expected, input [256*8-1:0] label);
begin
    tests_run = tests_run + 1;
    if (actual !== expected) begin      // use !== to handle X/Z correctly
        tests_failed = tests_failed + 1;
        $display("FAIL: %0s  actual=%h  expected=%h", label, actual, expected);
    end else begin
        $display("PASS: %0s  = %h", label, actual);
    end
end
endtask

initial begin
    // ... drive stimulus ...
    check_eq(sum, 5'd8,  "5+3");
    check_eq(sum, 5'd16, "7+9");
    // ... more ...
    $display("=== %0d passed, %0d failed ===", tests_run - tests_failed, tests_failed);
    $finish;
end
My thinking: Three key choices. (1) Use !== (case-inequality) — not != — to correctly compare signals that may contain X or Z. (2) Track tests_run and tests_failed as integer counters. (3) Always print a summary line at the end so CI/CD can grep for it.

🤝 We Do — == vs ===

reg [3:0] signal = 4'b10x1;

if (signal == 4'b1011)  $display("A fires?");  // result: X — if-condition is ambiguous
if (signal === 4'b1011) $display("B fires?");  // result: 0 — cleanly false, B does not fire
if (signal === 4'b10x1) $display("C fires?");  // result: 1 — matches including X, C fires
Together: == returns X when either operand contains X. In an if, ambiguous results often get treated as false, but the behavior is fragile. === treats X as a literal value and always returns a clean 0 or 1. In testbenches, always use === and !==.

🧪 You Do — Read the Summary

$ make sim
PASS: reset → count=0
PASS: 1 cycle → count=1
FAIL: 5 cycles → count=5  actual=xxxx  expected=5
PASS: rollover → count=0
FAIL: enable test  actual=0  expected=1
=== 3 passed, 2 failed ===

Two failures. What are the likely root causes?

Diagnosis:
  • “count=xxxx” → reset not held long enough, or missing default value — flops came out undefined and stayed that way.
  • “enable test actual=0” → the DUT ignored the enable; either the clock-enable wiring is wrong or there's a priority bug in the counter.
▶ LIVE DEMO

Convert a “Print” Testbench to Self-Checking

~5 minutes

▸ COMMANDS

cd labs/week2_day06/ex2_self_check/
# before: prints values
# after: PASS/FAIL + summary
diff tb_adder_before.v tb_adder_after.v
make sim

▸ EXPECTED STDOUT

PASS: 5+3 = 8
PASS: 7+9 = 16
PASS: 15+1 = 16
PASS: 0+0 = 0
PASS: F+F = 1E
=== 5 passed, 0 failed ===

▸ KEY OBSERVATION

The Makefile's make sim can now grep for “0 failed” to determine pass/fail automatically — enabling scripting, CI, and honest grading.

🔧 What Did the Tool Build?

Same answer as before — testbenches don't synthesize. But now the difference matters:

# Your DUT (adder.v) synthesizes:
$ yosys -p "synth_ice40 -top adder" ...
  SB_LUT4: 4    SB_CARRY: 4

# Your testbench (tb_adder.v) does not:
$ yosys -p "synth_ice40 -top tb_adder" ...
  ERROR: unsynthesizable constructs

# make stat runs ONLY on the DUT.
# make sim runs ONLY the testbench.
# They never mix.
The two-tool workflow made concrete: make sim is where verification lives. make stat is where synthesis lives. Self-checking testbenches are the contract between them.

🤖 Check the Machine

Ask AI: “Refactor this testbench to be self-checking with PASS/FAIL output and a summary line. Use !== for comparisons.”

TASK

Ask AI to self-check an existing testbench.

BEFORE

Predict: a check task, !== compare, counter-based summary.

AFTER

Strong AI uses !==. Weak AI uses != and handles X poorly.

TAKEAWAY

Test your AI by giving it code with an X and checking it handles the compare correctly.

Key Takeaways

 Manual waveform inspection is for debugging, not verification.

 Every testbench must print PASS/FAIL + a summary line.

 Use !== (case-inequality), never !=, for output checks.

 Grep "0 failed" from your Makefile for honest CI.

If the test doesn't report its own verdict, it isn't a test. It's a demo.

🔗 Transfer

Tasks for Organization

Video 3 of 4 · ~8 minutes

▸ WHY THIS MATTERS NEXT

You already met task in this video. Video 3 goes further: using tasks to clean up stimulus-and-check patterns so that each test case becomes a single readable line. This is how professional verification engineers structure their testbenches.