Skip to main content

Automating designs for FPGAs

Introduction

The aim of this tutorial is to provide a hands-on introduction to automating FPGA design using Test Driven Development (TDD) and show you how to apply these principles to your own projects in the future.

Test driven development is when simple tests are created to test a specific part of the design, before any actual code is written. Then enough code is written to pass that test, before the next test is written.

In this tutorial, you will be using Vivado to create a full adder circuit. As simple as the full adder circuit may seem, the principles behind it are fundamental to much larger systems. This tutorial aims to provide a hands-on introduction to creating a FPGA project and setting it up in a CI environment for efficient development and testing.

You will start by building two half adders using AND and XOR gates, and then combine them with an OR gate to create the full adder circuit. Each circuit will be tested locally before being added to its own branch of a repository. The final repository will then be tested using CI software to ensure its functionality.

By breaking down the full adder circuit into smaller, manageable pieces and testing each one before integrating them, you can ensure that the final circuit will function as expected. Additionally, by using CI software to test the final repository, you can ensure that any changes made to the circuit in the future will not introduce any bugs or errors.

Moreover, by using version control and CI integration, it becomes easier for a team of developers to work on the project simultaneously without the fear of breaking the code.

The goal of this tutorial is not just to show you how to create a full adder circuit, but also to teach you how to set up a FPGA project in a way that allows for easy development, testing, and team integration, so that you can apply these principles to your own projects in the future.

Overview

The tutorial will consist of several steps to guide you through the process of creating a full adder circuit using Vivado and TDD.

  1. Defining the project specs. This step will involve outlining the requirements for the circuit and the individual components that will be used to build it.

  2. Creating a repository structure with branches for each module. This step will involve setting up a version control system and creating branches for the AND gate, XOR gate, OR gate, and full adder circuit.

  3. Adding tests to each branch to make sure the circuits perform correctly. This step will involve writing test cases to build up our suite of tests.

  4. Designing the circuits for each branch, testing locally then pushing to the CI system to test automatically. This step will involve using Vivado to design the circuits, testing them locally, and then pushing the code to the CI system for automated testing. This process will be repeated for each branch to ensure that all components of the full adder circuit are working correctly before they are integrated.

By following these steps, you will learn how to create a full adder circuit using Vivado, set up a version control system, and use a CI environment for efficient development and testing.

You can find all code used in this project from the GitHub page.

Defining the project specs

Full adder

A full adder is a digital logic circuit that is capable of performing addition operations of two binary digits, including the carry-in bit. It consists of three inputs (A, B, and Cin) and two outputs (S and Cout). The S output is the sum of the three inputs, while the Cout output is the carry-out bit.

It can be constructed using two half adders and an OR gate. The first half adder is used to calculate the sum bit and the carry-out bit between the A and B inputs. The second half adder is used to calculate the sum bit and the carry-out bit between the sum bit of the first half adder and the carry-in bit (Cin).

The OR gate is used to combine the carry-out bit from the first half adder with the carry-out bit from the second half adder. This output is the Cout, the carry-out bit from the full adder. The truth table of a full adder is:

Input AInput BInput cinOutput sumOutput cout
00000
00110
01010
01101
10010
10101
11001
11111

With this circuit, you can perform addition of two binary digits including the carry-in bit, providing the sum and the carry-out bit. This circuit is widely used in digital circuits and it's used in many applications such as arithmetic circuits, memory, and control circuits.

plot

AND

An AND gate is a digital logic gate that performs a logical conjunction operation. It has two or more inputs and one output. The output of the AND gate is high 1 only if all of its inputs are high 1. If any of the inputs are low 0, the output will be low 0. It can be represented as a Boolean function as: Y = A.B where Y is the output and A, B are the inputs.

It is also represented as a truth table where it shows all possible input combinations and the corresponding outputs:

ABY
000
010
100
111

XOR

An XOR (Exclusive OR) gate is a digital logic gate that performs a logical exclusive disjunction operation. It has two or more inputs and one output. The output of the XOR gate is high 1 if any of its inputs are high 1, but not both. If all of the inputs are the same, the output will be low 0. It can be represented as a Boolean function as: Y = A ⊕ B

It can also be represented as a truth table:

ABY
000
011
101
110

The XOR gate is often used to compare two binary numbers and produce a signal indicating whether they are different or not.

OR

An OR gate is a digital logic gate that performs a logical disjunction operation. It has two or more inputs and one output. The output of the OR gate is high 1 if any of its inputs are high 1. If all of the inputs are low 0, the output will be low 0. It can be represented as a Boolean function as: Y = A + B where Y is the output and A, B are the inputs.

