Running a RISC-V core on an IcoBoard


Introduction

The RISC-V is an exciting Free and Open Instruction Set architecture originally designed by researchers at the University of California, Berkely. Clifford Wolf has implemented (in verilog), a size-optimized version of the RISC-V architecture called the PicoRV32. It is possible to implement a PicoRV32 processor using the Lattice ICE40 FPGA present on the IcoBoard; all the stages involved in the synthesis of this processor are done using the amazing IceStorm suite of tools! So we now have an open processor implemented completely using open tools!

If this is your first time working with the IcoBoard and the IceStorm suite of tools which enables a 100% Free Software flow for FPGA programming, it may be good to read this post to get some general ideas.

Setting up the development PC/Laptop

You need to set up the FPGA bitstream synthesis, programming and related tools by installing IceStorm tools, Arachne-PNR and Yosys following the instructions given here.

These tools can also be set up on the Raspberry Pi (making the Pi a stand-alone platform for FPGA development), but as some of them run slowly on the Pi (for larger designs), it is recommended that you install them on the development PC/laptop.

RISC-V processors can be programmed using the GNU toolchain; you need to install it next:

git clone git@github.com:cliffordwolf/picorv32.git
cd picorv32
make download-tools
make  build-tools

This process will take some time to complete as it will be installing a few different variants of the toolchain. The final executables will be under /opt/riscv32i, /opt/riscv32ic, /opt/riscv32im, /opt/riscv32imc. In-between the installation process, you will be asked to provide superuser privilege (to write to /opt).

[Note: Most of the information above has been taken from here]

The next step is to clone the icotools repository. This repository contains the source code of the PicoRV32 processor (and associated peripherals) besides a few other things.

Configuring the PC/Laptop for connecting with the Raspberry Pi

The process of uploading the bitstream which describes the picorv32 processor, and also the machine code which is to run on the picorv32 processor, is done using a tool called icoprog. This is the only tool which we will run on the Raspberry Pi. A script running on the PC/Laptop will non-interactively connect to the Raspberry Pi (using ssh) - this script expects the Raspberry Pi to be accessible using the name raspi; so we need to find out the IP address of our Raspberry Pi (can be done easily using nmap) and create an entry in our /etc/hosts file; here is what I have on my machine:

192.168.0.102 raspi

The script will connect to the user pi, and expects the connection to proceed without a password being asked. As this may require configuring ssh keys, an easier way to do it would be to use sshpass (you may need to install it). If the user pi has password xyz, make sure you are able to log in like this:

sshpass -pxyz ssh pi@raspi

You can then make a small change to the file icotools/icosoc/icosoc.py; change the line:

icosoc_mk["10-top"].append("SSH_RASPI ?= ssh pi@raspi")

to

icosoc_mk["10-top"].append("SSH_RASPI ?= sshpass -pxyz ssh pi@raspi")

Configuring the Raspberry Pi

We need to install a tool called icoprog on the Raspberry Pi. The source code of this tool is available as part of the icotools repository.

cd icotools/icoprog
make icoprog
make install

LED blinker running on the PicoRV32 processor!

The icotools/icosoc/examples directory contains C programs which can be executed on the PicoRV32 processor.

I have modified two of the programs and made them a bit simpler; you can download the code from here. You should untar the file under the icotools/icosoc/examples folder. You will get two new folders: blink and simple-timer.

Let’s try out the LED blinker first!

Here is our icotools/icosoc/examples/blink/main.c:

#include <stdio.h>
#include <stdint.h>
#include "icosoc.h"

void delay()
{
    for(int i = 0; i < 100000; i++)
        asm volatile("");
}

int main()
{
    while(1) {
        icosoc_leds(1);
        delay();
        icosoc_leds(0);
        delay();
    }
}

There are a few things to understand before we run the code.

