Skip to content

Verilog · Chapter 9.5 · Design & Testbench Creation

Testbench Creation Techniques in Verilog — Clock, Stimulus, Checking, Termination

You have learned to build a clean design module, the device under test. This lesson teaches you to prove it works by building a testbench, the simulation-only harness that wraps that design, brings it to life, and judges its behaviour. A testbench has a fixed set of jobs. It generates a clock, applies reset, drives stimulus into the inputs, observes the outputs, checks them against what was expected, and then terminates cleanly. You will learn the technique for each job, plus the one rule beginners get wrong on the harness side, which signals should be a reg and which should be a wire. This is where earlier lessons converge, from modules and instances to port mapping and the system tasks that control a run, into the harness that turns I wrote it into I proved it.

Foundation22 min readVerilogTestbenchVerificationDUTSelf-CheckingSimulation

Chapter 9 · Section 9.5 · Design & Testbench Creation

1. The Engineering Problem

You followed 9.4 and built a clean alu design module — synthesizable, single-purpose, well-interfaced. Is it correct? You have no idea. A design module sitting in a file does nothing: it has no clock ticking, no values on its inputs, no one watching its outputs. It is a chip with no power, no signal generator, and no oscilloscope attached.

A junior's instinct is to "look at the waveform and see if it seems right." That does not scale and it does not hold: eyeballing a few cycles proves nothing about the hundreds of cases that matter, and "seems right" has shipped countless bugs to silicon. The real question is:

How do you bring a design module to life, exercise it across the cases that matter, and have the design itself report whether it passed or failed — automatically?

The answer is a testbench: a simulation-only module that surrounds the DUT, generates its clock, drives its inputs, watches its outputs, and checks them against expected values, ending with a clear PASS or FAIL. It is the test rig that turns a dormant design into a running, judged experiment. This page builds that rig piece by piece — clock, reset, stimulus, observation, checking, termination — using the toolkit Chapter 8 and 9.1–9.4 already gave you.

2. Mental Model — The Testbench Is a Virtual Test Bench

Visual A — the six jobs of a testbench

What every testbench does

data flow
What every testbench doesClockgenerate the heartbeatResetknown starting stateStimulusdrive DUT inputsObserve$display / $monitorCheckactual vs expected →errorsTerminate$finish + watchdog
The six jobs map onto the simulation lifecycle from 8.5: clock + reset are Initialize, stimulus is the driving phase, observe + check are verification, and terminate is the deterministic end. Every testbench is an arrangement of these six.

3. The Anatomy of a Testbench

Here is the standard testbench skeleton wrapped around a DUT, with each region labelled. (The alu is the DUT from 9.4; its internals are not shown — the testbench only touches its interface.)

alu_tb.v — testbench skeleton
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
 
module alu_tb;                       // 1. module with NO ports
 
    // 2. driver signals: reg for DUT inputs, wire for DUT outputs
    reg  [15:0] a, b;
    reg  [2:0]  op;
    wire [15:0] y;
    wire        zero;
 
    // 3. instantiate the DUT (named port mapping — 9.3)
    alu u_dut (
        .a(a), .b(b), .op(op), .y(y), .zero(zero)
    );
 
    // 4. clock generation (combinational DUTs may omit this)
    //    ... see §4
 
    // 5. stimulus + reset + checking, in an initial block
    initial begin
        // apply reset, drive inputs, check outputs, finish
        // ... see §5–§8
    end
 
    // 6. watchdog timeout — bound the run (§8, Chapter 8.5)
 
endmodule

Six regions, every testbench: a port-less module, driver signals (the reg/wire split of §9), the DUT instance, clock generation, the stimulus-and-check body, and a watchdog. The shape is as invariant as the design-module skeleton in 9.4 — learn it once and every testbench is a variation on it.

4. Clock Generation

A synchronous DUT does nothing without a clock. The testbench generates one — a free-running square wave — with a single line:

clock-gen.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg clk = 0;
always #5 clk = ~clk;        // toggle every 5 time units → 10-unit period → 100 MHz at 1ns

The always #5 clk = ~clk flips the clock every 5 time units, producing a 10-unit period. This is sim-only (the #5 delay has no hardware meaning) and lives only in the testbench. Two cautions:

  • It runs forever. A free-running clock keeps the simulation's event queue alive indefinitely — which is exactly why a testbench must call $finish and carry a watchdog (Chapter 8.5). The clock will not stop on its own.
  • Combinational DUTs need no clock. A purely combinational block (an adder, a mux) has no clock port; its testbench drives inputs and waits for the outputs to settle, no clock generator required.

5. Reset Application

A sequential DUT starts in an unknown state until reset puts it into a known one. The testbench asserts reset at the very start, holds it briefly, then releases it before stimulus begins:

reset-apply.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg rst_n;
initial begin
    rst_n = 0;          // assert reset (active-low) at t=0
    #20;                // hold across a couple of clock edges
    rst_n = 1;          // release — DUT is now in a known state
    // ... stimulus follows ...
end

Reset is applied first, before any meaningful stimulus, so the DUT begins every test from a defined state. Forgetting it is a classic bug: stimulus applied to an unreset sequential DUT operates on x state and produces meaningless results (§11). The DUT's reset behaviour is its own design (Chapter 14); the testbench's job is simply to drive the reset signal at the right time.

6. Stimulus Application

Stimulus is the sequence of input values the testbench drives into the DUT to exercise it. In a directed testbench, the engineer writes out specific cases by hand:

directed-stimulus.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    rst_n = 0; #20; rst_n = 1;     // reset first
 
    // directed cases — drive inputs, wait, let outputs settle
    a = 16'd10; b = 16'd5; op = 3'b000; #10;   // add
    a = 16'd10; b = 16'd5; op = 3'b001; #10;   // subtract
    a = 16'd12; b = 16'd4; op = 3'b010; #10;   // and
    // ... more cases ...
 
    $finish;
end

Each case sets the inputs, waits (#10) for the DUT to respond, and moves on. Directed stimulus targets specific, known-interesting cases — boundary values, each operation, corner cases. (When you need thousands of varied cases, constrained-random stimulus uses $random/$urandom from Chapter 8.2 to generate them — that is a verification technique previewed here and drilled later; directed testing is where you start.)

7. Observation and Self-Checking

Driving stimulus is half the job; the testbench must also judge the result. There are two levels, and only one is acceptable for real work.

Observation prints what happened — useful, but it makes a human the checker:

observation-only.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$display("[t=%0t] a=%0d b=%0d op=%b -> y=%0d", $time, a, b, op, y);
// A person must read the log and decide if y is right. Doesn't scale.

Self-checking makes the testbench the checker — it computes the expected value and compares automatically, counting failures:

self-checking.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
integer errors = 0;
 
task check(input [15:0] exp);
    if (y !== exp) begin
        $display("[t=%0t] FAIL: a=%0d b=%0d op=%b -> y=%0d expected=%0d",
                 $time, a, b, op, y, exp);
        errors = errors + 1;
    end
endtask
 
// usage in stimulus:
a = 16'd10; b = 16'd5; op = 3'b000; #10; check(16'd15);   // add → expect 15

A self-checking testbench encodes the specification as executable comparisons — it fails loudly and automatically on any mismatch and needs no human to read the log. This is the standard for all real verification: a testbench that only prints (and relies on someone noticing) will pass a broken DUT the day no one is watching (§11, DebugLab 3). (The $display mechanics and the ===/!== X-aware comparison are Chapter 8.1 / Chapter 5; here the point is the testbench checks itself.)

Visual B — observation-only vs self-checking