The truth table looks like:

ABY
000
011
101
111

The OR gate is often used to combine multiple signals and to provide a single output signal.

Half Adder

A half adder is a digital logic circuit that is capable of performing simple addition operations of two binary digits. It consists of two inputs: A and B and two outputs: S and C. The S output is the sum of the two inputs, while the C output is the carry-out bit. It can be constructed using an XOR (Exclusive OR) gate and an AND gate. The XOR gate is used to calculate the sum S bit, while the AND gate is used to calculate the carry-out C bit.

The XOR gate is connected to the A and B inputs and its output is connected to the S output. The XOR gate gives the sum bit by setting the output to 1 only when one of the inputs is 1 and the other is 0. This is the same as a normal addition but without carrying the 1 to the next column. The AND gate is also connected to the A and B inputs and its output is connected to the C output. The AND gate gives the carry-out bit by setting the output to 1 only when both inputs are 1. This is used to carry the 1 to the next column in case of normal addition. The truth table of a half adder:

ABsumcarry
0000
0110
1010
1101

With this circuit, you can perform simple addition of two binary digits without considering the carry-in bit:

plot

Create a repository structure

The folder structure that will be used consists of several branches that each represent a different stage in the development process. The main branch is the top-level branch, serving as the starting point for all other branches. The OR, XOR, AND, Half Adder, and Full Adder branches are all subsidiary branches that stem from the main branch.

Each of these branches contains its own set of files and directories, allowing for independent development and management of different parts of the project. By using branches, multiple team members can work simultaneously on different aspects of the project and changes can be easily tracked and merged back into the main branch as needed.

This structure provides a flexible and efficient way to manage the project and ensures that the project remains organized and consistent throughout its development:

plot

Making the repositories

  1. Open the project folder made in the previous tutorial.
  2. Add the following .gitignore file to the project base (you might need admin permissions for this):
#########################################################################################################
## This is an example .gitignore file for Vivado, please treat it as an example as
## it might not be complete. In addition, XAPP 1165 should be followed.
#########################################################################################################
#########
#Exclude all
#########
*
!*/
!.gitignore
###########################################################################
## VIVADO
###########################################################################
#########
#Source files:
#########
#Do NOT ignore VHDL, Verilog, block diagrams or EDIF files.
!*.vhd
!*.v
!*.bd
!*.edif
#########
#IP files
#########
#.xci: synthesis and implemented not possible - you need to return back to the previous version to generate output products
#.xci + .dcp: implementation possible but not re-synthesis
#*.xci(www.spiritconsortium.org)
!*.xci
#*.dcp(checkpoint files)
!*.dcp
!*.vds
!*.pb
#All bd comments and layout coordinates are stored within .ui
!*.ui
!*.ooc
#########
#System Generator
#########
!*.mdl
!*.slx
!*.bxml
#########
#Simulation logic analyzer
#########
!*.wcfg
!*.coe
#########
#MIG
#########
!*.prj
!*.mem
#########
#Project files
#########
#XPR + *.XML ? XPR (Files are merged into a single XPR file for 2014.1 version)
#Do NOT ignore *.xpr files
!*.xpr
#Include *.xml files for 2013.4 or earlier version
!*.xml
#########
#Constraint files
#########
#Do NOT ignore *.xdc files
!*.xdc
#########
#TCL - files
#########
!*.tcl
#########
#Journal - files
#########
!*.jou
#########
#Reports
#########
!*.rpt
!*.txt
!*.vdi
#########
#C-files
#########
!*.c
!*.h
!*.elf
!*.bmm
!*.xmp


  1. Ensure that Git is installed with the following command:
sudo apt-get install git
  1. If you have not already done so, you can set up a GitHub account for free from their website.
  2. Make a new git repo called vhdltutorial in GitHub then follow the commands given to initialize a new repository in the folder where you did the previous tutorial:
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/[username]/vhdltutorial.git
git push -u origin main
  1. Run the following command to create the OR branch:
git checkout -b OR 
  1. Run the following command to create the XOR branch:
git checkout -b XOR 
  1. Run the following command to create the AND branch:
git checkout -b AND 
  1. Run the following command to create the Half Adder branch:
git checkout -b half_adder 
  1. Run the following command to create the Full Adder branch:
git checkout -b full_adder 
  1. Run the following command to see all the branches in the repository:
git branch
  1. Use the following command to switch between branches:
git checkout [branch_name]

Adding tests

