A technical knowledge portal by eVision Systems GmbH

EDA Praxis / Application notes

Develop a small self-checking VHDL testbench

A complete four-bit counter with reset, count, hold and overflow checks, plus a deliberately failing run.

Illustration of an engineering workspace for simulation and verification.

Problem and objective

A waveform can look plausible even when a module behaves incorrectly in a corner case. A self-checking testbench makes the expectation executable: it drives inputs, waits for a defined observation time and compares the result with an independent expected value. This note develops a small complete example. The test checks a four-bit counter with synchronous reset and enable. A deliberately triggered failure also demonstrates whether the run reliably propagates a negative result.

Prerequisites, tools and test environment

The sources use VHDL-2008, the IEEE packages std_logic_1164 and numeric_std, and std.env for test completion. A simulator supporting these constructs is required. The example was actually checked with GHDL 6.0.0, mcode, Windows x64. The official release archive’s SHA-256 was checked against the GitHub release metadata. No run was performed in Active-HDL or Riviera-PRO. GHDL official releases.

The connection to Aldec is the intended transfer into a VHDL workflow using its tools. Analysis options, library mapping and handling of error severity and completion status must be verified in the specific installation. This note claims neither a particular existing licence nor identical command-line options between GHDL and Aldec. Treat the VHDL sources and checking approach separately from the GHDL commands shown below.

Write the specification first

  • At every rising clock edge, an active reset sets the output to zero.
  • Without reset, an active enable increments the value modulo 16.
  • Without reset or enable, the value remains unchanged.
  • Reset takes priority when reset and enable are active simultaneously.
  • No defined output value is promised before the first effective reset edge.

These five statements establish the checking intent. The final statement is particularly important: it prevents incidental simulation initialisation from being treated as guaranteed hardware behaviour. The counter deliberately has no declarative initialisation for its internal state. A new project must decide separately how its target technology implements initialisation and reset. The synchronous reset used here is a teaching choice, rather than a general recommendation for every design.

Step 1: Create the DUT

Save the following complete source as rtl/counter.vhd. Its internal state is a four-bit unsigned vector. The fixed width causes addition to wrap from 15 back to zero. The output is continuously derived from the internal state. The interface remains small for the initial test so that reset, enable and the counter value can be observed unambiguously.

-- EDA Praxis: eigenes didaktisches Beispiel, VHDL-2008.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity counter is
  port (
    clk    : in  std_logic;
    rst    : in  std_logic;
    enable : in  std_logic;
    q      : out unsigned(3 downto 0)
  );
end entity;

architecture rtl of counter is
  signal value : unsigned(3 downto 0);
begin
  process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        value <= (others => '0');
      elsif enable = '1' then
        value <= value + 1;
      end if;
    end if;
  end process;
  q <= value;
end architecture;

Step 2: Separate stimulus from checking

The testbench uses a local procedure named step. Its arguments are reset, enable and the expected output. Inputs change at a falling clock edge; checking occurs one nanosecond after the next rising edge. This leaves sufficient separation between the input change and active edge for this ideal RTL model. The additional sampling delay allows signal assignments, including the output assignment, to take effect.

That one nanosecond is not a timing requirement for a real circuit. It is part of this simple RTL testbench with a ten-nanosecond clock period. For a gate-level simulation with delay information, or for another protocol, the observation point must be justified again. Blindly copying the sampling interval could otherwise conceal a genuine failure or judge correct behaviour too early.

Step 3: The complete testbench

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.all;

entity tb_counter is
  generic (INJECT_FAILURE : boolean := false);
end entity;

architecture test of tb_counter is
  signal clk : std_logic := '0';
  signal rst : std_logic := '1';
  signal enable : std_logic := '0';
  signal q : unsigned(3 downto 0);
