The simplest testbench you can write is a linear one. It applies a fixed sequence of inputs to the DUT, waits a set amount of time between each, and relies on the engineer to look at the output waveform to decide whether the design passed or failed. There is no automation, no randomization, and no intelligence — just stimulus applied one step at a time, in order.
8 min read | Intermediate Level
What You'll Learn
- The structure of a linear testbench: DUT instantiation, clock generation, reset, and stimulus
- How to check outputs using
$displayand manual waveform inspection - Why the linear style breaks down as designs grow in complexity
- When a linear testbench is still the right tool for the job
The DUT: A 4-bit Adder
Throughout this article we will verify a simple 4-bit adder. It takes two 4-bit inputs a and b, plus a single-bit cin (carry-in), and produces a 4-bit sum and a single-bit cout (carry-out). This is small enough that a linear testbench can cover it meaningfully, which makes it a good vehicle for understanding the style.
// DUT: 4-bit adder
// Inputs : a, b (4-bit operands)
// cin (carry-in from a previous stage)
// Outputs: sum (4-bit result)
// cout (carry-out — set when the result exceeds 4 bits)
module adder (
input [3:0] a,
input [3:0] b,
input cin,
output [3:0] sum,
output cout
);
assign {cout, sum} = a + b + cin;
endmodule
Structure of a Linear Testbench
A linear testbench is a separate module — it has no ports of its own because it is the top of the simulation hierarchy. It does three things in order: sets up the DUT and generates a clock, applies reset, then drives a hand-written sequence of input values and checks the outputs.
Step 1 — Instantiate the DUT and Generate a Clock
The testbench declares signals that connect to the DUT's ports, instantiates the DUT, and starts a clock oscillating. The clock is generated with a simple always block that flips the clock signal every half-period.
module tb_adder;
// ── Signals that connect to the DUT ports ──────────────────────
reg [3:0] a; // operand A — driven by the testbench
reg [3:0] b; // operand B — driven by the testbench
reg cin; // carry-in — driven by the testbench
wire [3:0] sum; // result — driven by the DUT, observed here
wire cout; // carry-out — driven by the DUT, observed here
// ── Instantiate the DUT ────────────────────────────────────────
// Connect testbench signals to DUT ports by name
adder dut (
.a (a),
.b (b),
.cin (cin),
.sum (sum),
.cout(cout)
);
// ── Clock generation ───────────────────────────────────────────
// This adder is combinational, so the clock is only used
// here to pace the stimulus — one test per clock cycle.
reg clk = 0;
always #5 clk = ~clk; // toggle every 5 time units → 10-unit period
The adder above is combinational — its output updates immediately whenever an input changes, with no clock needed for the logic itself. The clock in the testbench is used purely to control the pace of stimulus: we apply one set of inputs per clock cycle so the waveform is easy to read.
Step 2 — Apply Reset
Even for combinational logic, it is good practice to start the simulation with all inputs at a known value before applying test cases. For sequential designs, this step initialises registers to a defined state.
// ── Stimulus: applied inside an initial block ──────────────────
// An initial block runs once at the start of simulation
initial begin
// Start with all inputs at zero — known initial state
a = 4'b0000;
b = 4'b0000;
cin = 1'b0;
// Wait for two full clock cycles before applying test cases
@(posedge clk);
@(posedge clk);
Step 3 — Drive Stimulus and Check Outputs
After reset, the testbench applies each test case one at a time. After each input is applied, we wait for the next clock edge (giving the DUT time to settle) then print the result using $display. The engineer manually checks whether the printed values match the expected results.
// ── Test case 1: 0 + 0 + 0 = 0, no carry ─────────────────
a = 4'd0; b = 4'd0; cin = 0;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=0, cout=0
// ── Test case 2: 3 + 5 + 0 = 8, no carry ─────────────────
a = 4'd3; b = 4'd5; cin = 0;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=8, cout=0
// ── Test case 3: 7 + 8 + 0 = 15, no carry ────────────────
a = 4'd7; b = 4'd8; cin = 0;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=15, cout=0
// ── Test case 4: 8 + 8 + 0 = 16 → sum=0, cout=1 ─────────
// Result exceeds 4 bits — carry-out should assert
a = 4'd8; b = 4'd8; cin = 0;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=0, cout=1
// ── Test case 5: 15 + 15 + 1 = 31 → sum=15, cout=1 ──────
// Maximum possible inputs with carry-in
a = 4'd15; b = 4'd15; cin = 1;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=15, cout=1
$display("Simulation complete.");
$finish; // tell the simulator to stop
end
endmodule
Simulation Output
When this testbench runs, the simulator prints the following to the log. The engineer reads these lines and manually compares each result against the expected value written in the comments:
a=0 b=0 cin=0 | sum=0 cout=0 a=3 b=5 cin=0 | sum=8 cout=0 a=7 b=8 cin=0 | sum=15 cout=0 a=8 b=8 cin=0 | sum=0 cout=1 a=15 b=15 cin=1 | sum=15 cout=1 Simulation complete.
All five results match their expected values. The testbench has done its job for these five cases.
Quick Check
Q: Test case 4 inputs are a=8, b=8, cin=0. The expected output is sum=0, cout=1. Verify this manually: what is 8 + 8 in decimal, and why does a 4-bit sum field show 0 while cout is 1?
Show Answer
8 + 8 = 16 in decimal, which is 5'b1_0000 in binary — a 5-bit result. The 4-bit sum field holds the lower four bits: 4'b0000 = 0. The fifth bit, which overflowed the 4-bit boundary, is captured as cout = 1. This is exactly the carry-out mechanism a multi-stage adder uses to chain stages together.
What the Waveform Shows
Alongside the log output, the simulator records every signal's value over time in a waveform database. Opening it in a waveform viewer shows a, b, cin, sum, and cout changing at each clock edge as the testbench steps through its five cases. For a five-case test like this, eyeballing the waveform takes about thirty seconds.
This is the fundamental problem: waveform inspection scales with the number of test cases. Five cases takes thirty seconds. Five hundred cases takes an hour — and realistically, a verification engineer running a regression on a complex block needs thousands of test cases. Manual waveform inspection stops being feasible long before that point.
Limitations of the Linear Testbench
The five test cases above cover a handful of representative values. The 4-bit adder has 512 possible input combinations (16 × 16 × 2 for a, b, and cin). The linear testbench covers five of them — less than 1%. Nobody wrote the other 507 test cases because doing so by hand is impractical, and there is no way to know which of the untested combinations might expose a bug.
Three limitations flow directly from this:
No automation. Every input value was typed by hand. Every expected result was computed by hand and written as a comment. If the DUT produces a wrong answer and the engineer misreads a line in the log, the bug goes undetected. The testbench has no way to flag a failure on its own.
No scalability. Adding more test cases means typing more lines. There is no mechanism to generate inputs programmatically or to explore the input space systematically. For a 4-bit adder this is manageable; for a 32-bit ALU with dozens of operations it is not.
No reuse. This testbench is written specifically for this adder. Every signal name, every port connection, every expected value is tied to this exact DUT. Nothing in it can be carried to the next project.
A linear testbench with no automatic output checking can pass a regression even when the DUT is producing wrong results — as long as the engineer does not notice the mismatch in the log. Never rely on $display alone for sign-off level testing.
When a Linear Testbench Is the Right Tool
Despite these limitations, the linear testbench has a legitimate role. Use it when:
The DUT is small and the input space is exhaustible. A 2-to-1 multiplexer has four input combinations. A linear testbench can cover all of them in eight lines. For designs at this scale, building a more complex environment would take longer than writing the test.
You need a quick smoke test on new RTL. When a designer hands over a first RTL drop, the fastest way to confirm it compiles, resets, and produces any output at all is a linear testbench. It is a sanity check, not a sign-off vehicle.
You are debugging a specific known scenario. When a bug has been isolated to a precise sequence of inputs, a linear testbench that reproduces exactly that sequence is often the clearest way to confirm the fix. There is no randomization to get in the way.
Think of the linear testbench as a screwdriver, not a power drill. For the right job it is the fastest tool in the box. For the wrong job, reaching for it first costs time you will not get back.
Common Mistakes
Mistake #1: Forgetting to wait for the DUT output to settle before sampling it.
Wrong: Applying new inputs and calling $display in the same time step, reading the output before the combinational logic has updated.
Right: Applying inputs, then waiting for a clock edge (or a small delay like #1) before reading and printing the output — giving the simulator time to propagate the new values through the DUT.
Why? In an event-driven simulator, signal propagation is not instantaneous within a time step. Reading an output in the same time step as the input change may capture the old value, producing a misleading log that shows correct results even when the DUT has not yet computed them.
Mistake #2: Treating $display output as a passing result without verifying each line.
Wrong: Running the testbench, seeing no simulator errors, and concluding the design is correct — without checking whether the printed values actually match the expected results.
Right: Comparing every printed value against the expected result written in the testbench comments, or better, adding an explicit check using $error when the output does not match.
Why? $display always prints, regardless of whether the value is correct. A testbench that only uses $display will log "Simulation complete" even when every result is wrong. The simulator reports what happened — the engineer has to judge whether it was right.
Mistake #3: Using a linear testbench for a sequential DUT without accounting for pipeline latency.
Wrong: Applying an input to a registered pipeline and reading the output one clock cycle later, when the DUT actually takes two cycles to produce the result — reading X or a stale value instead of the correct output.
Right: Checking the DUT specification for the exact number of clock cycles between input and valid output, and waiting that many cycles before sampling $display.
Why? A linear testbench has no mechanism to detect that an output is not yet valid — it reads whatever is on the output wire at the moment $display executes. For pipelined or registered designs, timing the read incorrectly is one of the most common sources of false-passing tests.
Practice Exercise
Challenge: Extend the Testbench
The testbench above has five test cases but is missing two important boundary conditions for the adder:
- The minimum carry-in case:
a=0, b=0, cin=1— result should besum=1, cout=0. - A case that produces the maximum possible sum:
a=15, b=14, cin=0— verify whether this produces a carry-out.
Add both test cases to the initial block in the correct position (before $finish) and write the expected result as a comment next to each $display.
Hint
For case 2: 15 + 14 = 29 in decimal. A 4-bit field holds values 0–15. What are the lower four bits of 29, and what is the carry-out bit?
Sample Solution
// ── Boundary: minimum carry-in ─────────────────────────────
a = 4'd0; b = 4'd0; cin = 1;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=1, cout=0
// ── Boundary: 15 + 14 = 29 → sum=13, cout=1 ──────────────
// 29 in binary = 5'b1_1101
// Lower 4 bits = 4'b1101 = 13, carry-out = 1
a = 4'd15; b = 4'd14; cin = 0;
@(posedge clk);
$display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
a, b, cin, sum, cout);
// Expected: sum=13, cout=1
Summary
- A linear testbench instantiates the DUT, generates a clock, applies a fixed stimulus sequence in an
initialblock, and uses$displayto print outputs for manual inspection - Output checking is entirely manual — the engineer compares log output against expected values written as comments
- The linear testbench has three core limitations: no automatic checking, no scalability beyond a small number of hand-crafted cases, and no reuse across projects
- It remains the right tool for small exhaustible input spaces, quick smoke tests on new RTL, and reproducing a specific known bug scenario
- For sequential or pipelined designs, the number of cycles between input and valid output must be accounted for explicitly — the testbench has no way to detect a premature read