First, we don’t really have a PicoRV32 processor with us - the IcoBoard contains only a Lattice ICE40 FPGA. This FPGA has to be morphed into a processor by wiring the Logic blocks contained within it; this is a very complex process and the journey starts with a precise description of the processor’s functionality in Verilog; this is the verilog file which describes the processor: icotools/icosoc/common/picorv32.v.

Besides the processor core, we need to implement peripherals like GPIO ports, UART, SPI etc. The Verilog descriptions of these peripherals can be seen under directories named icotools/icosoc/mod_rs232, icotools/icosoc/mod_gpio etc.

When building an application program for the first time (say the example given above),the build scripts will first generate a bitstream that will configure the FPGA to act as a PicoRV32 processor (with the required peripherals). The bitstream will be transferred to the FPGA by icoprog running on the Raspberry Pi. We now have a working processor! Next, the application code will get compiled by the RISC-V GCC toolchain and it will be loaded into the “processor” by icoprog running on the Raspberry Pi; the processor will then start running the code.

Let’s look at how the above program works. The icosoc_leds function controls the 3 built-in LED’s on the IcoBoard. You can consider the 3 FPGA pins to which the LED’s are connected as forming a 3 bit write-only port. For example, the statement:

icosoc_leds(7);

will result in all three LED’s lighting up! It is now easy to see that what we have here is a simple LED blinker!

You can see the code in action by typing:

make run

This will take some time to finish when you do it for the first time because the processor and required peripherals have to be synthesized from the Verilog code and only then will the application code get compiled and flashed.

The icotools/icosoc/examples/blink folder contains a file called icosoc.cfg; this file is used by the build scripts to configure the processor in different ways; say your program needs a serial port, you can then configure the processor in such a way that it comes with a UART attached. This is something you can’t do with hard-wired processors.

Digging a bit deeper

If you read the auto-generated file icosoc.h (note: you will have to do “make run” first), you will find the definition for icosoc_leds:

static inline void icosoc_leds(uint8_t value)
{
    *(volatile uint32_t *)0x20000000 = value;
}

And if you go through the auto-generated Verilog file icotools/icosoc/examples/blink/icosoc.v, you will find a fragment of code:

 (mem_addr & 32'hF000_0000) == 32'h2000_0000: begin
        mem_ready <= 1;
        mem_rdata <= 0;
        if (mem_wstrb) begin
          if (mem_addr[23:16] == 0) begin
            if (mem_addr[7:0] == 8'h 00) 
                    {LED3, LED2, LED1} <= mem_wdata;

Even without any Verilog knowledge, we can sort of guess that a write to memory-mapped address 0x2000000 will result in the data getting directed at the three on-board LED’s!

Timer Interrupts

The directory icotools/icosoc/examples/simple-timer contains the code for a simple program which uses a timer interrupt service routine to blink an LED.

The timer counts down to zero and generates an interrupt when it hits zero. Here is the code:

#include <stdio.h>
#include <stdint.h>
#include "icosoc.h"

#define COUNT 10000000
void update_leds()
{
    static uint32_t status = 1;
    *(volatile uint32_t*)0x20000000 = status;
    status = !status;
}

void irq_handler(uint32_t irq_mask, uint32_t *regs)
{
    update_leds();
    icosoc_timer(COUNT); //re-load timer
}

int main()
{
        // register IRQ handler
        icosoc_irq(irq_handler);

        // enable IRQs
        icosoc_maskirq(0);

        // start timer (IRQ 0)
        icosoc_timer(1000000);


        while (1) { }
}

And here is what I believe is the description of the timer’s operation in the picorv32 processor core (file icotools/icosoc/common/picorv32.v):

if (ENABLE_IRQ && ENABLE_IRQ_TIMER && timer) begin
   if (timer - 1 == 0)
      next_irq_pending[irq_timer] = 1;
   timer <= timer - 1;
end

Here is a short video of the code in action:

picorv32 from Pramode C E on Vimeo.