begin
  clk <= not clk after 5 ns;
  dut : entity work.counter(rtl) port map (clk, rst, enable, q);

  stimulus : process
    variable errors : natural := 0;
    procedure step(r, e : std_logic; expected : natural) is
    begin
      -- An der Gegenflanke anlegen, nach der aktiven Flanke abtasten.
      wait until falling_edge(clk);
      rst <= r;
      enable <= e;
      wait until rising_edge(clk);
      wait for 1 ns;
      if q /= to_unsigned(expected, q'length) then
        errors := errors + 1;
        report "Zaehlerabweichung, erwartet " & integer'image(expected)
          severity error;
      end if;
    end procedure;
  begin
    step('1', '0', 0);
    for n in 1 to 20 loop
      step('0', '1', n mod 16); -- umfasst 15 -> 0
    end loop;
    step('0', '0', 4);
    step('0', '0', 4);
    step('1', '1', 0);         -- Reset hat Vorrang vor Enable
    step('0', '1', 1);
    if INJECT_FAILURE then
      errors := errors + 1;
      report "Absichtlich ausgeloester Negativtest" severity error;
    end if;
    if errors = 0 then
      report "EDA_TB_PASS" severity note;
      finish(0);
    else
      report "EDA_TB_FAIL" severity failure;
      finish(1);
    end if;
    wait;
  end process;

  watchdog : process
  begin
    wait for 2 us;
    report "Watchdog: Test nicht rechtzeitig beendet" severity failure;
    finish(1);
    wait;
  end process;
end architecture;

Save the source as tb/tb_counter.vhd. The loop counts twenty steps, thereby checking the transition from 15 to zero. Two hold cycles follow. Reset while enable is also active checks priority. The final count step verifies that counting resumes correctly after this reset. The reference model consists of independently specified numbers and a simple modulo expectation; it does not copy an internal DUT state.

The testbench accumulates errors if the environment continues after an error-severity message. It prints a success message only when the error count remains zero. This still does not constitute a universal exit-code guarantee: each runner must define how simulator completion maps to process status. A separate watchdog prevents this test from running indefinitely when ordinary clock or stimulus events fail to arrive.

Step 4: Build in a separate directory

Create a new directory build/plain and change into it. If the sources are arranged in the project root as described and GHDL is available on the search path, use the following commands. Each step must succeed before the next starts. Do not simply replace the executable name with another simulator: these are GHDL-specific options taken from its official documentation. GHDL simulation runtime options.

ghdl -a --std=08 ../../rtl/counter.vhd ../../tb/tb_counter.vhd
ghdl -e --std=08 tb_counter
ghdl -r --std=08 tb_counter --assert-level=error

The first command analyses the two sources in dependency order, the second elaborates the testbench and the third runs it. The actual positive test printed EDA_TB_PASS and completed with exit code 0. That is the observed result for the stated environment. The same result on another simulator version should be confirmed with a separate run.

Step 5: Deliberately exercise the failure path

ghdl -r --std=08 tb_counter -gINJECT_FAILURE=true --assert-level=error

This command sets the generic for the intentional failure. In the executed negative test, GHDL finished with exit code 1. This checks negative feedback as well as the success path. In the shown GHDL workflow, --assert-level=error makes a message of that severity stop the run. Without suitable settings, a surrounding script must not blindly infer success merely because a process has ended.

Troubleshooting and limitations

For an unexpected value, first compare the observation time with the active clock edge. Then check reset priority and the enable sequence. If the entity cannot be found, check analysis order and the working library. If std.env is unavailable, the selected language standard is often incorrect. When the watchdog stops the test, investigate clock generation and waiting processes before increasing the time limit.

The example does not check asynchronous reset, metastability, physical clock quality or post-implementation timing. Nor does it examine every possible input sequence. It is a deliberately small and understandable starting point. Additions should arise from a new requirement, such as repeated reset or longer pauses in enable. Simply increasing the number of similar counting steps does not automatically improve verification coverage.

Actual test status and next step

Executed: analysis, elaboration, a positive run and an intentional negative run with GHDL 6.0.0. Not executed: Aldec simulation, synthesis, hardware testing and timing verification. The environment and observed exit codes are documented in the project test record.

The regression note turns the same counter into two separately selectable VUnit tests. The OSVVM note demonstrates an alternative structured approach to result management. Both build on the same principle: the expectation must exist in the test, and a failure must visibly fail the overall run.

Sources and date

Research checked on 19 October 2026. IEEE 1076-2019: VHDL, GHDL simulation runtime options, GHDL official releases.

Would you like to apply this approach to your project?

Review your development workflow with us

EDA PRAXIS / NEWSLETTER

Fresh perspectives.
Straight to your inbox.

Practical knowledge about HDL, simulation and verification. New articles, application notes and insights into useful tools.