Every verification engineer starts here: writing a testbench that instantiates your DUT, drives stimulus, and checks results. This tutorial covers every element of a production-quality Verilog testbench with complete, runnable code.
A Verilog testbench is a module with no ports. It instantiates the DUT and drives/checks its signals. The key rule: inputs to the DUT are declared reg in the testbench; outputs from the DUT are declared wire.
// Testbench skeleton — no ports module tb_my_dut; // ---- Testbench signals ---- reg clk, rst_n; // DUT inputs → reg reg [7:0] data_in; wire [7:0] data_out; // DUT outputs → wire wire valid_out; // ---- DUT instantiation ---- my_dut dut ( .clk (clk), .rst_n (rst_n), .data_in (data_in), .data_out (data_out), .valid_out (valid_out) ); // ---- Clock generation ---- initial clk = 0; always #5 clk = ~clk; // 100 MHz (period = 10 ns) // ---- Stimulus ---- initial begin // reset rst_n = 0; data_in = 8'h00; repeat(4) @(posedge clk); rst_n = 1; // stimulus @(posedge clk); data_in = 8'hA5; @(posedge clk); data_in = 8'h3C; @(posedge clk); $display("Simulation complete at %0t ns", $time); $finish; end endmodule
Always use named port connections — never positional. Named connections are immune to argument reordering bugs and make the testbench self-documenting.
// Named port connection (preferred) my_adder dut ( .a (a), .b (b), .cin (cin), .sum (sum), .cout (cout) ); // Positional — avoid (order-sensitive, fragile) my_adder dut (a, b, cin, sum, cout); // fragile!
Three patterns for generating a clock in simulation:
// Pattern 1 — always block (most common) initial clk = 1'b0; always #5 clk = ~clk; // period = 10 ns → 100 MHz // Pattern 2 — forever loop (clearer intent) initial begin clk = 0; forever #5 clk = ~clk; end // Pattern 3 — asymmetric clock (40/60 duty cycle) initial clk = 0; always begin #4 clk = 1; // high for 4 ns #6 clk = 0; // low for 6 ns end // Multiple clock domains initial clk_a = 0; always #5 clk_a = ~clk_a; // 100 MHz initial clk_b = 0; always #3 clk_b = ~clk_b; // 166 MHz initial clk_c = 0; always #25 clk_c = ~clk_c; // 20 MHz
Always drive inputs to known values before releasing reset. A floating input during reset is a common source of X-propagation bugs that manifest only in gate-level simulation.
initial begin // 1. Initialise all DUT inputs rst_n = 0; data_in = 8'h00; valid = 0; addr = 0; // 2. Hold reset for at least 2 clock edges repeat(4) @(posedge clk); // 4 cycles of reset // 3. Release reset synchronously (on rising edge) @(posedge clk); #1; // small delay past the clock edge rst_n = 1; // deassert reset // 4. Wait a cycle before applying stimulus @(posedge clk); end
#1) to avoid hold-time races in gate-level simulation. This mimics the output flip-flop clock-to-Q delay.Stimulus is driven inside initial blocks. Use @(posedge clk) to synchronize to clock edges:
initial begin // Wait for reset release @(posedge clk iff rst_n); // Drive 5 consecutive transactions repeat(5) begin @(posedge clk); #1; data_in = $urandom_range(0, 255); valid = 1; end // Deassert valid, wait for DUT to drain @(posedge clk); #1; valid = 0; repeat(10) @(posedge clk); $finish; end // Wait for a specific condition (event-based) initial begin @(posedge done); // wait until DUT asserts done $display("Done seen at %0t", $time); end
| Task | When it fires | Best used for |
|---|---|---|
$display | Immediately when executed | Milestone prints, PASS/FAIL messages |
$monitor | End of timestep, whenever watched signals change | Automatic signal logging (call once) |
$strobe | End of current timestep (after all #0) | Stable signal value after all events settle |
$write | Immediately, no newline | Building output line incrementally |
// $monitor — fires whenever a, b, or sum changes initial $monitor("%0t: a=%0d b=%0d sum=%0d cout=%0b", $time, a, b, sum, cout); // $display — prints at this exact simulation time $display("[%0t] Reset deasserted", $time); // $strobe — wait for all events in this timestep $strobe("[%0t] Stable: out = %h", $time, data_out); // Common format specifiers // %0d — decimal, no leading zeros // %h — hex %b — binary %o — octal // %s — string %t — time %0t — time (no leading spaces)
A testbench that only prints outputs forces you to manually inspect the waveform. A self-checking testbench compares every DUT output against an expected value and fails automatically on mismatch:
integer errors = 0; // Task: apply input, check output task apply_and_check( input [7:0] in_a, in_b, input in_cin, input [7:0] exp_sum, input exp_cout ); begin a = in_a; b = in_b; cin = in_cin; @(posedge clk); #1; // wait for DUT to update if (sum !== exp_sum || cout !== exp_cout) begin $display("FAIL: a=%0d b=%0d cin=%0b → sum=%0d(exp %0d) cout=%0b(exp %0b)", in_a, in_b, in_cin, sum, exp_sum, cout, exp_cout); errors = errors + 1; end else $display("PASS: a=%0d b=%0d cin=%0b → sum=%0d", in_a, in_b, in_cin, sum); end endtask initial begin // ... reset sequence ... apply_and_check(8'd10, 8'd20, 0, 8'd30, 0); apply_and_check(8'd255, 8'd1, 0, 8'd0, 1); // overflow apply_and_check(8'd127, 8'd128, 1, 8'd0, 1); if (errors == 0) $display("=== ALL TESTS PASSED ==="); else $fatal(1, "=== %0d TEST(S) FAILED ===", errors); $finish; end
$fatal(1, msg) immediately stops simulation with exit code 1 — CI pipelines detect the failure. $error logs a message but simulation continues, useful for collecting all failures before stopping.Write simulation results to a log file for post-processing, or read stimulus vectors from a file:
integer log_fd; initial begin log_fd = $fopen("sim_log.txt", "w"); if (!log_fd) $fatal(1, "Cannot open log file"); end // Write to file (same format as $display) $fdisplay(log_fd, "%0t: out=%h", $time, data_out); // Close file at end final $fclose(log_fd); // Read stimulus from file with $readmemh reg [7:0] stim_mem [0:255]; initial $readmemh("stimulus.hex", stim_mem);
A complete, self-checking testbench for a 4-bit ripple carry adder, including exhaustive checking of all 512 input combinations:
// DUT (paste in same file or separate .v) module adder4 (input [3:0] a, b, input cin, output [3:0] sum, output cout); assign {cout, sum} = a + b + cin; endmodule // Testbench module tb_adder4; reg [3:0] a, b; reg cin; wire [3:0] sum; wire cout; adder4 dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout)); integer errors = 0; reg [4:0] expected; initial begin // Exhaustive check: all 4+4+1 = 9-bit combinations = 512 cases for (a = 0; a < 16; a = a + 1) for (b = 0; b < 16; b = b + 1) for (cin = 0; cin < 2; cin = cin + 1) begin #5; // combinational — just wait for propagation expected = a + b + cin; if ({cout, sum} !== expected) begin $display("FAIL: %0d+%0d+%0b = %0d(exp %0d)", a, b, cin, {cout,sum}, expected); errors++; end end if (errors == 0) $display("=== ALL 512 COMBINATIONS PASSED ==="); else $fatal(1, "%0d failures detected", errors); $finish; end endmodule
reg, DUT outputs are wire$monitor fires automatically on every signal change; $display fires once when called$fatal / error counters — CI pipelines catch failures automatically$readmemh loads hex stimulus from a file; $fdisplay writes results to a log