Skip to content

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:

wait until falling_edge(clk_s);
enable_s <= '1';

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:

wait until rising_edge(clk_s);
wait for 0 ns;
assert count_s = expected;

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.