Tutorial 09 · Verilog Series

Verilog Testbench Writing

A testbench is a Verilog module that drives and checks your design under test (DUT). No ports, no synthesizable constraints — pure simulation. Learning to write clean, self-checking testbenches is as important as writing the RTL itself.

No Ports Clock Generation Reset Sequence Stimulus Self-Checking VCD Waveform $display/$monitor
module tb_dut; <-- no ports! Clock Gen always #5 clk=~clk DUT (your RTL module) dut u_dut(.clk,.rst_n,..); Stimulus initial blocks Checker $display, $error pass/fail count VCD dump $finish Clock and stimulus drive DUT; checker monitors outputs
A testbench wraps the DUT: clock generator + stimulus (initial blocks) drive inputs; checker monitors outputs and reports pass/fail.

1. Testbench Structure (5 Parts)

01

Timescale — set time unit and precision

02

Signal declarations — reg for DUT inputs, wire for DUT outputs

03

DUT instantiation — connect signals to DUT ports

04

Clock generation — always block that toggles clock

05

Stimulus & checking — initial blocks that drive inputs and verify outputs

2. timescale Directive

verilog
`timescale 1ns/1ps   // unit = 1ns, precision = 1ps
// All # delays in ns. e.g. #5 = 5ns

`timescale 1ps/1fs   // ultra-fine — for analog or RF models
`timescale 1us/1ns   // coarse — for slow interfaces (I2C, UART)
Rule: Use `timescale 1ns/1ps for most RTL testbenches. Place it before the module declaration. The timescale of the top-level simulation module applies globally unless overridden per-file.

3. Clock Generation

