Simulation and Testbenches
Simulation is where you ask design questions before hardware makes them expensive. A testbench is not loaded into the FPGA; it is a verification environment that drives inputs, checks outputs, and leaves enough evidence to debug failures.
Learning Objectives
By the end of this lesson, you should be able to:
- distinguish synthesizable RTL from simulation-only testbench code;
- create a clock, reset sequence, stimulus, and checks;
- use assertions or explicit comparisons to make tests self-checking;
- generate waveforms for debugging rather than manual pass/fail judgment;
- identify simulation habits that reduce hardware bring-up risk.
RTL vs Testbench Code
| Design RTL | Testbench |
|---|---|
| Synthesizes into FPGA logic | Runs only in the simulator |
| Describes registers, combinational logic, memories, and interfaces | Drives inputs and observes outputs |
| Must follow synthesis rules | May use delays, file I/O, loops, tasks, procedures, and assertions |
| Must meet timing after place and route | Must expose behavior clearly |
| Becomes hardware | Becomes evidence |
Treat the design as the device under test, often abbreviated DUT. The testbench should behave like a lab instrument: apply known conditions, measure the response, and report clear failures.
A Useful Testbench Structure
A waveform-only testbench is a viewing setup. A self-checking testbench is a verification tool.
Minimal Verilog Example
`timescale 1ns/1ps
module tb_counter8;
reg clk = 1'b0;
reg rst = 1'b1;
reg en = 1'b0;
wire [7:0] count;
counter8 dut (
.clk(clk),
.rst(rst),
.en(en),
.count(count)
);
always #5 clk = ~clk; // 100 MHz illustrative clock
initial begin
$dumpfile("counter8.vcd");
$dumpvars(0, tb_counter8);
repeat (2) @(posedge clk);
rst <= 1'b0;
en <= 1'b1;
repeat (4) @(posedge clk);
#1;
if (count !== 8'd4) begin
$display("FAIL: expected count 4, got %0d", count);
$finish;
end
en <= 1'b0;
repeat (2) @(posedge clk);
#1;
if (count !== 8'd4) begin
$display("FAIL: enable hold broken");
$finish;
end
$display("PASS: counter8");
$finish;
end
endmodule
The #1 after the clock edge lets nonblocking assignments update before the check. In a SystemVerilog environment you may use clocking blocks or structured sampling, but the beginner rule is simple: do not drive and check everything at the exact same simulator instant.
What Good Tests Check
Good simulations check requirements, not just activity:
- reset state and reset release;
- first valid output after enable;
- normal transactions;
- boundary values, overflow, and wrap;
- hold or stall behavior;
- invalid inputs and unused states;
- back-to-back operations;
- recovery after reset or error.
The best first tests are small and deterministic. Random tests are useful later, but only when you already know how to reproduce and debug a failure.
Worked Example: Reset Requirement
Requirement: while reset is active at a rising clock edge, count must become zero.
Test plan:
- Let the counter reach a nonzero value.
- Assert reset.
- Wait for the active clock edge.
- Check
count == 0. - Release reset and check that counting resumes.
This catches missing reset wiring, inverted reset polarity, incomplete reset assignments, and testbenches that never actually exercise reset.
Waveforms Help Explain Timing
title "Illustrative clocked simulation"
time start=0 end=80 unit=ns divisions=8
CLK: square label="clk" low=0 high=1 duty=50 cycles=8 unit=logic color=#2563eb
RST: pulse label="rst" low=0 high=1 at=0 width=20 unit=logic color=#dc2626
EN: step label="en" low=0 high=1 at=25 unit=logic color=#7c3aed
COUNT: sawtooth label="count rises" min=0 max=5 cycles=1 unit=count color=#16a34a
marker RELEASE at=20 label="reset off"
marker CHECK at=70 label="check"
This is an explanatory waveform, not a simulator result. A real VCD should reflect the exact RTL, simulator scheduling, and testbench stimulus.
Verification Workflow
For each block:
- Simulate reset and a simple valid transaction.
- Add edge cases.
- Make every expected behavior a check.
- Dump a waveform only when debugging or documenting.
- Keep the test command repeatable.
- Run the simulation before synthesis after every meaningful RTL change.
Simulation does not prove timing closure, pin constraints, electrical safety, or board wiring. It proves logic behavior under the modeled conditions.
Common Failure Symptoms
| Symptom | Likely cause |
|---|---|
Output is X or U |
Missing reset, uninitialized signal, or multiple drivers |
| Test passes without checking anything | Waveform-only testbench |
| One-cycle mismatch | Checking before nonblocking or signal updates settle |
| Simulator never exits | Missing $finish, std.env.stop, or final wait strategy |
| Failure disappears in waveform | Race caused by driving inputs on the active edge |
| Hardware still fails | Timing, constraints, CDC, reset, or board issue not modeled |
Debugging Guidance
Read the first failure message first. Then inspect the smallest waveform window that includes the previous reset, the stimulus change, the active clock edge, and the failing output. Avoid changing both RTL and testbench at once; if you do, you will not know which change fixed the symptom.
When a design has many states, add checks for legal state transitions and timeouts. A timeout is important because a deadlocked FSM may otherwise leave the simulator running forever.
Common Mistakes
- Treating "no simulator crash" as success.
- Looking at a waveform once and never automating the check.
- Forgetting reset behavior.
- Driving inputs exactly on the active clock edge and creating artificial races.
- Testing only the happy path.
- Assuming simulation checks timing constraints or metastability.
Summary
A good testbench is a repeatable experiment. It creates clock and reset conditions, applies meaningful stimulus, checks expected behavior automatically, and uses waveforms to debug failures. Hardware testing is still required, but it should not be the first time your design is exercised.
Next: Verilog Simulation: Verilator, Icarus Verilog, and GTKWave.