Testbench fundamentals¶
A testbench is VHDL with no external ports. It instantiates the design under test (DUT), drives inputs, observes outputs, and decides pass/fail.
Minimal structure¶
library ieee;
use ieee.std_logic_1164.all;
use std.env.finish;
entity tb_example is
end entity;
architecture sim of tb_example is
signal input_s : std_logic := '0';
signal output_s : std_logic;
begin
dut : entity work.example(rtl)
port map (
input_i => input_s,
output_o => output_s
);
stimulus : process
begin
input_s <= '0';
wait for 1 ns;
assert output_s = '0' severity failure;
input_s <= '1';
wait for 1 ns;
assert output_s = '1' severity failure;
report "PASS";
finish;
end process;
end architecture;
Clock generation¶
constant CLOCK_PERIOD : time := 10 ns;
signal clk_s : std_logic := '0';
clk_s <= not clk_s after CLOCK_PERIOD / 2;
This is simulation-only. It creates the 100 MHz clock used by the Basys 3 design.
An alternative stoppable generator:
clock_process : process
begin
while simulation_running loop
clk_s <= '0';
wait for CLOCK_PERIOD / 2;
clk_s <= '1';
wait for CLOCK_PERIOD / 2;
end loop;
wait;
end process;
Reset stimulus¶
For synchronous active-high reset:
reset_s <= '1';
wait until rising_edge(clk_s);
wait until rising_edge(clk_s);
reset_s <= '0';
wait until rising_edge(clk_s);
Driving inputs away from the active clock edge avoids artificial races:
Then the DUT sees a stable value at the next rising edge.
Checking registered outputs¶
After a rising edge, registered signal updates occur in a delta cycle. A robust pattern is:
Alternatively check just before the following active edge, or use clocking procedures that establish a consistent phase.
Be consistent about whether an expected model is updated before or after the DUT edge.
Assertions¶
assert actual = expected
report "count mismatch: expected=" & integer'image(expected) &
" actual=" & integer'image(actual)
severity failure;
The condition describes success. The report runs only when it is false.
Add context:
- Transaction number.
- Input values.
- Expected and actual values.
- State or cycle count.
Reusable check procedure¶
procedure check_equal(
constant actual : in natural;
constant expected : in natural;
constant message : in string
) is
begin
assert actual = expected
report message & ": expected=" & integer'image(expected) &
", actual=" & integer'image(actual)
severity failure;
end procedure;
Watchdog timeout¶
Every test that waits for a condition should have an independent timeout:
watchdog : process
begin
wait for 1 ms;
assert false
report "TIMEOUT: test did not finish within 1 ms"
severity failure;
end process;
finish ends the simulation before the watchdog fires on success.
Procedures that drive signals¶
procedure apply_and_check(
signal a_s : out std_logic;
signal b_s : out std_logic;
signal y_s : in std_logic;
constant a_value : in std_logic;
constant b_value : in std_logic;
constant expected_y : in std_logic
) is
begin
a_s <= a_value;
b_s <= b_value;
wait for 1 ns;
assert y_s = expected_y severity failure;
end procedure;
Signal parameters are required when a procedure schedules signal assignments.
Initialization strategy¶
Initialize testbench-driven signals explicitly. In the DUT, use reset to establish operational state. An initial declaration value may synthesize on many FPGAs, but it should not replace a deliberate reset/startup contract where one is required.
Waveforms¶
Open waveforms after a failure to answer:
- Did the driver apply the intended transaction?
- Which cycle first diverged?
- Was reset/enable polarity correct?
- Was a result sampled too early?
- Is an unknown propagating?
Do not manually inspect waveforms for every regression run.
Testbench checklist¶
- No external ports.
- DUT is directly instantiated.
- Clock and reset match the specification.
- Inputs change at a deliberate phase.
- Expected values are automatically checked.
- Failure reports contain useful context.
- A timeout catches hangs.
- Success ends with
finish.