verilog
`timescale 1ns/1ps

module tb_counter;  // no ports!
    reg clk, rst_n;
    reg [3:0] d;
    wire [3:0] q;
    wire carry;

    // Clock: 100MHz → period = 10ns → half = 5ns
    initial clk = 0;
    always #5 clk = ~clk;   // 10ns period (100MHz)

// ... rest of testbench below
Initialize first: Always initialize clk = 0 in an initial block before the always block starts. If clk starts at X, the always block won't toggle correctly.

4. Reset Sequence

verilog // Reset sequence inside initial block initial begin // Assert reset (active-low) rst_n = 0; d = 0; // Hold reset for 3 clock cycles (30ns) @(posedge clk); // wait for rising edge @(posedge clk); @(posedge clk); // Deassert reset after clock edge #2; // 2ns after edge to avoid setup time issues rst_n = 1; // ... stimulus follows ... end
Deassert after edge: Change inputs a few ns after the rising clock edge, not at the edge. This avoids race conditions between the clock and the data and matches real setup time requirements. @(posedge clk); #2; rst_n = 1;

5. Applying Stimulus

verilog // Method 1: absolute time delay d = 4'hA; #20; // hold for 20ns d = 4'hF; #20; // Method 2: clock-aligned (preferred for synchronous DUT) @(posedge clk); #2; d = 4'h5; @(posedge clk); #2; d = 4'h3; // Method 3: repeat for N cycles repeat(10) @(posedge clk); // wait 10 clock cycles // Method 4: for loop over all input combinations integer i; for (i=0; i<16; i=i+1) begin d = i[3:0]; @(posedge clk); #2; end $finish; // end simulation

6. System Tasks for Checking

TaskWhen it firesUse Case
$displayImmediately when calledPrint once: checkpoint messages, pass/fail
$writeImmediately (no newline)Like $display but no trailing newline
$monitorWhenever any argument changesContinuously print signal values
$strobeEnd of current timestepPrint after all NBA updates (clean values)
$timeCurrent simulation timeInclude in prints: $display("t=%0t", $time)
$errorImmediately when calledFlag a test failure (continues simulation)
$fatalImmediately when calledFlag a fatal error and stop simulation
$finishImmediately when calledEnd simulation gracefully
$stopImmediately when calledPause simulation (interactive debug)
verilog — $display format strings $display("%d", val); // decimal $display("%h", val); // hexadecimal $display("%b", val); // binary $display("%o", val); // octal $display("%0d", val); // decimal without leading zeros $display("%t", $time); // simulation time $display("t=%0t a=%h b=%h sum=%h", $time, a, b, sum);

7. Self-Checking Testbench

A self-checking testbench compares actual DUT output against expected values automatically — no manual waveform inspection needed.

verilog — self-checking pattern integer pass_cnt, fail_cnt; task automatic check_result; input [7:0] expected; input [7:0] actual; input [31:0] test_num; begin if (actual === expected) begin $display("PASS [%0d]: got %h", test_num, actual); pass_cnt = pass_cnt + 1; end else begin $error("FAIL [%0d]: expected %h, got %h", test_num, expected, actual); fail_cnt = fail_cnt + 1; end end endtask initial begin pass_cnt = 0; fail_cnt = 0; // ... drive inputs, then: @(posedge clk); #1; // settle after clock check_result(8'h55, dut_out, 1); $display("--- RESULTS: %0d PASS, %0d FAIL ---", pass_cnt, fail_cnt); if (fail_cnt > 0) $fatal(1, "Tests FAILED"); else $display("All tests PASSED"); $finish; end
Use === for checking: Always use === (case equality) when comparing DUT output to expected values in a testbench. If === detects X, it returns 0 (mismatch), flagging the bug. Using == returns X when the output contains X, which evaluates as false in an if-statement — silently passing a buggy test.

8. VCD Waveform Dump

verilog initial begin $dumpfile("wave.vcd"); // output file name $dumpvars(0, tb_counter); // 0=all depths, tb_counter=scope // ... reset + stimulus ... $finish; end // Open wave.vcd in GTKWave: // $ gtkwave wave.vcd

GTKWave is a free waveform viewer for Linux/Mac/Windows. EDA Playground (free online) lets you view VCD inline.

9. Complete Testbench Example (4-bit Counter)

verilog — tb_counter4.v `timescale 1ns/1ps module tb_counter4; reg clk, rst_n, load, en; reg [3:0] d; wire [3:0] q; wire carry; integer pass_cnt=0, fail_cnt=0; // DUT instantiation counter4 dut (.clk(clk), .rst_n(rst_n), .load(load), .en(en), .d(d), .q(q), .carry(carry)); // Clock generation: 100MHz initial clk=0; always #5 clk=~clk; // Check task task chk; input [3:0] exp_q; input exp_carry; begin @(posedge clk); #1; if (q===exp_q && carry===exp_carry) begin $display("PASS q=%h carry=%b", q, carry); pass_cnt++; end else begin $error("FAIL q=%h (exp %h) carry=%b (exp %b)", q, exp_q, carry, exp_carry); fail_cnt++; end end endtask initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb_counter4); // Reset {rst_n, load, en, d} = 0; repeat(3) @(posedge clk); #2; rst_n=1; // Test 1: count up en=1; chk(4'd1, 0); chk(4'd2, 0); // Test 2: load a value en=0; load=1; d=4'hE; chk(4'hE, 0); // Test 3: count to overflow load=0; en=1; chk(4'hF, 1); // carry should assert at 0xF with en chk(4'h0, 0); // rolls over to 0 $display("--- %0d PASS, %0d FAIL ---", pass_cnt, fail_cnt); $finish; end endmodule

10. Running with Icarus Verilog (Free)

bash — compile and simulate # Compile DUT and testbench together iverilog -o sim counter4.v tb_counter4.v # Run simulation (outputs to console + wave.vcd) vvp sim # View waveform gtkwave wave.vcd &
EDA Playground: If you don't want to install tools, paste your RTL and testbench at edaplayground.com — select Icarus Verilog, enable "Open EPWave" for waveforms, and run in seconds for free.