Verilog Testbench: Structure, Examples, Self-Checking
A Verilog testbench is a module that wraps around your design, drives its inputs, and watches its outputs. It is never synthesized into hardware, so it can use constructs the synthesis tool would reject: delays (#10), initial blocks, system tasks like $display, loops that run “forever”, and random numbers. In an interview or on the job, “can you write a testbench for this?” is often the very next question after “can you write the RTL?” — so treat this skill as equal in importance to design itself.
Structure of a Verilog testbench
Every testbench, from a 10-line mux check to a full verification environment, has the same skeleton. The testbench module has no ports — nothing drives it from outside; it is the top of the simulation.
module design_tb; // no ports: this is the simulation top // 1. Signal declarations // reg for everything the testbench drives (DUT inputs) // wire for everything the DUT drives (DUT outputs) // 2. DUT instantiation // 3. Clock generation (for sequential designs) // 4. Reset sequence + stimulus in initial blocks // 5. Observation: $monitor / $display, waveform dump, checkers // 6. $finish to end the simulation endmodule
Two rules explain the reg/wire split, and they trip up almost every beginner:
- Signals assigned inside an
initialoralwaysblock (procedural assignment) must be declaredreg. DUT inputs get their values this way, so they areregin the testbench. - Signals driven by a module output port or a
assignstatement must bewire. DUT outputs are thereforewirein the testbench.
DUT instantiation
The design under test (DUT) is instantiated inside the testbench exactly like any submodule. Always use named port connections (.port_name(signal)) rather than positional ones. Positional connections silently break the day someone reorders the port list; named connections either stay correct or fail loudly at compile time.
A first complete testbench
Here is a 2-to-1 multiplexer and its testbench in one file. The initial block applies a new input pattern every 10 time units, and $monitor prints a line automatically whenever any signal in its list changes value.
// Design under test: 2-to-1 multiplexer
module mux2 (
input wire a,
input wire b,
input wire sel,
output wire y
);
assign y = sel ? b : a;
endmodule
// Testbench
module mux2_tb;
reg a, b, sel; // testbench drives these, so they are reg
wire y; // DUT drives this, so it is wire
// Instantiate the DUT using named port connections
mux2 dut (
.a (a),
.b (b),
.sel (sel),
.y (y)
);
initial begin
// Print a line automatically whenever any listed signal changes
$monitor("time=%0t a=%b b=%b sel=%b -> y=%b", $time, a, b, sel, y);
a = 0; b = 0; sel = 0; // apply first input pattern
#10 a = 1; // after 10 time units, change a
#10 sel = 1; // now y should follow b
#10 b = 1;
#10 sel = 0; // back to a
#10 $finish; // end the simulation
end
endmoduletime=0 a=0 b=0 sel=0 -> y=0 time=10 a=1 b=0 sel=0 -> y=1 time=20 a=1 b=0 sel=1 -> y=0 time=30 a=1 b=1 sel=1 -> y=1 time=40 a=1 b=1 sel=0 -> y=1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpm49xkj0v/mux2_tb.v:33: $finish called at 50 (1s)
Note the difference between the two printing tasks you will use constantly:
| System task | When it prints | Typical use |
|---|---|---|
$display | Once, at the moment the statement executes | Step-by-step messages, pass/fail reports |
$monitor | Automatically on every change of any listed signal (only one monitor can be active) | Compact trace of the whole run |
$time | Not a print task — returns current simulation time | Timestamping messages; %0t formats it without padding |
Without $finish, a simulation containing a free-running clock never ends — the simulator just keeps toggling the clock forever. Always terminate explicitly.
Clock and reset generation
Sequential designs need a clock. The standard idiom is one initial block to set the starting value and one always block to toggle it:
initial clk = 1'b0; // define the starting value always #5 clk = ~clk; // toggle every 5 ns => 10 ns period = 100 MHz
If you skip the initial line, clk starts as x (unknown), and ~x is still x — the clock never toggles and your waveform is a flat red line. This single mistake accounts for a remarkable number of “my simulation does nothing” questions.
For reset, assert it at time 0, hold it for a couple of clock edges, then release it away from an active clock edge so you don’t create a race between reset release and the first capturing edge.
Dumping waveforms with $dumpfile and $dumpvars
$dumpfile("name.vcd") names a Value Change Dump file, and $dumpvars(0, tb_name) records every signal below the named scope (the first argument 0 means “all levels of hierarchy”). You can then open the .vcd file in GTKWave or any waveform viewer. The following example puts everything together: clock generation, an active-low asynchronous reset, an enable, and a waveform dump for a 4-bit counter.
// Design under test: 4-bit up counter with synchronous enable
module counter4 (
input wire clk,
input wire rst_n, // active-low asynchronous reset
input wire en,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else if (en)
count <= count + 4'd1;
end
endmodule
// Testbench
module counter4_tb;
reg clk, rst_n, en;
wire [3:0] count;
counter4 dut (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count)
);
// Clock generator: 10 ns period (toggle every 5 ns)
initial clk = 1'b0;
always #5 clk = ~clk;
// Waveform dump for GTKWave
initial begin
$dumpfile("counter4.vcd");
$dumpvars(0, counter4_tb);
end
// Reset and stimulus
initial begin
$monitor("time=%0t rst_n=%b en=%b count=%d", $time, rst_n, en, count);
rst_n = 1'b0; en = 1'b0; // hold reset at start
#12 rst_n = 1'b1; // release reset between clock edges
#8 en = 1'b1; // start counting
#60 en = 1'b0; // pause the counter
#20 en = 1'b1; // resume
#30 $finish;
end
endmoduleVCD info: dumpfile counter4.vcd opened for output. time=0 rst_n=0 en=0 count= 0 time=12 rst_n=1 en=0 count= 0 time=20 rst_n=1 en=1 count= 0 time=25 rst_n=1 en=1 count= 1 time=35 rst_n=1 en=1 count= 2 time=45 rst_n=1 en=1 count= 3 time=55 rst_n=1 en=1 count= 4 time=65 rst_n=1 en=1 count= 5 time=75 rst_n=1 en=1 count= 6 time=80 rst_n=1 en=0 count= 6 time=100 rst_n=1 en=1 count= 6 time=105 rst_n=1 en=1 count= 7 time=115 rst_n=1 en=1 count= 8 time=125 rst_n=1 en=1 count= 9 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpfv3yqw9m/counter_tb.v:47: $finish called at 130 (1s)
Read the log carefully: the count stays at 0 until reset is released and an enabled clock edge arrives, increments once per rising edge while en is high, freezes at 6 when en drops, and resumes from 6 — not from 0 — when en returns. Being able to predict a log like this before running it is exactly what interviewers probe.
Notice also that the three initial/always blocks all start at time 0 and run in parallel. A testbench is concurrent code, not a program that executes top to bottom.
Self-checking testbenches
Reading printed values by eye does not scale. A self-checking testbench computes the expected output itself (a “reference model”) and compares it with the DUT’s actual output, counting failures. This is the pattern real verification teams use, scaled up.
Three details matter in the example below:
- A
taskpackages the apply–wait–compare sequence so each test case is one line. - The comparison uses
!==(case inequality) instead of!=. With!=, anxon either side makes the whole comparison unknown and theifsilently skips the error branch — a bug in your DUT could hide behind anx.!==comparesxandzliterally. - Directed corner cases (zero, overflow, all-ones) come first; random stimulus with
$randomadds breadth afterwards.
// Design under test: 4-bit adder with carry out
module adder4 (
input wire [3:0] a,
input wire [3:0] b,
output wire [3:0] sum,
output wire cout
);
assign {cout, sum} = a + b;
endmodule
// Self-checking testbench
module adder4_tb;
reg [3:0] a, b;
wire [3:0] sum;
wire cout;
integer errors;
reg [4:0] expected; // 5 bits: carry + sum
adder4 dut (.a(a), .b(b), .sum(sum), .cout(cout));
// Reusable check: apply inputs, wait, compare against the model
task check;
input [3:0] ta, tb;
begin
a = ta; b = tb;
#5; // let combinational logic settle
expected = ta + tb; // reference model
if ({cout, sum} !== expected) begin
errors = errors + 1;
$display("FAIL: %d + %d -> got %d (cout=%b), expected %d",
ta, tb, sum, cout, expected);
end else begin
$display("PASS: %d + %d = %d cout=%b", ta, tb, sum, cout);
end
end
endtask
integer i;
initial begin
errors = 0;
// Directed corner cases
check(4'd0, 4'd0);
check(4'd15, 4'd1); // overflow into cout
check(4'd15, 4'd15); // maximum inputs
check(4'd7, 4'd8);
// A few random patterns
for (i = 0; i < 4; i = i + 1)
check($random, $random);
if (errors == 0)
$display("RESULT: all tests passed");
else
$display("RESULT: %0d test(s) FAILED", errors);
$finish;
end
endmodulePASS: 0 + 0 = 0 cout=0 PASS: 15 + 1 = 0 cout=1 PASS: 15 + 15 = 14 cout=1 PASS: 7 + 8 = 15 cout=0 PASS: 4 + 1 = 5 cout=0 PASS: 9 + 3 = 12 cout=0 PASS: 13 + 13 = 10 cout=1 PASS: 5 + 2 = 7 cout=0 RESULT: all tests passed /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmple6p1rrc/adder_sc_tb.v:57: $finish called at 40 (1s)
The final RESULT line means a script — or a tired engineer at 2 a.m. — can judge the run with a single grep. Every serious testbench you write should end with an unambiguous verdict like this.
Checklist and common mistakes
- Declared an input as
wire? You cannot assign it in aninitialblock; make DUT inputsreg. - Clock stuck at
x? You forgot to initialize it before toggling. - Simulation never ends? Missing
$finishwith a free-running clock. - No output at all? You wrote
$monitorbut used only one of several allowed monitors, or you never changed any monitored signal. - Checker never fails even on a broken DUT? You compared with
!=while signals carriedx; use!==in checkers. - Empty VCD file?
$dumpvarsmust name an existing scope (usually the testbench module) and run before signals start changing. - Reset released on a clock edge? Shift the release into the middle of a cycle to avoid a race.
Practice path: re-type the counter testbench from memory, then modify it — make the reset synchronous, add a load input, and extend the self-checking adder to 8 bits with 20 random vectors. Each change forces you through the structure again, and the structure is what interviews test.
Quick check
Why does a testbench module have no ports?
It is the top of the simulation hierarchy — nothing outside it drives or observes it. All stimulus is generated inside the testbench and all DUT outputs are observed inside it, so there is nothing to connect through ports.
Why are DUT inputs declared as reg and DUT outputs as wire in the testbench?
The testbench assigns DUT inputs inside initial/always blocks, and procedural assignment targets must be reg. DUT outputs are driven by the instantiated module’s output ports, and port-driven signals must be wire.
What goes wrong if you write “always #5 clk = ~clk;” without initializing clk?
clk starts as x (unknown), and ~x is still x, so the clock never produces real edges. You must set clk to 0 or 1 in an initial statement (or in the declaration) before the toggle takes effect.
Why should a self-checking testbench compare with !== instead of !=?
With !=, if either operand contains x or z the comparison result is unknown, the if condition is not true, and the error branch is silently skipped — hiding real bugs. The case inequality operator !== compares x and z values literally, so mismatches involving unknowns are still flagged.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses