Loading header...

Exercise: VHDL Counter and Testbench

This exercise turns the VHDL rules from the previous lessons into a complete FPGA habit: write a small register-transfer design, simulate it before hardware, and make the testbench decide pass or fail automatically.

Learning Objectives

By the end of this exercise, you should be able to:

  • write a synthesizable clocked counter using numeric_std;
  • separate design RTL from non-synthesizable testbench code;
  • verify synchronous reset, enable hold, increment, wrap, and terminal count;
  • generate a VCD waveform for debugging;
  • explain common VHDL simulation failures without guessing.

Prerequisites

You should already know:

  • VHDL entity, architecture, signal, and port syntax;
  • std_logic, std_logic_vector, and unsigned;
  • the difference between combinational and sequential logic;
  • how rising_edge(clk) describes edge-triggered registers;
  • the basic GHDL flow: analyze, elaborate, and run.

Install GHDL and GTKWave if you want to run the waveform portion locally. The RTL is written for a generic FPGA flow and does not depend on a board constraint file.

Concrete Task

Create two files:

  • counter4.vhd, a synthesizable 4-bit up-counter;
  • counter4_tb.vhd, a self-checking testbench.

The counter requirements are:

  • Clock: state changes only on rising_edge(clk).
  • Reset: rst = '1' synchronously loads zero.
  • Enable: en = '1' increments by one.
  • Hold: en = '0' keeps the previous count.
  • Wrap: 1111 + 1 becomes 0000.
  • Terminal count: terminal_count = '1' when count is 15.

Use active-high reset and keep all arithmetic in unsigned. Do not use nonstandard packages such as std_logic_unsigned.

Implementation: counter4.vhd

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

entity counter4 is
    port (
        clk            : in  std_logic;
        rst            : in  std_logic;
        en             : in  std_logic;
        count          : out std_logic_vector(3 downto 0);
        terminal_count : out std_logic
    );
end entity counter4;

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

    count <= std_logic_vector(count_r);
    terminal_count <= '1' when count_r = 15 else '0';
end architecture rtl;

terminal_count is combinational because it directly reflects the current registered count. That is appropriate for this exercise, but in a larger design you may register it if another module needs a timing-friendly one-cycle pulse.

Testbench: counter4_tb.vhd

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

entity counter4_tb is
end entity counter4_tb;

