LEON3 processor on a Digilent Arty-A7

The LEON3 processor is a space-grade processor. Its VHDL sources are open source, which means you can have your own space-grade processor running on a FPGA board, at home. The LEON processor is a 32-bit CPU fully compatible with the SPARC-V8 instruction set from Sun Microsystems. What makes this processor interesting (for me at least), is that it is designed by the European Space Agency (ESA) for use in space missions. While development is currently no longer done by ESA themselves, the LEON processors are still used in ESA missions. The latest version is the LEON5. Wikipedia has much more information for further reading.

The LEON processors and supporting software are currently developed by Cobham Gaisler, but the VHDL sources, build tools and software libraries are available under a GPL license, I guess as a result of ESA funding much of their development with public money. This means that you can have your own spacecraft processor at home! Well, a softcore version of one at least.

While the latest version of the LEON processor series is the LEON5, I choose the LEON3. It can be configured to fit on fairly small (read: cheap) FPGAs and because it has been around for about 20 years already, there is a lot of documentation and tools available for it.

Choosing an FPGA board

GRLIB, which contains the VHDL sources and supporting scripts for the LEON processors and IP cores, can be downloaded from Gaisler's download page. On that page, you'll find an Excel sheet that provdes some estimates on the required FPGA sizing (number of LUTs, RAM, etc.) for a number of configuration options. You can use this to choose an FPGA board, but easier is to download the library and look under the designs directory. In there you'll find all the FPGA boards that are supported "out of the box" by GRLIB, in the sense that they provide a CPU configuration that will fit in that particular FPGA. They also provide the required constraint files and other parameters that are needed to get something useful running on that board.

Gaisler supports some boards with pre-built bitstreams and a bit more documentation. These are an order of magnitude more expensive. Gaisler also sells evaluation boards for their own (radiation-hardened) processors.

From the boards supported by GRLIB, I choose the Digilent Arty-A7 35T, mostly because it is cheap, yet sufficient for some first steps with the LEON3.

Preparation

The following was tried on CentOS 8, but should translate to any Linux distribution.

General requirements

Install libusb libftdi libXtst tcl tk ncurses via your preferred way (package manager or whatever your prefer). GRLIB expects libncurses.so.5 and libform.so.5, but you can symlink to *.so.6. You'll also need the usual developer tools, like make, autoconf, etc.; on CentOS I just did a groupinstall of "Development Tools".

Digilent Adept runtime and utilities

Install the Digilent Adept runtime and utilities. Both come with an install script that must be run as root user. After installation, plug in the board and run:

        $ dadutil enum
        Found 1 device(s)

        Device: Arty
            Device Transport Type: 00020001 (USB)
            Product Name:          Digilent Arty A7-35T
            User Name:             Arty
            Serial Number:         210319B0C2D8
        

If this utility doesn't show your board, fix that first. If you are running in a virtual machine, make sure to have USB3 enabled (xHCI). Also make sure that your user has rights to access serial devices; on CentOS you need to be added to the `dialout` group.

Also check that libftd2xx is installed. Digilent Adept (and Xilinx Vivado as well) requires it, but this library is closed source and therefore not available via package managers. Digilent ships a version of this library inside the runtime archive (in subfolder ftdi.drivers-<version>), or you can download the newest version from FTDI. Unfortunately, the Adept 2 runtime installation does not install this library automatically, nor does it ask you to do so. Hence, it is easy to end up with an incomplete installation. This forum post provides a way to check if your installation is complete: use ldconfig to see what libraries are known. The output should look like this:

        $ /sbin/ldconfig -p | grep ftd2xx
            libftd2xx.so (libc6,x86-64) => /usr/lib64/libftd2xx.so
            libdftd2xx.so.1 (libc6,x86-64) => /usr/lib64/digilent/adept/libdftd2xx.so.1
            libdftd2xx.so (libc6,x86-64) => /usr/lib64/digilent/adept/libdftd2xx.so
        

If libftd2xx is not installed the first line will be missing; the libraries in the digilent folder are not complete implementations, as is explained in the linked forum post.

Vivado

You'll need something to generate a bitstream and program the FPGA. A full list of supported software can be found in the GRLIB User Manual. I only have some limited experience with Xilinx Vivado, and fortunately the Arty-A7 is fully supported by the free WebPACK edition. I used version 2020.2 for writing this.

Before running the installer, set the LC_ALL and LANG environment variables (put this also in .bashrc or similar, becaue not setting these correctly results in strange errors):

        $ export LC_ALL=C.UTF-8
        $ export LANG=C.UTF-8
        

On CentOS 8 I had to create symlink libtinfo.so.5, since CentOS came with a newer libtinfo.so.6.

You'll need an account to download and install Vivado. Xilinx provides only a single generic installer, and the installer will prompt you for your account details in order to verify that you have a license for what you want to install. Hence, make sure to only install the WebPACK components. In order to save space, select only Artix-7 support.

You can install Vivado in any location. You don't need root access to run the main installer, but I'd recommend to install it in a location that is read-only for normal users, to avoid accidentally breaking things. Vivado comes with a small bash script to set environment variables (such as adding vivado to your PATH); you need to source it:

        $ . /path/to/Vivado/2020.2/settings64.sh"
        

After finishing the main installer, you need to also install the Xilinx cable drivers. This must be done as root (sudo will not work!):

        $ sudo su
        # /path/to/Vivado/2020.2/data/xicom/cable_drivers/lin64/install_script/install_drivers/install_drivers
        

GRLIB and GRMON

You can download GRLIB from Gaisler's download page. It is a single compressed archive that you only need to extract somewhere. I recommend putting it in a read-only location.

You will also need GRMON, their debug tool. It too can be downloaded from this page. It is free for personal use. Extract the archive and add the bin64 and lib64 paths to your PATH and LD_LIBRARY_PATH variables.

Building the LEON3 example design

GRLIB comes with a designs for a large number of boards. They reside in the "designs" directory. To get started, copy the entire leon3-digilent-arty-a7 directory to a workspace directory (careful: there is an identically named directory under "boards"!). In the top-level Makefile, modify the GRLIB variable to point to the root of the extracted GRLIB archive and then run:

        $ make scripts
        

This will create a number of files and directories with support files for various toolchains, including Vivado. Open the file vivado/leon3mp_vivado.tcl with a text editor and find the line:

        #upgrade_up [getips mig]
        

It should be near the end of the file. Uncomment it by removing the #. "MIG" refers to Memory Interface Generator and is a Xilinx IP core. You need to build it only once, so after the first run you can comment the line in the TCL script out again. Now, start the build and synthesis process:

        $ make vivado
        

This takes about 15 minutes on a reasonably up to date machine. Alternatively, run:

        $ make vivado-launch
        

for an interactive session (in that case, do not uncomment the line in the TCL script, as it will immediately try to build the MIG core on startup; instead, do the build from the context menu (look for "Upgrade IP...").

Note: Vivado requires 3.5 GB of RAM with the Arty-A7, more than the 3 GB that Xilinx reports. My headless CentOS virtual machine needs 5 GB of RAM to synthesize the default design. If you run from the Vivado GUI, you'll need more.

Flashing the bitstream

When the build is complete, you can flash it to the board. You have to options:

  1. Program the FPGA core only; the bitstream is not stored in ROM and thus "gone" after a power cycle.
  2. Put the bitstream in ROM; the bitstream is automatically loaded into the core when power is turned on.

For the first option, make sure that the MODE jumper (JP1) on the board is open and execute:

        $ make vivprog
        

For the second option, make sure that the MODE jumper (JP1) on the board is closed and execute:

        $ make vivrom
        

After a few minutes, the LED labeled DONE will light up. Press the RESET button to start the LEON3 core. LED 4, 5 and 7 should light up. Now, use GRMON to connect to the core (the -freq flag is should not be necessary, but sometimes GRMON detects the frequency incorrectly):

        $ grmon -digilent -freq 83
          GRMON debug monitor v3.2.11.1 64-bit eval version

          Copyright (C) 2021 Cobham Gaisler - All rights reserved.
          For latest updates, go to http://www.gaisler.com/
          Comments or bug-reports to support@gaisler.com

          This eval version will expire on 20/07/2021

        JTAG chain (1): xc7a35t
          GRLIB build version: 4261
          Detected frequency:  83.0 MHz

          Component                            Vendor
          LEON3 SPARC V8 Processor             Cobham Gaisler
          JTAG Debug Link                      Cobham Gaisler
          GR Ethernet MAC                      Cobham Gaisler
          SPI Memory Controller                Cobham Gaisler
          AHB/APB Bridge                       Cobham Gaisler
          LEON3 Debug Support Unit             Cobham Gaisler
          Xilinx MIG Controller                Cobham Gaisler
          Generic UART                         Cobham Gaisler
          Multi-processor Interrupt Ctrl.      Cobham Gaisler
          Modular Timer Unit                   Cobham Gaisler
          AMBA Wrapper for OC I2C-master       Cobham Gaisler
          SPI Controller                       Cobham Gaisler
          General Purpose I/O port             Cobham Gaisler
          General Purpose I/O port             Cobham Gaisler
          General Purpose I/O port             Cobham Gaisler

          Use command 'info sys' to print a detailed report of attached cores

        grmon3>
        

Compiler toolchain

To compile software for the LEON3, you need the LEON Bare-C Cross Compilation System (BCC), which is provided by Gaisler for free. It is based on GCC, so it easy to work with if you are used to those tools. BCC can be downloaded from here. Make sure to download BCC 2.x.

Installing BCC2 is easy: as explained in the user manual, just extract it somewhere and add the folder with executables to you PATH environment variable, and the folder with manual pages to the MANPATH environment variable.

Hello, world

In the BCC2 archive there is an directory with examples. In it is a README file, a Makefile, and a number of directories with various examples worked out. The folder hello contains the essential "hello, world" example, implemented in a single file hello.c:

        #include <stdlib.h$gt;
        #include <stdio.h$gt;

        int main(void)
        {
                printf("hello, world\n");

                return EXIT_SUCCESS;
        }
        

From the root of the examples directory (i.e., where the Makefile is), execute:

        $ make CFLAGS="-mcpu=leon3 -msoft-float" hello.elf
        

This will produce the executable hello.elf. With the board connected, start GRMON in UART debug mode:

        $ grmon -digilent -u
        

Reset the processor and load the binary that was just compiled:

        $ grmon -digilent -u
        grmon3> reset
        grmon3> load hello.elf
                  40000000 .text             23.0kB /  23.0kB   [===============>] 100%
                  40005C00 .rodata            128B              [===============>] 100%
                  40005C80 .data              1.2kB /   1.2kB   [===============>] 100%
          Total size: 24.28kB (19.45kbit/s)
          Entry point 0x40000000
          Image /path/to/examples/hello.elf loaded
        

The program can be executed via the "run" command. Because GRMON was started in UART debug mode, the output is printed to the GRMON console:

        grmon3> run
        hello, world

          Program exited normally
        

Flashing to ROM

Instead of having to manually load and run our program, it can also be flashed to ROM. The MKPROM utility takes any executable and embeds it into a an image that takes care of system initialization, then extracts the executable to RAM and runs it. It is basically a tool that adds your program to a bootloader. MKPROM2 can be downloaded from Gaisler's download page.

MKPROM2 must be installed in /opt. There are some hard-coded paths in the utility that look for things there. A symlink in /opt that points to the extracted archive works.

The MKPROM2 utility needs to know a few things of the board that it will be compiling for, in particular the frequency it runs at and the baudrate of the UART. Both can be extracted via GRMON:

        $ grmon -digilent
        ...
        grmon3> info mkprom2
          Mkprom2 switches:
          -leon3 -freq 83 -baud 38425
        

The reported frequency should (and it does) match the frequency that was specified when the FPGA bitstream was built. The baudrate is estimated and the value reported here is not a standard baudrate. We need to use the nearest standard rate, which is 38400.

The same examples folder used before has an mkprom-hello folder with a Makefile and a C file that we will be using. All commands in this section must be executed in this folder.

The example LEON3 design for the Arty A7 does not have a floating point unit (not available in the GPL version of GRLIB) and has single vector trapping model enabled. Combined with the parameters reported by GRMON, the compilation command for the MKPROM example is:

        $ make CFLAGS="-mcpu=leon3 -msoft-float -qsvt" MKPROMOPT="-leon3 -msoft-float -qsvt -freq 83 -baud 38400"
        

The flags to MKPROM ("MKPROMOPT") must match those for compilation ("CFLAGS") of the executable itself!

The resulting executable has an entry point at 0x0, as can be observed via obj-dump:

        $ sparc-gaisler-elf-objdump -x hello.prom

        hello.prom:     file format elf32-sparc
        hello.prom
        architecture: sparc, flags 0x00000012:
        EXEC_P, HAS_SYMS
        start address 0x00000000

        Program Header:
            LOAD off    0x00000060 vaddr 0x00000000 paddr 0x00000000 align 2**5
                 filesz 0x0000b940 memsz 0x0000b940 flags rwx

        Sections:
        Idx Name          Size      VMA       LMA       File off  Algn
          0 .text         0000b940  00000000  00000000  00000060  2**5
        ...
        

In the README inside the design folder for the Digilent Arty-A7 (designs/leon3-digilent-arty-a7/README.txt) there is however a very important message:

        Typically the lower part of the SPI flash device will hold the
        configuration bitstream for the FPGA. The SPIMCTRL core is configured
        with an offset value that will be added to the incoming AHB address
        before the address is propagated to the SPI flash device. The
        default offset is 0x00400000 (this value is set via xconfig and the
        constant is called CFG_SPIMCTRL_OFFSET). When the processor starts
        after power-up it will read address 0x0, this will be translated by
        SPIMCTRL to 0x00400000.

        SPIMCTRL can only add this offset to accesses made via the core's
        memory area. For accesses made via the register interface the offset
        must be taken into account. This means that if we want to program
        the Flash with an application which is linked to address 0x0 (our
        typical bootloader) then we need to add the offset 0x00400000 before
        programming the file with GRMON. We load the Flash with our application
        starting at 0x00400000 and SPIMCTRL will then translate accesses from
        AMBA address 0x0 + n to Flash address 0x00400000 + n.
        

You can verify this offset in the configuration code for the board support:

        $ grep SPIMCTRL_OFFSET leon3-digilent-arty-a7/config.vhd
          constant CFG_SPIMCTRL_OFFSET : integer := 16#400000#;
        

To add the offset, we need to modify the Load Memory Address (LMA) of our binary. This can be easily done with the following command:

        $ sparc-gaisler-elf-objcopy --change-section-lma *+0x400000 hello.prom hello.spim
        

I changed the extension from .prom to .spim for clarity. Verify that the LMA address has changed:

        $ sparc-gaisler-elf-objdump -x hello.spim

        hello.spim:     file format elf32-sparc
        hello.spim
        architecture: sparc, flags 0x00000012:
        EXEC_P, HAS_SYMS
        start address 0x00000000

        Program Header:
            LOAD off    0x00000060 vaddr 0x00000000 paddr 0x00400000 align 2**5
                 filesz 0x0000b940 memsz 0x0000b940 flags rwx

        Sections:
        Idx Name          Size      VMA       LMA       File off  Algn
          0 .text         0000b940  00000000  00400000  00000060  2**5
        ...
        

To flash the binary to ROM, start GRMON and let it detect the flash memory module before proceeding:

        $ grmon -digilent -freq 83
        ...
        grmon3> spim flash detect
          Got manufacturer ID 0x01 and device ID 0x2018
          Detected device: Cypress S25FL128S
        

The detection step is necessary and cannot be skipped. Now load the offset binary to flash; it will be loaded at 0x40000:

        grmon3> spim flash load hello.spim
                    400000 .text             46.3kB /  46.3kB   [===============>] 100%
          Total size: 46.30kB (820.42bit/s)
          Entry point 0x00000000
          Image /path/to/examples/mkprom-hello/hello.spim loaded
        

To verify that it was successful, one could run the verify command, but we can also just have a quick look at the first four instructions (the address is the address that the processor sees, not the physical address!):

        grmon3> disassemble 0 4
               0x00000000: a7580000  mov  %tbr, %l3
               0x00000004: a1480000  mov  %psr, %l0
               0x00000008: a60ceff0  and  %l3, 0xff0, %l3
               0x0000000c: a734e004  srl  %l3, 0x4, %l3
        

Compare it with the binary we just built:

        $ sparc-gaisler-elf-objdump -D hello.prom | head -n 11

        hello.prom:     file format elf32-sparc


        Disassembly of section .text:

        00000000 <_start_svt_real>:
               0:	a7 58 00 00   rd  %tbr, %l3
               4:	a1 48 00 00   rd  %psr, %l0
               8:	a6 0c ef f0   and  %l3, 0xff0, %l3
               c:	a7 34 e0 04   srl  %l3, 4, %l3
        

Note: ROM must be erased before writing. Use the SPIM "flash erase" command to erase memory, but be careful: without arguments it erases the entire ROM, including the FPGA bitstream!

To run the program, open a second terminal and connect it to the UART:

        $ screen /dev/ttyUSB1 38400
        

In the first terminal, in GRMON, instruct the processor run from address 0:

        grmon3> run 0
          Program exited normally
        

On the second terminal, you should see something like this:

          MKPROM2 boot loader v2.0.67
          Copyright Cobham Gaisler AB - all rights reserved

          system clock   : 83.0 MHz
          baud rate      : 38425 baud
          prom           : 512 K, (15/15) ws (r/w)
          sram           : 2048 K, 1 bank(s), 3/3 ws (r/w)

          decompressing .text to 0x40000000
          decompressing .rodata to 0x4000f120
          decompressing .data to 0x4000f930

          starting hello.elf

        --- EXAMPLE BEGIN ---

        Hello World!

        tick 1
        tick 2
        tick 3
        tick 4
        tick 5
        tick 6
        tick 7
        tick 8
        tick 9
        tick 10

        --- EXAMPLE END ---
        

The executable will be run every time the board is powered on, immediately after flash programming. You can verify this by attaching a screen session to the serial device and press the reset button on the board. Note that without GRMON attached, you’ll see two ttyUSBx devices in /dev: the first is the JTAG bridge, the second is the UART.

        $ screen /dev/tty.usbserial-210319B0C2D81 38400
        

Home | Last update: 2021-05-23