Skip to main content

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, b, cin (driven by initial) feed the full_adder DUT, which produces sum and cout, observed by $monitor

A few points that pull together nearly every construct from this topic:

  • full_adder_tb has 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 declared reg in the testbench, even though they're plain combinational inputs on the DUT itself — because the testbench procedurally drives them (inside an initial block), and per Data Types, anything assigned inside a procedural block must be a variable, not a net. The DUT's outputs (sum, cout) are wire, 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.
  • #10 between 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.
  • $finish ends the simulation — without it, most simulators would simply run out of scheduled events and stop anyway here, but explicit $finish is good practice for testbenches with any ongoing process (like a forever-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.