RISC-V CPU & custom GPU on an FPGA Part 1 – Tools and Setup

During these last few months I’ve been working on creating a small system with a 32bit RISC-V CPU (conforming to rv32imf model), alongside with a custom GPU. The purpose is to have a system that I can write some small games on and use as a teaching model. I also wanted to have GDB support for debugging, hardware interrupt support in machine mode, and a task manager so I can run a series of time-sliced tasks. For the entire series, I’m going to be using Ubuntu 20.04.2 LTS as that’s the only OS that had all FPGA/RISC-V related tools working at optimal efficiency (or had any tools working at all)

I decided to make a series of tutorials on how I built this system, and this is part one of that series, so let’s begin!

Prerequisites

During the course of this project we’ll be needing some tools and devices to work with. Here’s a short list of what’s necessary and what’s been used by myself. Any compatible alternative is of course acceptable and should work as well as the versions I’ve listed:

  • Ubuntu 20.04.2 LTS
  • Python 3 (for building the riscvtool, it uses WAF as its build system which requires Python)
  • Visual Studio Code for Linux (optional)
  • Xilinx Vivado 2020.2 (higher versions didn’t have support for Spartan 7 series at the time of writing)
  • Digilent A7-100T FPGA board (any other Digilent board should work with minor changes to the Verilog project setup and perhaps by reducing the block memory sizes)
  • A custom RISC-V uploader tool (github links)
  • The RISC-V GNU toolchaing (build instructions)
  • 12bpp Digital Video PMOD from 1BitSquared (optional if you’re skipping the GPU)
  • An LCD panel or a monitor with HDMI(DVI) input
  • Micro SD Card PMOD from Digilent (optional if you only need USB/UART access)

Once upon a time, on paper…

Before jumping into the development environment, as with any project, it is essential to have a rough plan. It doesn’t have to be a perfect plan, but enough to get us started.

So far, we know what we need for the final product:

  • A RISC-V processor
  • Machine mode hardware interrupt support
  • Machine mode timer interrupt support
  • A custom GPU
  • Some form of onboard memory (BRAM suffices for now)
  • A means to upload programs (USB/UART is perfect for this)
  • Video output via HDMI
  • SDCard access
  • USB/UART access
  • GDB debugging stub

For the RISC-V CPU, the task is lengthy but easy. Since we can find all the documentation and tools necessary online, let’s get started with that first. We can always add our GPU at a later step (or skip it if you only need a CPU)

RISC-V

RISC-V is a really neat and open ISA (instruction set architecture) and has all of its ISA details listed on this very handy web site:

https://riscv.org/technical/specifications/

Initially, we will be needing the ‘Unprivileged Spec‘ from the above link to have it as our guide during development.

After we make the CPU work somehow with a few hand-made assembly instructions, we will be needing something more serious to develop with. For this purpose, I’ll be using riscv64-unknown-elf-* series of gcc 10.2.0.

Building the gcc compiler tool chain for RISC-V

Let’s go ahead and build our compiler tool chain so it’s ready when we need it.

First, we’ll need to pull the source from the git repository (make sure your home directory has enough space for several gigabytes of build artifacts)

cd ~
git clone https://github.com/riscv/riscv-gnu-toolchain.git

Next, we’ll build the necessary variants alongside the compiler tools

// For prerequisites:
sudo apt update
sudo apt upgrade
sudo apt install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev

// To build the toolchain for this project
cd riscv-gnu-toolchain/
make clean
./configure --prefix=/opt/riscv --with-multilib-generator="rv32i-ilp32--;rv32ic-ilp32--;rv32imc-ilp32--;rv32im-ilp32--;rv32imfc-ilp32f--;rv32imf-ilp32f--"
make

What the above command line does is that it produces the compiler tools alongside with the entire set of library variations we will be needing during our work. This list includes core integer instruction set, compressed instruction set, integer multiply/divide support, floating point support and most common combinations of these. For reference, the only thing we’ll be needing on the final product is the ‘rv32imf ilp32f’ pair, as we’ll only be supporting integer base set with interger multiply and floating point math. As a side note, compressed instructions were not supported at the time I started this article, but it’s always a good idea to be ready ahead of time.
You can find the full instructions at https://github.com/riscv-collab/riscv-gnu-toolchain

Grabbing the development tools and samples

To accompany this article, I’ve produced some tools that can talk to our SoC as it’s being developed using the onboard USB/UART port, to upload binaries and act as the debug port in the final build.

The tools are available on github and can be placed alongside our previous gcc folder

cd ~
git clone https://github.com/ecilasun/riscvtool.git
cd riscvtool
code .

This will open up Visual Studio Code with the riscvtool project. Now we will configure and build it by following these steps:

  • Use Ctrl+Shift+B to bring up Build Tasks and select Configure. This needs to be done one time only
  • Use Ctrl+Shift+B to bring up the same list and this time select Build, which will build the riscvtool executable

Alternatively, if you’re not using Visual Studio Code, you can do the following to build riscvtool

python waf --out='build/release' configure
python waf build -v
# and use this to 'clean' the build if you need to rebuild:
# python waf clean

This needs to be done once, and as long as you don’t change the tool, should be OK to keep around for the remainder of this series.

To build the samples and the ROM image to use during development, I use a very simple and straightforward batch file. Simply use

build.sh

at the root of the riscvtool folder and it will compile and build the ELF files and the ROM images to test with. The first ROM image (ROM_nekoichi.coe) will be the actual ROM we use to upload executables to the SoC with. The second ROM image (ROM_experimental.coe) contains experimental code used during development which didn’t make it into the actual ROM image yet.

Installing Xilinx tools

Now the fun part. Head over to Xilinx web site to download Vivado Design Suite 2020.2 version. Make sure you don’t get the 2020.3 version because that seems to lack support for any Series 7 devices. You may also want to grab any product updates while you’re at it.

Note that the installer won’t install the USB cable drivers, so you’ll need to follow this very short guide to install them. To re-cap how this works:

cd <Vivado Install>/data/xicom/cable_drivers/lin64/install_script/install_drivers/
sudo ./install_drivers
sudo adduser $USER dialout
# if you have another user that needs to use the USB port, for each one do this
# sudo adduser <username> dialout

And that concludes our project setup, and marks the end of part 1.