Observation-only versus self-checking testbenchObservation-only$display → human reads logpasses bugs silentlyno one is watchingSelf-checkingcompute expected → comparedeterministicPASS/FAILfails loudly, automatically12
Two levels of testbench. Observation-only prints the DUT's outputs and relies on a human to spot wrong values — it does not scale and silently passes bugs no one happens to read. Self-checking computes the expected value, compares automatically, counts errors, and reports a deterministic PASS/FAIL. Real verification is always self-checking.

8. Termination and the Watchdog

A testbench must end deterministically with a verdict — and protect itself against a hung DUT. Both come straight from Chapter 8.5.

termination.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// at the end of the stimulus sequence:
initial begin
    // ... reset, stimulus, checks ...
    if (errors == 0) $display("TEST PASSED");
    else             $display("TEST FAILED (%0d errors)", errors);
    $finish;                          // deterministic end with a verdict
end
 
// independent watchdog — bound the run so a hung DUT can't run forever
initial begin
    #100000;
    $display("TEST FAILED — TIMEOUT");
    $finish;
end

The stimulus block prints a parseable verdict and $finishes; an independent watchdog block bounds the worst case so a deadlocked DUT fails loudly instead of running to the farm's wall-clock limit. Every testbench carries both. (The full discipline — $finish vs $stop, the watchdog race, exit semantics — is Chapter 8.5; here it is the testbench's closing region.)

9. The reg/wire Rule — Which Side Drives

The one rule beginners get wrong on the testbench side, and the complement to 9.3's port-direction rule:

  • Signals connected to DUT inputs must be reg. The testbench drives them from procedural (initial/always) blocks, and only a reg (a procedural variable) can be assigned there.
  • Signals connected to DUT outputs must be wire. The DUT drives them through its instance, and an instance output must connect to a net (the 9.3 rule, seen from the testbench).
the reg/wire split in the testbench
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   testbench DRIVES → DUT input    ⇒  declare as  reg
   DUT DRIVES → testbench observes  ⇒  declare as  wire

The mnemonic: the testbench writes its regs and reads the DUT's wires. Get it backwards — an output declared reg, or an input declared wire — and the tool rejects it (DebugLab 1). The logic is the same direction rule as 9.3, now applied from inside the harness: whoever drives a signal determines its type, and for DUT inputs that driver is the testbench.

Visual C — testbench drives reg inputs, reads wire outputs

Testbench reg inputs and wire outputsTestbenchdrives inputs, readsoutputsreg a, b, opTB drives → DUT inputsDUTcomputeswire y, zeroDUT drives → TB reads12
The signal types follow the drive direction. The testbench drives the DUT's inputs from procedural blocks, so those signals are reg. The DUT drives its outputs through the instance, so those signals are wire — the testbench only reads them. This is 9.3's port-direction rule seen from inside the harness.

10. Worked Examples

Three testbench fragments — clock, a reset-plus-stimulus sequence, and a self-checking skeleton.

10.1 Example 1 — clock generation

tb-clock.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module dut_tb;
    reg clk = 0;
    always #5 clk = ~clk;        // 10-unit period, free-running
    // ... DUT instance and stimulus use clk ...
endmodule

One line makes the heartbeat. Remember it runs forever — the testbench must $finish and carry a watchdog, or the simulation never ends.

10.2 Example 2 — reset then directed stimulus

tb-reset-stimulus.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    // 1. reset into a known state
    rst_n = 0;  en = 0;  d = 0;
    #20;  rst_n = 1;
 
    // 2. directed stimulus, synchronized to the clock
    @(posedge clk); en = 1; d = 8'hA5;
    @(posedge clk); d = 8'h3C;
    @(posedge clk); en = 0;
 
    // 3. end
    #20; $finish;
end

Reset first, then a short directed sequence driving the DUT's inputs across clock edges, then a clean $finish. This is the everyday shape of a directed stimulus block.

10.3 Example 3 — a self-checking skeleton

tb-self-checking.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
integer errors = 0;
 
task expect_eq(input [15:0] got, input [15:0] exp, input [127:0] name);
    if (got !== exp) begin
        $display("FAIL %0s: got=%0d expected=%0d at t=%0t", name, got, exp, $time);
        errors = errors + 1;
    end