At the beginning of the tutorial, you established the expected behavior of our code through a set of specifications or requirements. This defines the desired outcome or functionality of the code and serves as the guide for the development process. Now, you are adding code to test that behavior. By writing tests that check for the specified behavior, you can verify that our code is functioning as expected. This helps to catch any errors or bugs early in the development process and ensures that our code meets the established standards. Additionally, it allows you to make changes to the code with confidence, as you can run the tests to see if any changes have broken any existing functionality. In this way, testing helps to maintain the integrity and quality of the code, while completing the project.

AND module tests

  1. In the terminal:
git checkout AND
  1. Delete the current project files in the branch (from the previous tutorial).
  2. Add the following file as a test bench file (simulation source):
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity AND_GATE_TB is
end AND_GATE_TB;

architecture Behavioral of AND_GATE_TB is
component AND_GATE is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
Y : out STD_LOGIC);
end component;
signal A, B, Y : STD_LOGIC;
begin
DUT : AND_GATE
Port map (A => A, B => B, Y => Y);
process begin
-- Test case 1: A = 0, B = 0
A <= '0';
B <= '0';
wait for 10 ns;
assert Y = '0' report "Test case 1 failed" severity failure;

-- Test case 2: A = 0, B = 1
A <= '0';
B <= '1';
wait for 10 ns;
assert Y = '0' report "Test case 2 failed" severity failure;

-- Test case 3: A = 1, B = 0
A <= '1';
B <= '0';
wait for 10 ns;
assert Y = '0' report "Test case 3 failed" severity failure;

-- Test case 4: A = 1, B = 1
A <= '1';
B <= '1';
wait for 10 ns;
assert Y = '1' report "Test case 4 failed" severity failure;
end process;
end Behavioral;
  1. Push the test bench to the appropriate branch with:
git add .
git commit -m "First commit to AND"
git push origin AND

XOR module tests

  1. In the terminal:
git checkout XOR
  1. Delete the current project files in the branch (from the previous tutorial).
  2. Add the following file as a test bench file:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity XOR_GATE_TB is
end XOR_GATE_TB;

architecture Behavioral of XOR_GATE_TB is
signal A, B, Y : std_logic;
begin
DUT: entity work.XOR_GATE(Behavioral)
port map (A => A, B => B, Y => Y);

-- Stimulus process
stim_proc: process
begin
A <= '0'; B <= '0'; wait for 10 ns;
assert (Y = '0') report "Error: A=0, B=0, Y should be 0" severity error;

A <= '0'; B <= '1'; wait for 10 ns;
assert (Y = '1') report "Error: A=0, B=1, Y should be 1" severity error;

A <= '1'; B <= '0'; wait for 10 ns;
assert (Y = '1') report "Error: A=1, B=0, Y should be 1" severity error;

A <= '1'; B <= '1'; wait for 10 ns;
assert (Y = '0') report "Error: A=1, B=1, Y should be 0" severity error;

wait;
end process;
end Behavioral;
  1. Push the test bench to the appropriate branch with:
git add .
git commit -m "First commit to XOR"
git push

OR module tests

  1. In the terminal:
git checkout OR
  1. Delete the current project files in the branch (from the previous tutorial).
  2. Add the following file as a test bench file:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity OR_GATE_TB is
end OR_GATE_TB;

architecture Behavioral of OR_GATE_TB is
signal A, B, Y : std_logic;
begin
DUT: entity work.OR_GATE(Behavioral)
port map (
A => A,
B => B,
Y => Y
);
process begin
-- Test case 1: A = 0, B = 0
A <= '0';
B <= '0';
wait for 10 ns;
assert Y = '0' report "Test case 1 failed" severity failure;

-- Test case 2: A = 0, B = 1
A <= '0';
B <= '1';
wait for 10 ns;
assert Y = '1' report "Test case 2 failed" severity failure;

-- Test case 3: A = 1, B = 0
A <= '1';
B <= '0';
wait for 10 ns;
assert Y = '1' report "Test case 3 failed" severity failure;

-- Test case 4: A = 1, B = 1
A <= '1';
B <= '1';
wait for 10 ns;
assert Y = '1' report "Test case 4 failed" severity failure;
end process;
end Behavioral;

Push the test bench to the appropriate branch with:

git add .
git commit -m "First commit to OR"
git push

Half Adder module tests

  1. In the terminal:
git checkout half_adder
  1. Delete the current project files in the branch (from the previous tutorial).
  2. Add the following file as a test bench file :
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- Import the half adder and AND gate modules
use work.half_adder;

