NEORV32 is an Open Source RISC-V SoC written in VHDL. The CPU neither the smallest nor the fastest RISC-V core, but its main benefit is the complete ecosystem: Not only does it provide peripherals such as I2C, SPI, GPIO and more. It also ships ready-to-use drivers and extensive developer and user documentation. And if bare-metal is not enough, it also offers a FreeRTOS port. Let’s see how we can port NEORV32 to the Tang Nano FPGA.
Basic Setup
We’re going to reuse the setup introduced in the first article of this series. To get started, just open the distrobox container again:
distrobox enter fpga
You can find my final design here. Let’s clone it and then discuss some of the more interesting aspects:
git clone --recursive https://github.com/jpf91/neorv32-tang20k.git
Porting NEORV32
When using the NEORV32 in a project, the first question is whether to copy the sources and what directory structure to use. NEORV32 gets frequent updates and drivers and hardware RTL in different versions are often not compatible. It is therefore important to know which NEORV32 git revision is being used.
Adding NEORV32 Sources
The easiest way to keep track of this is to check out NEORV32 as a git submodule. You can find it in the lib folder, and if you want to recreate the setup manually, this is how to:
# Initialize new repository
git init
# Setup NEORV32 submodule
mkdir lib
git submodule add https://github.com/stnolting/neorv32.git lib/neorv32
# Checkout a tagged release
cd lib/neorv32
git checkout v1.10.7
I use the original NEORV32 repository, not a custom fork as this makes updating easier. If I need to modify RTL files (such as the memory modules or the bootloader ROM), I copy them to the src/hdl/neorv32 folder instead.
The Top-Level Module
When using NEORV32 you’ll usually still have to write a custom top module, as I did in src/hdl/top.vhd
.
First include the NEORV32 and IEEE libraries:
library ieee;
use ieee.numeric_std.all;
use ieee.std_logic_1164.all;
library neorv32;
use neorv32.neorv32_package.all;
Some minimal port declarations look like this:
entity top is
port (
sys_clk: in std_logic;
key1, key2: in std_logic;
sys_led: out std_logic_vector(5 downto 0);
sys_tx: out std_logic;
sys_rx: in std_logic;
sys_tms: in std_logic;
sys_tck: in std_logic;
sys_tdi: in std_logic;
sys_tdo: out std_logic;
mspi_do: in std_logic;
mspi_di: out std_logic;
mspi_cs: out std_logic;
mspi_clk: out std_logic
);
end;
This provides a clock signal, two buttons, UART lines, SPI for the flash and JTAG.
Clock Debug LED
At least for early design stages, I like to always see if the clocks are working properly. For this, I use a simple LEDBlink module to divide the clock and show it on an LED:
blink: entity work.LEDBlink
generic map (
TOGGLE_CYCLES => 50000000
)
port map (
clk => clk,
arstn => '1',
led => sys_led(2)
);
Reset Button Debouncer
I use one of the buttons on the board as a reset signal for the FPGA firmware. To ensure that the button is not bouncing, I’m introducing a small debouncer module:
-- Low active reset
arstn_btn <= not key1;
rst: entity work.BTNReset
generic map (
DEBOUNCE_CYCLES => 15
)
port map (
clk => clk,
arstn_i => arstn_btn,
rstn_o => rstn
);
sys_led(0) <= rstn;
LED 0 shows if the reset signal is asserted.
As both LED outputs and rstn
are active low, we don’t have to invert the signal.
PLL Module
Next, we will introduce a module to generate a 108 MHz clock from the 27 MHz input:
-- PLL: Generate 108 MHz from 27 MHz input
pll: SysPLL
port map (
sys_clk => sys_clk,
enable => '1',
clk => clk,
locked => pll_locked
);
sys_led(1) <= not pll_locked;
We also report the PLL lock on a (low active) LED.
SysPLL
is currently written in Verilog, as I’m not sure whether passing generics from VHDL to macros is supported.
Configuring the NEORV32
NEORV32 is configured using generics. The comments should explain what the individual parameters are doing. This is a quite minimal configuration and more parameters can be found in the neorv32_top file.
neorv: neorv32_top
generic map (
-- Clocking --
CLOCK_FREQUENCY => 108000000, -- clock frequency of clk_i in Hz
-- Boot Configuration --
BOOT_MODE_SELECT => 0, -- boot via internal bootloader
-- Enable JTAG / Debugger --
OCD_EN => true,
-- RISC-V CPU Extensions --
RISCV_ISA_C => true, -- implement compressed extension?
RISCV_ISA_M => true, -- implement mul/div extension?
RISCV_ISA_Zicntr => true, -- implement base counters?
-- Internal Instruction memory --
MEM_INT_IMEM_EN => true, -- implement processor-internal instruction memory
MEM_INT_IMEM_SIZE => 24288, -- size of processor-internal instruction memory in bytes
-- Internal Data memory --
MEM_INT_DMEM_EN => true, -- implement processor-internal data memory
MEM_INT_DMEM_SIZE => 16192, -- size of processor-internal data memory in bytes
-- Processor peripherals --
IO_GPIO_NUM => 4, -- number of GPIO input/output pairs (0..64)
IO_MTIME_EN => true, -- implement machine system timer (MTIME)?
IO_UART0_EN => true, -- implement primary universal asynchronous receiver/transmitter (UART0)?
IO_SPI_EN => true, -- Used for flash bootloader
IO_GPTMR_EN => true,
IO_DMA_EN => true
)
Connecting NEORV32
The NEORV32 port map is quite straightforward, just see the source file for details.
Pin Constraints
Pins are configured in src/constraints/top.cst:
IO_LOC "sys_clk" 4;
IO_PORT "sys_clk" PULL_MODE=UP;
IO_LOC "key1" 88;
IO_PORT "key1" PULL_MODE=DOWN;
IO_LOC "key2" 87;
IO_PORT "key2" PULL_MODE=DOWN;
IO_LOC "sys_tx" 69;
IO_LOC "sys_rx" 70;
IO_LOC "sys_led[0]" 15;
IO_LOC "sys_led[1]" 16;
IO_LOC "sys_led[2]" 17;
IO_LOC "sys_led[3]" 18;
IO_LOC "sys_led[4]" 19;
IO_LOC "sys_led[5]" 20;
IO_LOC "sys_tms" 5;
IO_LOC "sys_tck" 6;
IO_LOC "sys_tdi" 7;
IO_LOC "sys_tdo" 8;
IO_LOC "mspi_do" 62;
IO_LOC "mspi_di" 61;
IO_LOC "mspi_cs" 60;
IO_LOC "mspi_clk" 59;
Clock Constraints
As one last thing, we also need the clock constraints in src/constraints/top.py:
# The external clock
ctx.addClock("sys_clk", 27)
# The processor clock
ctx.addClock("clk", 108)
Pin Summary
Summarizing everything, we use the following LEDs and Buttons:
Hardware | Description |
---|---|
LED 0 | System reset status. On if the system is in reset. |
LED 1 | PLL locked signal. On if the PLL is locked. |
LED 2 | Blinks if processor clock is toggling. |
LED 3-5 | LEDs used by software application. Bootloader blinks LED3 on boot. |
Switch S1 | FPGA firmware reset button. |
Switch S2 | Connected to GPIO0 input. To be used by software application. |
Building the Firmware
The firmware FPGA is built using the commands explained in the first article of this series. To make things more convenient, I have introduced a Makefile for these commands. To compile the firmware, just do:
make clean
make bitstream
Flashing the FPGA firmware can also be done using the Makefile:
# To SRAM, fast
make upload
# To SPI Flash, persistent
make upload-flash
Installing the Software Toolchain
To compile the application, we can use any bare metal RISC-V toolchain. The xPack GNU RISC-V Embedded GCC is up-to-date and easy to install, so let’s install that one into our distrobox container:
curl --progress-bar -L "https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v14.2.0-3/xpack-riscv-none-elf-gcc-14.2.0-3-linux-x64.tar.gz" -o gcc.tgz
tar -xf gcc.tgz
rm gcc.tgz
sudo mv xpack-riscv-none-elf-gcc-14.2.0-3 /opt/
Note that the NEORV32 build system also needs a gcc
for the host system.
This is not installed by default in our minimal container, so let’s do that now:
sudo dnf install -y gcc
As in the previous article, let’s also set up a profile snippet.
With this, whenever we execute distrobox enter
, the tools will be available in PATH:
sudo sh -c 'echo "export PATH=$PATH:/opt/xpack-riscv-none-elf-gcc-14.2.0-3/bin/" > /etc/profile.d/z_riscv_xpack.sh'
Then exit and enter again to make the tools available:
exit
distrobox enter fpga
# Now test GCC:
riscv-none-elf-gcc -v
Using built-in specs.
COLLECT_GCC=riscv-none-elf-gcc
COLLECT_LTO_WRAPPER=/opt/xpack-riscv-none-elf-gcc-14.2.0-3/bin/../libexec/gcc/riscv-none-elf/14.2.0/lto-wrapper
Target: riscv-none-elf
...
Building the Hello World Application
Building software examples for NEORV32 is quite simple once you have the toolchain installed.
Always make sure to use the same NEORV32 commit for the software as for the hardware RTL. As we cloned the whole repo as a submodule, we can just build in that folder and things will be fine.
cd lib/neorv32/sw/example/hello_world/
make clean_all
make exe
Memory utilization:
text data bss dec hex filename
5516 0 116 5632 1600 main.elf
Compiling image generator...
Generating neorv32_exe.bin
Executable size in bytes:
5528
Flashing the Application
As we’re using the bootloader, we can flash the firmware using the UART connection. First connect to picocom as shown previously:
The precompiled NEORV32 bootloader uses a baudrate of 19200.
picocom -b 19200 /dev/ttyUSB1
After this, press SW1
to initiate a firmware reset.
You should see this output:
<< NEORV32 Bootloader >>
BLDV: Oct 31 2024
HWV: 0x01100700
CLK: 0x066ff300
MISA: 0x40801104
XISA: 0x00000c83
SOC: 0x1007c01d
IMEM: 0x00008000
DMEM: 0x00004000
Autoboot in 10s. Press any key to abort.
Press any key to enter the menu:
Aborted.
Available CMDs:
h: Help
r: Restart
u: Upload
s: Store to flash
l: Load from flash
x: Boot from flash (XIP)
e: Execute
Then press u to start the upload:
CMD:> u
Awaiting neorv32_exe.bin...
In another terminal window, stream the compiled neorv32_exe.bin
file to the UART:
cat neorv32_exe.bin > /dev/ttyUSB1
Go back to the picocom
terminal.
You should see the application upload having succeeded:
Awaiting neorv32_exe.bin... OK
CMD:>
You could now execute the application by pressing e
.
However, let’s first store it to flash using s
:
CMD:> s
Write 0x0000158c bytes to SPI flash @ 0x00400000? (y/n) y
Flashing... OK
Running the Software
Now let’s execute the application by pressing e
:
CMD:> e
Booting from 0x00000000...
# ### ## ## ##
## ## ######### ######## ####### ## ## ######## #### #### ## ############## #
#### ## ## ## ## ## ## ## ## ## ## ## ## ### ### ####
## ## ## # ## ## ## # ## ## ## ## ## ## ###### #
## ## ## ######### ## ## ######### ## ## ##### ## ## ### ###### ####
## ## ## # ## ## ## # # ## ## ## # ## ## ###### #
## #### ## ## ## ## ## ## ### # ## ## ## ### ####
## ## ######### ######## ## # ## ## ######## #### ##### ## ############## #
## ## ## ##
Hello world! :)
As we programmed it to the SPI flash, the bootloader will also boot it automatically, if we don’t press any key for 10 seconds after reset.
FIXME: For some reason booting from SPI is currently broken in my test setup.