architecture tb of counter4_tb is
    constant CLK_PERIOD : time := 10 ns;

    signal clk            : std_logic := '0';
    signal rst            : std_logic := '1';
    signal en             : std_logic := '0';
    signal count          : std_logic_vector(3 downto 0);
    signal terminal_count : std_logic;

    procedure expect_count(
        signal observed : in std_logic_vector(3 downto 0);
        constant value  : in natural;
        constant label  : in string
    ) is
    begin
        assert unsigned(observed) = to_unsigned(value, observed'length)
            report label & ": expected " & integer'image(value) &
                   ", got " & integer'image(to_integer(unsigned(observed)))
            severity error;
    end procedure;
begin
    clk <= not clk after CLK_PERIOD / 2;

    dut : entity work.counter4
        port map (
            clk            => clk,
            rst            => rst,
            en             => en,
            count          => count,
            terminal_count => terminal_count
        );

    stimulus : process
    begin
        -- Synchronous reset takes effect on a rising clock edge.
        rst <= '1';
        en  <= '0';
        wait until rising_edge(clk);
        wait for 1 ns;
        expect_count(count, 0, "reset");

        rst <= '0';
        en  <= '1';
        for i in 1 to 4 loop
            wait until rising_edge(clk);
            wait for 1 ns;
            expect_count(count, i, "increment " & integer'image(i));
        end loop;

        en <= '0';
        wait until rising_edge(clk);
        wait for 1 ns;
        expect_count(count, 4, "enable hold");

        en <= '1';
        for i in 5 to 15 loop
            wait until rising_edge(clk);
            wait for 1 ns;
            expect_count(count, i, "count to " & integer'image(i));
        end loop;

        assert terminal_count = '1'
            report "terminal_count was not high at 15"
            severity error;

        wait until rising_edge(clk);
        wait for 1 ns;
        expect_count(count, 0, "wrap");

        assert terminal_count = '0'
            report "terminal_count stayed high after wrap"
            severity error;

        assert false report "PASS counter4 testbench complete" severity note;
        wait;
    end process;
end architecture tb;

The wait for 1 ns statements avoid checking a signal in the same simulator delta cycle as the clock edge. In real testbenches you can also sample on a later phase of the clock.

Run And Inspect

ghdl -a counter4.vhd
ghdl -a counter4_tb.vhd
ghdl -e counter4_tb
ghdl -r counter4_tb --vcd=counter4.vcd
gtkwave counter4.vcd

Expected terminal output includes a note similar to:

counter4_tb.vhd:...: assertion note: PASS counter4 testbench complete

If GHDL reports assertion error, read the first failing message before inspecting the waveform. The first failure is usually closest to the real bug.

Expected Behavior

title "Illustrative counter test sequence"
time start=0 end=90 unit=ns divisions=9

CLK: square label="clk" low=0 high=1 duty=50 cycles=9 unit=logic color=#2563eb
RST: pulse label="rst high" low=0 high=1 at=0 width=10 unit=logic color=#dc2626
EN: step label="en" low=0 high=1 at=12 unit=logic color=#7c3aed
COUNT: sawtooth label="count 0 to wrap" min=0 max=15 cycles=1 unit=count color=#16a34a
TC: pulse label="terminal_count" low=0 high=1 at=78 width=8 unit=logic color=#ea580c

marker RESET at=5 label="reset edge"
marker WRAP at=85 label="wrap"

This waveform is explanatory. Your VCD should show the exact simulator events produced by the code.

Verification Steps

Check all of these before calling the exercise complete:

  • ghdl -a counter4.vhd succeeds with no syntax errors.
  • ghdl -a counter4_tb.vhd succeeds after the design file is analyzed.
  • ghdl -e counter4_tb finds the testbench entity.
  • ghdl -r counter4_tb --vcd=counter4.vcd ends with the PASS note and no assertion errors.
  • count stays 0000 during reset.
  • count increments once per rising clock edge while en = '1'.
  • count holds while en = '0'.
  • terminal_count is high only when count = "1111".
  • The next increment after 15 wraps to 0.

Common Failure Symptoms

  • no declaration for unsigned: missing use ieee.numeric_std.all;.
  • unit counter4 not found: testbench analyzed before design or wrong entity name.
  • Count remains undefined: register lacks reset or initialization in simulation.
  • First increment check fails: output sampled before the clocked signal update settled.
  • Terminal count never asserts: compared a std_logic_vector incorrectly or used the wrong width.
  • Wrap check fails: counter was converted to an integer with an out-of-range limit.

Debugging Guidance

Start with the first assertion error. Confirm the compile order, then open counter4.vcd and inspect clk, rst, en, count, and terminal_count. If the waveform looks one cycle later than your expectation, review whether the counter is synchronous and whether the testbench waits until after the active clock edge.

For arithmetic bugs, temporarily add reports in the testbench rather than changing the RTL blindly. The design should stay simple: one register process, one output conversion, and one terminal-count comparison.

Extension Challenge

Make the counter width configurable:

entity counter_n is
    generic (
        WIDTH : positive := 8
    );
    port (
        clk            : in  std_logic;
        rst            : in  std_logic;
        en             : in  std_logic;
        count          : out std_logic_vector(WIDTH - 1 downto 0);
        terminal_count : out std_logic
    );
end entity;

Update the testbench so it runs the same checks for WIDTH = 4 and WIDTH = 8. Explain why the terminal-count value is 2**WIDTH - 1 and why very large widths need careful integer range handling in test code.

Explained Solution

The design stores the counter in an unsigned(3 downto 0) register because unsigned has well-defined addition in numeric_std. The output is converted to std_logic_vector only at the port boundary. Reset is inside the rising_edge(clk) branch, so it is synchronous: asserting rst does not change count_r until the next rising edge.

The testbench first proves reset, then proves four increments, then disables en to prove the hold behavior. It counts up to 15, checks terminal_count, advances one more edge, and verifies wrap to zero. Because every expected value is asserted, the simulation can be used in a regression script instead of relying on manual waveform inspection.

Common Mistakes

  • Using std_logic_unsigned or std_logic_arith instead of numeric_std.
  • Expecting synchronous reset to act before a clock edge.
  • Checking count immediately at the same delta cycle as rising_edge(clk).
  • Forgetting that a 4-bit unsigned naturally wraps from 15 to 0.
  • Writing a waveform-only testbench with no assertions.
  • Making terminal_count high after wrap because it was registered at the wrong time.

Summary

A useful FPGA exercise ends with a repeatable check. This counter is small, but it practices the same workflow used for larger blocks: write synthesizable RTL, analyze dependencies in order, run a self-checking testbench, inspect waveforms only when needed, and debug from the first failing requirement.

Next: Packages, numeric_std, and Project Structure.

Further Reading

Mind Map

mindmap root((VHDL Counter Lab)) Core 4 bit register Sync reset Enable hold Natural wrap Code unsigned count_r rising_edge clk count vector port terminal_count at 15 Checks reset equals 0 en increments en low holds 15 wraps to 0 Tools ghdl analyze ghdl elaborate run with VCD GTKWave inspect Debug first assertion compile order delta cycle wait width mismatch Mistakes std_logic arithmetic async expectation waveform only wrong entity name