FuPy: MicroPython for FPGAs
2018-09-26Thanks to Tim ‘mithro’ Ansell and Ewen McNeill for their help getting my head around FuPy and correcting my misunderstandings. Remaining mistakes are all my own!
FuPy is a port of MicroPython which runs on a Soft Microprocessor core implemented on an FPGA.
It builds on four other projects:
- Migen, a Python tool for VLSI design. It provides an alternative to designing chips in VHDL or Verilog. Migen includes a system-on-a-chip library called MiSoC.
- LiteX, a fork of MiSoC which includes lots of peripherals implemented in Migen and a choice of CPU cores such as LatticeMico32, OpenRISC and RISC-V, mostly written in Verilog.
- LiteX Buld Env, a build system for LiteX SoC designs.
- MicroPython a Python 3 implementation for resource-limited systems. The FuPy MicroPython fork adds support for FPGA-based systems.
I don’t know a lot about FPGAs, but I was fortunate enough to have Tim and Ewen introduce me to FuPy at PyConAU 2018 and provide a Digilent Arty A7 board to play with, so let’s go from there:
Building
Summary of steps from Ewen’s instructions for Artix 7 and the HowTo FuPy Arty A7 doc on the litex-buildenv Wiki:
- Download Xilinx Vivado HLx 2018.2 WebPACK Installer
- Install to
/opt/Xilinx/
- Vivado is the tool you need to program the Artix 7.
- WebPACK is the name for the zero-cost version.
- The 100MB download is just the installer, the whole bundle is 17GB.
- I am greatly looking forward to using a platform with a compact, free toolchain.
- Install to
- set up environment (I use direnv):
- CPU=lm32
- PLATFORM=arty
- TARGET=base
- FIRMWARE=micropython
- git clone https://github.com/timvideos/litex-buildenv.git
- cd litex-buildenv
- scripts/download-env.sh
- This step downloads another couple of GB.
- sudo scripts/download-env-root.sh
- This just installs the timvideos PPA and various packages
- source scripts/enter-env.sh
- make gateware
- scripts/build-micropython.sh
- make gateware-load
- make firmware-load
The make gateware
step is pretty CPU intensive as it tries to work out how to
arrange all the stuff you asked for onto the FPGA efficiently.
It’s a good way to get the dust out of your laptop’s CPU fan.
This may not be the best project to try out on the aeroplane.
On Ubuntu 18.04, the scripts/build-micropython.sh
step (or even just
running lm32-elf-newlib-gcc
) crashes out with a message:
lm32-elf-newlib-gcc: loadlocale.c:130: _nl_intern_locale_data:
Assertion `cnt < (sizeof (_nl_value_type_LC_TIME) /
sizeof (_nl_value_type_LC_TIME[0]))' failed.
This seems to be some kind of glibc error loading locales. Thanks to the hint in
this stack exchange
post I found that either setting LANG=/usr/locale/C.UTF-8/
(just for this command)
or installing the Ubuntu locales-all
package would fix this problem.
If it gets stuck at the [FLTERM] Starting...
message try pressing the hardware reset button,
Following these instructions gets us as far as a serial REPL running on the Arty, from which we can flash an LED:
>>> import litex
>>> led1 = litex.LED(1)
>>> led1.on()
>>> led1.off()
Okay, so that’s not the most exciting thing in the world, but its something!
Files & Repositories
Before we change anything we’ve got to work out what’s where.
The Litex Buildenv repository pulls in a whole bunch of toolchain stuff as submodules, and also the build scripts pull in other modules including FuPy. All up there’s about 2.5GB of stuff in this build directory now.
Platforms
The platforms/
directory contains descriptions of the pin assignments for various
development platforms, for example platforms/arty.py
contains (in part):
_io = [
("user_led", 0, Pins("H5"), IOStandard("LVCMOS33")),
("user_led", 1, Pins("J5"), IOStandard("LVCMOS33")),
# ...
("user_sw", 0, Pins("A8"), IOStandard("LVCMOS33")),
# ...
("user_btn", 0, Pins("D9"), IOStandard("LVCMOS33")),
# ...
("serial", 0,
Subsignal("tx", Pins("D10")),
Subsignal("rx", Pins("A9")),
IOStandard("LVCMOS33")),
# ...
]
These definitions map the ridiculous number of I/O pins to the development board hardware. User LED 0 is connected on pin H5, and expects CMOS 3.3V voltages. Etc.
Targets / Gateware
The targets/
directory contains descriptions of the SoC used on the target boards,
pulling in submodules which implement individual bits of hardware. A lot of stuff is
under the ‘CAS’ (“Control and Status”, or sometimes “Configuration and Status”) module.
The gateware/
directory contains implementations for the hardware. For example,
gateware/cas.py
includes code to enumerate all the LEDs defined in the platforms/*.py
file,
and turn them into constructs which can be compiled into the FPGA.
I’m still finding my way around this code …
When you make gateware
, it is compiled into a binary description of the configuration of the
gates on the FPGA, sometimes known as a “bitstream”. This is a very slow step because the
compilation process involves finding an optimal way to arrange the logic you’ve asked for onto the
available logical units … careful arrangement leads to higher clock speeds.
CSR
The compilation step also produces a mapping of “Control Status Registers” aka CSR. The gateware defined above is mapped into the softcore’s memory, and accessed by memory reads and writes, just like on a lot of microcontrollers.
The CSR map is written out as:
- C header file format:
build/arty_base_lm32/software/include/generated/csr.h
… macro defines and wrapper functions for each register to make them available from C functions - CSV tabular format:
build/arty_base_lm32/test/csr.csv
… the same information in tabular form.
For example, the LEDs above end up being written out as something like this in csr.h
:
#define CSR_CAS_BASE 0xe0006800
#define CSR_CAS_LEDS_OUT_ADDR 0xe0006800
#define CSR_CAS_LEDS_OUT_SIZE 1
static inline unsigned char cas_leds_out_read(void) {
unsigned char r = MMPTR(0xe0006800);
return r;
}
static inline void cas_leds_out_write(unsigned char value) {
MMPTR(0xe0006800) = value;
}
MicroPython
Finally, we can build micropython. It is downloaded into
third_party/micropython/
and the FuPy port is at ports/fupy
.
The module litex_leds.c
includes csr.h
(see above) and uses that to find the
registers corresponding to the LEDs. These are then wrapped up into Python calls:
STATIC mp_obj_t litex_led_on(mp_obj_t self_in) {
litex_led_obj_t *led = self_in;
char value = cas_leds_out_read();
cas_leds_out_write(value | (1 << (led->num - 1)));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(litex_led_on_obj, litex_led_on);
… and this gets exposed as the LED.on()
method called in our REPL code above.
UPDATE 2018-11-13
FuPy now builds on the TinyFPGA BX … a much smaller and cheaper board with a (reverse-engineered) Open Source FPGA toolchain. See:
Further Work
My first step is going to be to get the hang of how all this works by adding in support for PWM channels and the RGB LEDs. I’d like all 16 channels (4 x mono LEDs plus 4 x RGB LEDs) to support 8 bit PWM, and all work from the same PWM timer.
Next, I’d like to look at automating the wrapping process, so that instead of
having to write individual C functions like litex_led_on
above, we could have a
module _csr
(or some name like that) which automatically makes available the
CSR registers to Python with appropriate wrappers, and then more specific
driver behaviour can be implemented in Python.
Eventually, I’d like to look at how LiteX could generate a DeviceTree and MicroPython could read that to discover the register mappings. This would greatly decouple the gateware compilation process from the MicroPython compilation process. Also, it should allow MicroPython to discover the hardware properties of DeviceTree-compatible SPI devices through the use of Device Tree Overlays