Day 02 / 25
← Day 01 Day 03 →
HomeVerificationDay 2 — Verilog Testbench
Track 1 — Foundations

Verilog Testbench Basics

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.

⏱ 20 min read📖 Day 2 of 25🎯 Verilog · Testbench
Contents
  1. Testbench Module Structure
  2. DUT Instantiation
  3. Clock Generation
  4. Reset Sequencing
  5. Driving Stimulus
  6. $display, $monitor, $strobe
  7. Self-Checking Testbench
  8. File I/O with $fopen / $fclose
  9. Full Example: 4-bit Adder Testbench

1. Testbench Module Structure

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

2. DUT Instantiation

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!
Common mistake: Connecting an output wire of the DUT to a reg in the testbench causes a multi-driver conflict. DUT outputs must be wire; only signals driven by initial/always blocks should be reg.

3. Clock Generation

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

4. Reset Sequencing

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
Best practice: Drive signal changes with a small delay after the clock edge (e.g., #1) to avoid hold-time races in gate-level simulation. This mimics the output flip-flop clock-to-Q delay.

5. Driving Stimulus

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

6. $display, $monitor, $strobe

TaskWhen it firesBest used for
$displayImmediately when executedMilestone prints, PASS/FAIL messages
$monitorEnd of timestep, whenever watched signals changeAutomatic signal logging (call once)
$strobeEnd of current timestep (after all #0)Stable signal value after all events settle
$writeImmediately, no newlineBuilding 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)

7. Self-Checking Testbench

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 vs $error: $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.

8. File I/O with $fopen / $fclose

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);

9. Full Example: 4-bit Adder Testbench

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

Key Takeaways — Day 2

Next → Day 03
SystemVerilog for Verification
Interfaces, clocking blocks, programs, mailbox, semaphore, virtual interfaces — the SV constructs unique to testbenches.