Creating testbenches in Verilog is an essential practice to verify the functionality of your modules and ensure your design behaves as expected.
A testbench is a special Verilog module used only for simulation. It instantiates the Device Under Test (DUT), applies input stimuli, and monitors the outputs to validate functionality.
What Is a Testbench?A testbench is not synthesized and is used exclusively for simulation. It is responsible for applying inputs to the DUT and observing its outputs. It acts as a controlled environment where your design can be tested before hardware implementation.
Basic Structure of a Verilog TestbenchA typical testbench includes the following components:
- Signal declaration β using
regandwire - DUT instantiation β connecting the module under test
- Stimulus generation β applying input combinations
- Output monitoring β observing and logging results
- Timing control β using
#delayto sequence events
This example demonstrates a complete testbench for a 1-bit Full Adder, a combinational circuit that adds two bits and a carry-in, producing a Sum and a Carry-out.
Full Adder ModuleThe Full Adder is implemented using a single continuous assignment:
module full_adder (
input A,
input B,
input Cin,
output Sum,
output Cout
);
assign {Cout, Sum} = A + B + Cin;
endmoduleFull Adder TestbenchThe testbench applies all 8 possible input combinations, covering the complete truth table:
module tb_full_adder;
reg A, B, Cin;
wire Sum, Cout;
// DUT Instantiation
full_adder uut (
.A(A),
.B(B),
.Cin(Cin),
.Sum(Sum),
.Cout(Cout)
);
// Stimulus block
initial begin
A = 0; B = 0; Cin = 0;
#10 A = 0; B = 0; Cin = 1;
#10 A = 0; B = 1; Cin = 0;
#10 A = 0; B = 1; Cin = 1;
#10 A = 1; B = 0; Cin = 0;
#10 A = 1; B = 0; Cin = 1;
#10 A = 1; B = 1; Cin = 0;
#10 A = 1; B = 1; Cin = 1;
#10;
$finish;
end
// Monitor block
initial begin
$monitor("A=%b, B=%b, Cin=%b -> Sum=%b, Cout=%b",
A, B, Cin, Sum, Cout);
end
endmoduleSimplified Testbench DiagramThe diagram below illustrates the relationship between the testbench and the DUT: the testbench drives input signals into the DUT and monitors the output signals in return.
Signal Declaration
Inputs of the DUT are declared as reg because they are driven procedurally inside initial or always blocks. Outputs are declared as wire since they are driven by the DUT itself.
reg A, B, Cin;
wire Sum, Cout;DUT Instantiation
The DUT is instantiated and connected to the testbench signals using named port mapping. The instance is named uut (Unit Under Test), a common naming convention also adopted by tools such as Vivado.
full_adder uut (
.A(A),
.B(B),
.Cin(Cin),
.Sum(Sum),
.Cout(Cout)
);Stimulus Generator
The initial block applies different input combinations using #delay for timing control. Each #10 represents a 10-time-unit simulation delay, giving the circuit time to settle before the next input is applied.
#10 A = 1; B = 1; Cin = 0;Output Monitoring
The $monitor system task automatically prints signal values to the console whenever any of them change, creating a live log of simulation behavior.
$monitor("A=%b, B=%b, Cin=%b -> Sum=%b, Cout=%b",
A, B, Cin, Sum, Cout);Running the Simulation
With the testbench ready, compile and run the simulation using tools such as Vivado Simulator, ModelSim, Xcelium or Icarus Verilog (which pairs well with VSCode for a lightweight setup).
The waveform below was generated using Vivado:
Verify that every column in the waveform matches the expected Sum and Cout values from the Full Adder truth table.
Full Adder Truth Table
A B Cin | Sum Cout
-------------------
0 0 0 | 0 0
0 0 1 | 1 0
0 1 0 | 1 0
0 1 1 | 0 1
1 0 0 | 1 0
1 0 1 | 0 1
1 1 0 | 0 1
1 1 1 | 1 1Improving the TestbenchHere are some ways to enhance your testbench:
- Automatic verification: Use assertions to compare DUT outputs with expected values.
- Randomized testing: Apply random stimuli using
$randomto check for unexpected behavior. - Edge case testing: Specifically test boundary values and transitions for robustness.
Simulation-first design is a professional industry practice. A well-written testbench helps you prevent hardware debugging headaches, validate logic before synthesis, reduce FPGA iteration time, and improve overall design reliability.
The full source code is available on GitHub.






Comments