VHDL template in action

VHDL project template for open-source projects

written on November 18, 2020

categories: engineering

tags: VHDL, debugging, tooling

VHDL development is often associated with old and bloated (mostly proprietary) software, installations that take forever and take up a huge amount of space... what if there were a simple open-source alternative?

I do work on some VHDL projects (like this one, or this one) from time to time, so I wanted a simple, Makefile-based project template I could build upon, which would be supported at least on Linux and macOS, and which would be easy to install and setup and yet powerful and scalable. So I rolled my own VHDL open-source project template. In this blogpost I'd like to explore how it works and give a concrete example of building something with it.

It depends on GHDL and GTKWave (installation instructions for Linux and macOS are provided in the repo's README). GTKWave actually isn't strictly required, as all building/testing can be achieved with GHDL alone, but it's fun to have and it sometimes helps to visually debug an IP using a digital timing diagram.

Anyway, let's say we want to build a simple 3-input AND gate IP. We first have to clone the template repo:

$ git clone https://github.com/kokkonisd/vhdl-template/

We can then rename the folder to reflect our actual project name, and get rid of all the git-related stuff we don't need:

$ mv vhdl-template/ my-awesome-ip/
$ cd my-awesome-ip/
$ rm -rf .git/ LICENSE README.md

As you can see, the project template is laid out as follows:

$ tree
.
├── Makefile
├── simu
├── src
└── tb

3 directories, 1 file

There are three main folders: src, which contains the VHDL sources (like MyIP.vhdl), tb which contains the test benches associated with the sources (like MyIP_tb.vhdl) and finally simu, which contains the simulation files generated based on the test benches. These files can then be opened by GTKWave, but we'll get to that.

Let's start by building a simple 2-input AND gate:

src/SimpleAndGate.vhdl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
library ieee;
use ieee.std_logic_1164.all;


entity SimpleAndGate is
    port (
        A : in  std_logic;
        B : in  std_logic;
        O : out std_logic
    );
end entity;


architecture behavioral of SimpleAndGate is
begin
    O <= A and B;
end architecture;

Very simple, just has two inputs and one output (all std_logic), and the output is equal to the ANDing of the two inputs. Let's try to compile this IP by running make:

$ make
[Compiling `SimpleAndGate.vhdl` ...]
ghdl -s --workdir=build src/SimpleAndGate.vhdl
ghdl -a --workdir=build src/SimpleAndGate.vhdl

No errors! Our simple IP is working. The first command checks the syntax of the .vhdl file, and the second one analyzes it. It should now be ready to be used by other IPs. Speaking of which, let's code a 3-input AND gate IP based on the one we just implemented:

src/AwesomeTripleAndGate.vhdl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
library ieee;
use ieee.std_logic_1164.all;


entity AwesomeTripleAndGate is
    port (
        A : in  std_logic;
        B : in  std_logic;
        C : in  std_logic;
        O : out std_logic
    );
end entity;


architecture behavioral of AwesomeTripleAndGate is
    signal temp : std_logic;
begin
    AND0: entity work.SimpleAndGate(behavioral) port map (A, B, temp);
    AND1: entity work.SimpleAndGate(behavioral) port map (C, temp, O);
end architecture;

Let's also write a simple test bench we can use to verify that it's working. Note that the test bench file must have exactly the same name as the source IP file, plus "_tb". So in this case, it will be tb/AwesomeTripleAndGate_tb.vhdl:

tb/AwesomeTripleAndGate_tb.vhdl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;


entity AwesomeTripleAndGate_tb is
end entity;


architecture test of AwesomeTripleAndGate_tb is
    signal A : std_logic;
    signal B : std_logic;
    signal C : std_logic;
    signal O : std_logic;

    signal temp : std_logic_vector (2 downto 0) := "000";
begin
    T0: entity work.AwesomeTripleAndGate(behavioral) port map (A, B, C, O);

    test: process
    begin
        -- For inputs 000 through 110, output should be zero
        for i in 0 to 6 loop
            temp <= std_logic_vector(to_unsigned(i, 3));
            wait for 1 ns;

            A <= temp(0);
            B <= temp(1);
            C <= temp(2);

            wait for 1 ns;
            assert O = '0' report "Output is wrong for inputs other than 111" severity error;
        end loop;

        A <= '1';
        B <= '1';
        C <= '1';

        wait for 1 ns;
        assert O = '1' report "Output is wrong for input 111" severity error;
    end process;
end architecture;

Once again, very simple: we set up an entity to test, and then we start a "test" process, where we set the inputs of the triple AND gate to various values and assert the output, triggering an error if it's incorrect.

Let's make and see if this whole thing works:

$ make
[Compiling `AwesomeTripleAndGate.vhdl` & `AwesomeTripleAndGate_tb.vhdl` ...]
ghdl -s --workdir=build src/AwesomeTripleAndGate.vhdl tb/AwesomeTripleAndGate_tb.vhdl
src/AwesomeTripleAndGate.vhdl:18:23:error: unit "simpleandgate" not found in library "work"
src/AwesomeTripleAndGate.vhdl:19:23:error: unit "simpleandgate" not found in library "work"
make: *** [AwesomeTripleAndGate] Error 1

Whoops, that doesn't seem right. What happened? Well, I named the IPs on purpose, so that the second one would get picked up by the Makefile before the first one. That way, it tries to compile the second IP without first compiling its dependency. The simplest way to fix this is to declare the dependency in the Makefile, by replacing this:

Makefile

# Dependencies
# <my entity>: <my other entity>

with this:

Makefile

# Dependencies
AwesomeTripleAndGate: SimpleAndGate

So now that the dependencies issue is fixed, let's re-run make:

$ make
[Compiling `SimpleAndGate.vhdl` ...]
ghdl -s --workdir=build src/SimpleAndGate.vhdl
ghdl -a --workdir=build src/SimpleAndGate.vhdl
[Compiling `AwesomeTripleAndGate.vhdl` & `AwesomeTripleAndGate_tb.vhdl` ...]
ghdl -s --workdir=build src/AwesomeTripleAndGate.vhdl tb/AwesomeTripleAndGate_tb.vhdl
ghdl -a --workdir=build src/AwesomeTripleAndGate.vhdl tb/AwesomeTripleAndGate_tb.vhdl
ghdl -e --workdir=build -o build/AwesomeTripleAndGate_tb AwesomeTripleAndGate_tb
[Running simulation of `AwesomeTripleAndGate_tb` ...]
ghdl:info: simulation stopped by --stop-time @100ns
[`AwesomeTripleAndGate` PASS]

Now the SimpleAndGate gets built first, and then when it's AwesomeTripleAndGate's turn to be built, it can find the reference to SimpleAndGate and build successfully.

Just to properly test out all of the features of this Makefile, we can open the resulting simulation in GTKWave, by running:

$ make simu

This will open up GTKWave and will allow us to visually check the test bench simulation:

Visualization of the simulation of the test bench

Visualizing the simulation of the test bench in GTKWave.

And that's it! You can now build, debug and run automated tests using this lightweight VHDL project template.


< Macgrind: containerized Valgrind on macOS! Moving files between Git repos while preserving history >