endtask
 
initial begin
    rst_n = 0; #20; rst_n = 1;
 
    a = 16'd10; b = 16'd5; op = 3'b000; #10; expect_eq(y, 16'd15, "add");
    a = 16'd10; b = 16'd5; op = 3'b001; #10; expect_eq(y, 16'd5,  "sub");
 
    if (errors == 0) $display("TEST PASSED");
    else             $display("TEST FAILED (%0d errors)", errors);
    $finish;
end

The testbench encodes the spec as expect_eq checks, counts failures, and reports a deterministic verdict — no human in the loop. This is the standard structure all six worked together; 9.6 assembles a complete DUT-plus-testbench pair end to end.

11. Industry Perspective

  • Self-checking is mandatory. No serious team accepts an observation-only ("eyeball the waveform") testbench. A testbench that does not encode the expected result and fail automatically provides no regression protection — it passes a broken DUT the moment no one reads the log.
  • Testbenches run unattended, in bulk. Thousands of testbenches run nightly on compute farms with no human watching, which is exactly why deterministic $finish, watchdog timeouts, and a parseable PASS/FAIL verdict (Chapter 8.5) are non-negotiable — the harness must report its own result.
  • Directed gives way to constrained-random + coverage. Directed testbenches (this page) are the foundation; at scale, verification moves to constrained-random stimulus ($urandom) plus functional coverage to reach cases no human would enumerate. The directed skeleton here is the base those techniques build on.
  • The testbench is half the engineering. In modern projects, verification code often exceeds design code in volume and effort. The testbench is not an afterthought to the DUT — it is a co-equal deliverable, built in the same loop (the Chapter 9 overview's thesis).

12. Common Mistakes

  1. Wrong signal types — a DUT input declared wire (the testbench can't drive it procedurally) or a DUT output declared reg (an instance output must drive a net). The reg/wire rule of §9 (DebugLab 1).
  2. No clock — a synchronous DUT with no clock generator never advances; the simulation runs but nothing happens (DebugLab 2).
  3. No $finish — a free-running clock keeps the run alive forever (Chapter 8.5 / §8).
  4. No self-checking — only $display, relying on a human to notice wrong values; passes a broken DUT silently (DebugLab 3).
  5. Forgetting reset — driving stimulus into an unreset sequential DUT, which operates on x state and yields meaningless results (§5).

13. Debugging Lab

Three testbench debug post-mortems

Pitfall 1 — DUT output declared as reg in the testbench
Buggy Code
module alu_tb;
  reg  [15:0] a, b;
  reg  [2:0]  op;
  reg  [15:0] y;      // <-- WRONG: y is a DUT OUTPUT
  reg         zero;   // <-- WRONG: zero is a DUT OUTPUT

  alu u_dut ( .a(a), .b(b), .op(op), .y(y), .zero(zero) );

  initial begin
      a = 16'd10; b = 16'd5; op = 3'b000; #10;
      $display("y=%0d", y);
      $finish;
  end
endmodule

// Error: output port 'y'/'zero' connected to a reg — an instance output
// must drive a net.
Symptom

The testbench fails to compile with an error like "output port connected to a variable" or "reg cannot be driven by an instance output", pointing at the DUT instantiation. The inputs a/b/op are fine; the failure is on the output connections, which look identical in style.

Root Cause

Signal type in a testbench follows the DRIVE direction. y and zero are DUT OUTPUTS — driven by the alu instance — so on the testbench side they must be NETS (wire): an instance output must connect to a net (the 9.3 rule). Declaring them as 'reg' tells the tool the testbench intends to drive them procedurally, which conflicts with the instance driving them.

The inputs a/b/op are correctly 'reg' because the TESTBENCH drives those from the initial block. The rule is symmetric: testbench writes its regs, reads the DUT's wires.

The fix is to declare DUT-output signals as wire.

Fix
module alu_tb;
  reg  [15:0] a, b;       // TB drives DUT inputs  → reg
  reg  [2:0]  op;
  wire [15:0] y;          // DUT drives outputs    → wire
  wire        zero;

  alu u_dut ( .a(a), .b(b), .op(op), .y(y), .zero(zero) );

  initial begin
      a = 16'd10; b = 16'd5; op = 3'b000; #10;
      $display("y=%0d", y);   // TB READS the wire
      $finish;
  end
endmodule
Pitfall 2 — no clock, so the DUT never advances
Buggy Code
module counter_tb;
  reg        clk;        // declared, but never toggled
  reg        rst_n;
  wire [7:0] count;

  counter u_dut ( .clk(clk), .rst_n(rst_n), .count(count) );

  initial begin
      clk = 0;           // set once, never toggled again
      rst_n = 0; #20; rst_n = 1;
      #100;
      $display("count = %0d", count);   // prints 0 — DUT never counted
      $finish;
  end
endmodule

// 'count' stays 0 forever. The counter is correct; it just never sees a
// clock edge, so it never increments.
Symptom

A counter testbench reports count = 0 no matter how long it runs. The counter design module is correct and passes other tests, but here it never counts. Reset works; time advances; the count simply never changes.

Root Cause

A synchronous DUT advances only on clock edges, and this testbench never generates any. 'clk' is set to 0 once in the initial block and never toggled, so there is no posedge for the counter's always block to fire on. Time passes (the #20, #100 delays), but with a flat clock the DUT is frozen — like a chip with power but no oscillator.

The counter is fine; the testbench forgot the single most important job — generating the clock.

The fix is to add a free-running clock generator. (And, since it runs forever, keep the $finish that ends the run.)

Fix
module counter_tb;
  reg        clk = 0;
  reg        rst_n;
  wire [7:0] count;

  always #5 clk = ~clk;            // <-- the missing clock generator

  counter u_dut ( .clk(clk), .rst_n(rst_n), .count(count) );

  initial begin
      rst_n = 0; #20; rst_n = 1;
      #100;
      $display("count = %0d", count);   // now non-zero — DUT counted
      $finish;
  end
endmodule
Pitfall 3 — observation-only testbench passes a broken DUT
Buggy Code
module alu_tb;
  reg  [15:0] a, b;  reg [2:0] op;
  wire [15:0] y;     wire zero;

  alu u_dut ( .a(a), .b(b), .op(op), .y(y), .zero(zero) );

  initial begin
      a = 16'd10; b = 16'd5; op = 3'b000; #10;
      $display("add: y=%0d", y);     // prints "add: y=14"  (should be 15!)
      a = 16'd10; b = 16'd5; op = 3'b001; #10;
      $display("sub: y=%0d", y);     // prints "sub: y=5"
      $finish;                       // no error counter, no PASS/FAIL
  end
endmodule

// The ALU has an off-by-one in 'add' (outputs 14, not 15). The log shows
// it — but nothing CHECKS it, so the test 'passes' and the bug ships.
Symptom

A nightly regression for the ALU is green for weeks. A customer reports that addition is off by one. Re-running the testbench locally still shows no failure — yet the log clearly contains "add: y=14" where 15 was expected. The bug was visible the whole time; the testbench just never flagged it.

Root Cause

The testbench is OBSERVATION-ONLY: it prints the DUT's outputs but never COMPARES them to expected values, and has no error counter or PASS/FAIL verdict. The off-by-one is right there in the log ("y=14"), but a testbench that only $displays relies on a HUMAN to read every line and spot wrong values — which no one does on a green nightly run. So a broken DUT 'passes'.

The root cause is the absence of self-checking. A testbench must encode the expected result and fail automatically; printing is not checking.

The fix is to compute the expected value, compare with !==, count errors, and report a deterministic verdict — so the off-by-one fails the test loudly instead of hiding in the log.

Fix
module alu_tb;
  reg  [15:0] a, b;  reg [2:0] op;
  wire [15:0] y;     wire zero;
  integer errors = 0;

  alu u_dut ( .a(a), .b(b), .op(op), .y(y), .zero(zero) );

  task check(input [15:0] exp);
      if (y !== exp) begin
          $display("FAIL: a=%0d b=%0d op=%b y=%0d exp=%0d", a, b, op, y, exp);
          errors = errors + 1;
      end
  endtask

  initial begin
      a = 16'd10; b = 16'd5; op = 3'b000; #10; check(16'd15);  // now FAILS: y=14
      a = 16'd10; b = 16'd5; op = 3'b001; #10; check(16'd5);
      if (errors == 0) $display("TEST PASSED");
      else             $display("TEST FAILED (%0d errors)", errors);
      $finish;
  end
