Ada on FPGAs with PicoRV32
by Fabien Chouteau –
When I bought the TinyFPGA-BX board, I thought it would be an opportunity to play a little bit with FPGA, learn some Verilog or VHDL. But when I discovered that it was possible to have a RISC-V CPU on it, I knew I had to run Ada code on it.
The RISC-V CPU in question is the PicoRV32 from Clifford Wolf. It is written in Verilog and implements the RISC-V 32bits instruction set (IMC extensions). In this blog post I will explain how I added support for this CPU and made an example project.
Compiler and run-time
More than a year ago I wrote a blog post about building an experimental Ada compiler and running code the first RISC-V micro-controller, the HiFive1. Since then, we released an official support of RISC-V in the Pro and Community editions of GNAT so you don’t even have to build the compiler anymore.
For the run-time, we will start from the existing HiFive1 run-time and change a few things to match the specs of the PicoRV32. As you will see it’s very easy.
Compared to the HiFive1, the PicoRV32 run-time will have
A different memory map (RAM and flash)
A different text IO driver (UART)
Different instruction set extensions
Memory map
This step is very simple, we just use linker script syntax to declare the two memory areas of the TinyFPGA-BX chip (ICE40):
MEMORY
{
flash (rxai!w) : ORIGIN = 0x00050000, LENGTH = 0x100000
ram (wxa!ri) : ORIGIN = 0x00000000, LENGTH = 0x002000
}
Text IO driver
Again this is quite simple, the UART peripheral provided with PicoRV32 only has two registers. We first declare them at their respective addresses:
UART_CLKDIV : Unsigned_32
with Volatile, Address => System'To_Address (16#02000004#);
UART_Data : Unsigned_32
with Volatile, Address => System'To_Address (16#02000008#);
Then the code just initializes the peripheral by setting the clock divider register to get a 115200 baud rate, and sends characters to the data register:
procedure Initialize is
begin
UART_CLKDIV := 16_000_000 / 115200;
Initialized := True;
end Initialize;
procedure Put (C : Character) is
begin
UART_Data := Unsigned_32 (Character'Pos (C));
end Put;
Run-time build script
The last modification is to the Python scripts that create the run-times. To create a new run-time, we add a class that defines different properties like compiler switches or the list of files to include:
class PicoRV32(RiscV32):
@property
def name(self):
return 'picorv32'
@property
def compiler_switches(self):
# The required compiler switches
return ['-march=rv32imc', '-mabi=ilp32']
@property
def has_small_memory(self):
return True
@property
def loaders(self):
return ['ROM']
def __init__(self):
super(PicoRV32, self).__init__()
# Use the same base linker script as the HiFive1
self.add_linker_script('riscv/sifive/hifive1/common-ROM.ld',
loader='ROM')
self.add_linker_script('riscv/picorv32/memory-map.ld',
loader='ROM')
# Use the same startup code as the HiFive1
self.add_sources('crt0', ['riscv/sifive/fe310/start-rom.S',
'riscv/sifive/fe310/s-macres.adb',
'riscv/picorv32/s-textio.adb'])
Building the run-time
Once all the changes are made, it is time to build the run-time.
First download and install the Community edition of the RISC-V32 compiler from adacore.com/download (Linux64 host only).
Then run the script inside the bb-runtime repository to create the run-time:
$ git clone https://github.com/AdaCore/bb-runtimes
$ cd bb-runtimes
$ ./build_rts.py --bsps-only --output=temp picorv32
Compile the generated run-time:
$ gprbuild -P temp/BSPs/zfp_picorv32.gpr
And install:
$ gprinstall -p -f -P temp/BSPs/zfp_picorv32.gpr
This is it for the run-time. For a complete view of the changes, you can have a look at the commit on GitHub: here.
Example project
Now that we can compile Ada code for the PicoRV32, let’s work on an example project. I wanted to include a custom Verilog module, otherwise what’s the point of using an FPGA, right? So I made a peripheral that controls WS2812 RGB LEDs, also known as Neopixels.
I won’t explain the details of this module, I would just say that digital logic is difficult for the software engineer that I am :)
The hardware/software interface is a memory mapped register that once written to, sends a WS2812 data frame. To control a strip of multiple LEDs, the software just has to write to this register multiple times in a loop.
The example software is relatively simple, the Neopixel driver package is generic so that it can handle different lengths of LED strips. The address of the memory mapped register is also a parameter of the generic, so it is possible to have multiple peripherals controlling different LED strips. The memory mapped register is defined using Ada’s representation clauses and Address attribute:
type Pixel is record
B, R, G : Unsigned_8;
end record
with Size => 32, Volatile_Full_Access;
for Pixel use record
B at 0 range 0 .. 7;
R at 0 range 8 .. 15;
G at 0 range 16 .. 23;
end record;
Data_Register : Pixel with Address => Peripheral_Base_Addr;
Then an HSV to RGB conversion function is used to implement different animations on the LED strip, like candle simulation or rainbow effect. And finally there are button inputs to select the animation and the intensity of the light.
Both hardware and software sources can be found in this repository. I recommend to follow the TinyFPGA-BX User Guide first to get familiar with the board and how the bootloader works.
Feeling inspired and want to start Making with Ada? We have the perfect challenge for you!
The Make with Ada competition, hosted by AdaCore, calls on embedded developers across the globe to build cool embedded applications using the Ada and SPARK programming languages and offers over €8000 in total prizes.