Simple Testbenches
Every module in this topic so far has been the design under test (DUT) — the thing meant to become real hardware. A testbench is different: it's Verilog code whose entire job is to drive stimulus into a DUT and check its response, and which never itself becomes hardware — it only ever runs in simulation. This page is intentionally light; a full treatment of testbench architecture, randomization, and checking belongs to Testbench and, at greater scale, UVM — this page's job is just the bridge, showing the minimum needed to actually exercise the full adder built across this entire topic.
The shape of a minimal testbench
module full_adder_tb;
// testbench signals: reg for anything you drive, wire for anything you read
reg a, b, cin;
wire sum, cout;
// instantiate the DUT
full_adder dut (
.a(a), .b(b), .cin(cin),
.sum(sum), .cout(cout)
);
initial begin
// apply every input combination, 10 time units apart
{a, b, cin} = 3'b000; #10;
{a, b, cin} = 3'b001; #10;
{a, b, cin} = 3'b010; #10;
{a, b, cin} = 3'b011; #10;
{a, b, cin} = 3'b100; #10;
{a, b, cin} = 3'b101; #10;
{a, b, cin} = 3'b110; #10;
{a, b, cin} = 3'b111; #10;
$finish;
end
initial
$monitor("t=%0t a=%b b=%b cin=%b | sum=%b cout=%b", $time, a, b, cin, sum, cout);
endmodule
A few points that pull together nearly every construct from this topic:
full_adder_tbhas no ports. A testbench is the top of the simulation hierarchy — nothing instantiates it, so it has nothing to connect to externally.- The DUT's inputs (
a,b,cin) are declaredregin the testbench, even though they're plain combinational inputs on the DUT itself — because the testbench procedurally drives them (inside aninitialblock), and per Data Types, anything assigned inside a procedural block must be a variable, not a net. The DUT's outputs (sum,cout) arewire, since the testbench only ever reads them. {a, b, cin} = 3'b000;is the concatenation operator from Operators and Expressions, used here on the left-hand side to assign three separate signals from one 3-bit literal in a single line — a common testbench shorthand.#10between each stimulus line is exactly the simulation-only delay from Timing Controls — spacing out each input combination so$monitor(from System Tasks and Directives) has time to print a settled result before the next change.$finishends the simulation — without it, most simulators would simply run out of scheduled events and stop anyway here, but explicit$finishis good practice for testbenches with any ongoing process (like aforever-looped clock generator) that would otherwise never naturally end.
Dumping a waveform instead of reading a printout
$monitor's printed lines work for a circuit this small, but for anything bigger, a waveform viewer (a GUI showing every signal's value over time as a timing diagram) is the practical way to actually inspect behavior. $dumpfile/$dumpvars are the system tasks that make that possible:
initial begin
$dumpfile("full_adder_tb.vcd"); // where to write the waveform data
$dumpvars(0, full_adder_tb); // 0 = dump every signal, recursively, from full_adder_tb down
end
$dumpfile names the output file (in the standard VCD — Value Change Dump — format nearly every waveform viewer reads); $dumpvars(0, full_adder_tb) tells the simulator to record every signal inside full_adder_tb and everything instantiated beneath it (the DUT included), so the whole hierarchy shows up in the viewer. Like $monitor, both are simulation-only and appear only in testbenches, never in RTL.
Self-checking: the next step past "watch the printout"
The testbench above only displays results — a human has to compare each printed line against the expected sum/carry by hand. A self-checking testbench compares the DUT's output against an expected value in code and reports a pass/fail directly:
initial begin
{a, b, cin} = 3'b011; #10;
if ({cout, sum} !== (a + b + cin))
$display("FAIL at t=%0t: got sum=%b cout=%b", $time, sum, cout);
else
$display("PASS at t=%0t", $time);
end
{cout, sum} concatenates the DUT's two outputs into one 2-bit value, compared against a + b + cin computed directly by Verilog's own + operator — a genuinely independent "reference model" for a circuit this simple, since the testbench isn't reusing any of the DUT's own logic to check itself. This is exactly the idea that Testbench and UVM build on at real scale: a self-checking testbench that doesn't need a human watching a waveform is the only kind of check that stays useful once a design is too large, or is re-run too often (regression testing), for manual inspection to be practical.
What's next
The topic closes with one complete worked example, larger than the full adder that's threaded through every page so far — a full RTL implementation of the serial frame receiver controller from Digital Design's FSM Example Walkthrough, applying gate/dataflow/behavioral modeling, FSM coding style, and a testbench together on one real design.