endmodule

// The off-by-one now produces a loud FAIL and a non-zero error count —
// the regression catches it instead of shipping it.

14. Interview Q&A

15. Exercises

Exercise 1 — Classify the testbench signals

A DUT has ports input clk, input rst_n, input [7:0] d, input en, output [7:0] q, output valid. In the testbench, state which signals must be reg and which must be wire, and explain the rule in one sentence.

Exercise 2 — Write the missing pieces

Given a testbench skeleton with a DUT instance already in place, write: (a) a free-running clock generator with a 10-unit period, (b) a reset sequence that asserts active-low reset at t=0, holds it 20 units, and releases it, and (c) a $finish after the stimulus. Code only these three fragments.

Exercise 3 — Make it self-checking

This fragment is observation-only:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = 8'd7; b = 8'd9; #10;
$display("sum = %0d", sum);

Convert it to self-checking: compute the expected value, compare against sum, increment an errors counter on mismatch, and (separately) print a final PASS/FAIL. Describe what the final verdict block looks like.

Exercise 4 — Find the testbench bugs

List every testbench bug below (reference §4–§9 / §12) and give a one-line fix for each.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module dut_tb;
    reg        clk;
    reg  [7:0] d;
    reg  [7:0] q;          // q is a DUT output
    dut u_dut ( .clk(clk), .d(d), .q(q) );
    initial begin
        clk = 0;           // never toggled
        d = 8'h10; #10;
        $display("q=%0d", q);
        // no $finish
    end