entity half_adder_tb is
end half_adder_tb;

architecture Behavioral of half_adder_tb is
signal a, b, sum, carry : std_logic;
begin
-- Instantiate the half adder
uut : entity work.half_adder(Behavioral)
port map (
a => a,
b => b,
sum => sum,
carry => carry
);
process
begin
-- Test case 1
a <= '0';
b <= '0';
wait for 10 ns;
assert sum = '0' and carry = '0' report "Test case 1 failed" severity failure;

-- Test case 2
a <= '0';
b <= '1';
wait for 10 ns;
assert sum = '1' and carry = '0' report "Test case 2 failed" severity failure;

-- Test case 3
a <= '1';
b <= '0';
wait for 10 ns;
assert sum = '1' and carry = '0' report "Test case 3 failed" severity failure;

-- Test case 4
a <= '1';
b <= '1';
wait for 10 ns;
assert sum = '0' and carry = '1' report "Test case 4 failed" severity failure;
end process;
end Behavioral;

Push the test bench to the appropriate branch with:

git add .
git commit -m "First commit to half adder"
git push

Full adder module tests

  1. In the terminal:
git checkout full_adder
  1. Delete the current project files in the branch (from the previous tutorial).
  2. Add the following file as a test bench file:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

-- Full Adder Test Bench
entity full_adder_tb is
end full_adder_tb;

architecture behavior of full_adder_tb is
-- Inputs
signal a, b, cin : std_logic;
-- Outputs
signal sum, cout : std_logic;

begin
-- Instantiate the Full Adder
uut: entity work.full_adder(behavior)
port map (
a => a,
b => b,
cin => cin,
sum => sum,
cout => cout
);
begin process
-- Test Case 1
a <= '0';
b <= '0';
cin <= '0';
wait for 10 ns;
assert sum = '0' and cout = '0' report "Test Case 1 Failed" severity error;

-- Test Case 2
a <= '0';
b <= '1';
cin <= '1';
wait for 10 ns;
assert sum = '0' and cout = '1' report "Test Case 2 Failed" severity error;

-- Test Case 3
a <= '1';
b <= '0';
cin <= '1';
wait for 10 ns;
assert sum = '0' and cout = '1' report "Test Case 3 Failed" severity error;

-- Test Case 4
a <= '1';
b <= '1';
cin <= '1';
wait for 10 ns;
assert sum = '1' and cout = '1' report "Test Case 4 Failed" severity error;

-- Test Case 5
a <= '1';
b <= '1';
cin <= '0';
wait for 10 ns;
assert sum = '0' and cout = '1' report "Test Case 5 Failed" severity error;

-- Stop the Simulation
wait;
end process;
end;

Push the test bench to the appropriate branch with:

git add .
git commit -m "First commit to full adder"
git push

Designing the circuits for each branch

Now that you have established the test benches, you can start the development of the modules themselves.

AND circuit

  1. In the terminal:
git checkout AND
  1. Add the following file as a logic file:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity AND_GATE is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
Y : out STD_LOGIC);
end AND_GATE;

architecture Behavioral of AND_GATE is
begin
Y <= A and B;
end Behavioral;
  1. Test the file in your local version of Vivado.
  1. You will now enable CI testing. To do this you need to add a yaml file, tcl file and a folder to the system.

  2. Make a file in the root of the project called sim.tcl and add the following. Change project_1 to your Vivado project.

open_project ./project_1.xpr
set_property -name {xsim.simulate.log_all_signals} -value {true} -objects [get_filesets sim_1]
launch_simulation