endmodule

16. Summary

A testbench is the simulation-only harness that proves a design module (the DUT) works — it brings the dormant DUT to life, exercises it, and judges its behaviour automatically.

The core ideas:

  • Six jobs: generate the clock, apply reset, drive stimulus, observe outputs, check them against expected values, and terminate with a verdict (plus a watchdog). Every testbench arranges these six.
  • A testbench is a port-less, simulation-only module that reaches down into the DUT and freely uses initial, # delays, $display, $finish — everything a design module must not.
  • The reg/wire rule: DUT-input signals are reg (the testbench drives them); DUT-output signals are wire (the DUT drives them). The testbench writes its regs and reads the DUT's wires.
  • Self-checking, not observation-only: the testbench computes the expected result and compares automatically, failing loudly — printing alone passes broken DUTs silently.
  • Clock generation + $finish + watchdog (Chapter 8.5) make the run advance, end deterministically, and survive a hung DUT.

The discipline this page instils:

  • Always self-check — encode the spec as comparisons; a testbench that only prints is no testbench.
  • Reset first, clock always, finish always — known state, a heartbeat, and a deterministic end.
  • Type signals by who drives themreg for DUT inputs, wire for DUT outputs.

You can now build the harness that turns a design module into a verified one — the full convergence of Chapters 8 and 9.1–9.4. The final step is to see it all assembled: Chapter 9.6 Practical Examples walks a complete, working DUT-plus-testbench pair end to end — design module and self-checking testbench together — closing Chapter 9 and the foundation arc before the curriculum turns to the RTL core (Chapter 10 Operators onward).