Making the config file

  1. In the project directory you need to make a folder called `.bbx then make a file called .bbx/config.yaml and add the following, before pushing the files to the AND branch. Change project_1 to your Vivado project name.
runners:
local-runner:
image: work1-virtualbox:5000/ubuntu-vivado-2020-1:latest

jobs:
build_run_sim:
resource_spec: small
runner: local-runner
type:
build: hardware
current_working_directory: /tools/Xilinx/Vivado/2020.1/workspace/project_1
output:
artifact:
- ./project_1.xpr
- ./project_1.cache
- ./project_1.hw
- ./project_1.ip_user_files
- ./project_1.sim
- ./project_1.srcs
steps:
- run:
name: Run tcl
command: |
source /tools/Xilinx/Vivado/2020.1/settings64.sh
vivado -mode tcl -source sim.tcl
type: miscellaneous
workflows:
complete-build-test:
jobs:
- build_run_sim

  1. Add the project to BeetleboxCI. You can find out more information about how to add GitHub repositories here. You will see errors because not all of the branches have yaml files yet. The AND branch does have a yaml so when you click on the project that branch should appear and be able to run.

  2. Click the run button. Ensure that test is passing correctly.

XOR circuit

  1. In the terminal:
git checkout XOR
  1. Add the following file as a logic file:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity XOR_GATE is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
Y : out STD_LOGIC);
end XOR_GATE;

architecture Behavioral of XOR_GATE is
begin
Y <= A xor B;
end Behavioral;
  1. Test the file in your local version of Vivado.

  2. Run the following code to pull the test and files.

git checkout AND ./.bbx/config.yaml  sim.tcl
git add .bbx/config.yaml sim.tcl
git commit -m "Pipelines for OR test"
git push

Head over to BeetleboxCI and refresh the project and the XOR branch should be there to test.

OR circuit

  1. In the terminal:
git checkout OR
git checkout AND ./.bbx/config.yaml sim.tcl
  1. Add the following file as a logic file:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity OR_GATE is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
Y : out STD_LOGIC);
end OR_GATE;

architecture Behavioral of OR_GATE is
begin
Y <= A or B;
end Behavioral;
  1. Test the file in your local version of Vivado.

  2. Run the following code to pull the test and files.

git checkout AND ./.bbx/config.yaml  sim.tcl
git add .bbx/config.yaml sim.tcl
git commit -m "ci stuff"
git push

Head over to BeetleboxCI and refresh the project and the OR branch should be there to test.

Half Adder circuit

  1. In the terminal:
git checkout halfadder
  1. Now you need to merge the other repos in with the following command

git checkout XOR <XOR vhd file location and name>
git checkout AND <AND vhd file location and name> ./.bbx/config.yaml sim.tcl

For example:


git checkout AND vhdltutorial.srcs/sources_1/new/and_gate.vhd ./.bbx/config.yaml sim.tcl

  1. Add the following file as a logic file
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;



entity half_adder is
Port ( a : in STD_LOGIC;
b : in STD_LOGIC;
sum : out STD_LOGIC;
carry : out STD_LOGIC);
end half_adder;



architecture Behavioral of half_adder is

begin
--instantiate and do port map for the first half adder.
XOR_GATE : entity work.XOR_GATE port map(a,b,sum);
--instantiate and do port map for the second half adder.
AND_GATE : entity work.AND_GATE port map(a,b,carry);

end;
  1. Run the following code to pull the test and files.
git add .bbx/config.yaml sim.tcl
git commit -m "ci stuff"
git push

Head over to BeetleboxCI and refresh the project and the OR branch should be there to test.

Full adder circuit

  1. In the terminal:
git checkout full_adder
  1. Now you need to merge the other repos in with the following command:
git checkout halfadder <halfadder XOR AND vhd files location and name> ./.bbx/config.yaml  sim.tcl

git checkout or <or vhd file location and name>
  1. Add the following file as a logic file :
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity FULL_ADDER is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
Cin : in STD_LOGIC;
Sum : out STD_LOGIC;
Cout : out STD_LOGIC);
end FULL_ADDER;

architecture Behavioral of FULL_ADDER is
signal HA1_Sum, HA1_Cout : std_logic;
signal HA2_Sum, HA2_Cout : std_logic;
begin
-- Instantiate the two half adder sub-modules
HA1 : entity work.HALF_ADDER(Behavioral)
port map (A => A, B => B, Sum => HA1_Sum, Cout => HA1_Cout);

HA2 : entity work.HALF_ADDER(Behavioral)
port map (A => HA1_Sum, B => Cin, Sum => HA2_Sum, Cout => HA2_Cout);

-- Use the outputs of the two half adders to calculate the final outputs
Sum <= HA2_Sum;
Cout <= HA1_Cout OR HA2_Cout;
end Behavioral;
  1. Run the following code to pull the test and files:
git add .bbx/config.yaml sim.tcl
git commit -m "ci stuff"
git push

Head over to BeetleboxCI and refresh the project and the OR branch should be there to test.

Conclusion

You have stepped through how to set up a basic Test Driven Development flow for FPGAs. You used the simple example of a full adder which was broken into all of its gate level logic. Each module then had a test built before, you then made the code for it.

By placing these tests within different branches of the git repository, BeetleboxCI cant hen be used to trigger each test individually before running the final complete test.

What’s coming next?

In the next tutorial, you will be looking more in depth about the synthesis and simulation process. You will learn hwo to automate important steps, such as post-synthesis simulation.