Porting Micropython to Raspberry Pi

This article is in English for I hope it will be useful for someone who tries to port Micropython to new hardware as I do here.

Micropython [1] is a very small Python interpreter, which can run on very restricted hardware. The pyboard is based on an STM32-F04 ARM processor, with 1 KiB of Flash-ROM and 192 KiB of RAM. Micropython can and was ported to different hardware before. But the Raspberry Pi seems a bit overpowered im comparison to the F04-processor.

This text describes the way to built a bare-metal port of Micropython for the Raspberry Pi. Using the article you will build a Micropython running on the Raspberry Pi as Operating System, there is no Unix running to aid Micropython with anything.

Software Environment

I have to computer to build Micropython on: An Arch Linux and a (very old) Linux Mint 17.1. You will need the following software:

  • arm-none-eabi-gcc
  • arm-none-eabi-binutils

Micropython does some calls to a C-library, which I use newlib [2] for. My code can be found under [3]. The Makefile contains rules for building newlib seperately by invoking make newlib. This will call  the configure-script of newlib and compile it to a static libary especially for the Raspberry Pi.

You will probably want to use a tool like Raspbootin [4] to transfer the binary to the Raspberry Pi. Raspbootin makes it possible to transmit compiled code to the Pi via a serial (i.e. UART) interface to the Pi.

Details on the newlib-build

Building newlib for the Raspberry Pi was quite a challenge for me, as I struggled at first to build it with support for the Raspberry Pis Vector Floating Point Unit (VFPv2). The following setup seems to work fine:

CFLAGS_FOR_TARGET="-mcpu=arm1176jzf-s -mfpu=vfp -marm -mfloat-abi=hard"
configure --target=arm-none-eabi --enable-newlib-hw-fp --with-float=hard --with-cpu=arm1176jzf-s \
          --with-fpu=vfp --disable-multilib --disable-shared --enable-target-optspace  \
          --disable-newlib-supplied-syscalls

At first I need to specify the CPU of the build, i.e. the CPU I want to build for. The Raspberry Pi BCM2835-chip contains a arm1176jzf-s processor. Be aware, that this only holds true for the Raspberry Pi 1 Model A, Model B+, etc. but not for the Raspberry Pi 2 or 3. Then I instruct the configure-script still inside the CFLAGS_FOR_TARGET-field, to build float-abi hard and vfp-support, and to emit ARM-code not code for the Thumb-mode of that processor.

The configure call itself instructs configure to use the target arm-none-eabi, enables the hardware floating point unit, disables multilib, i.e. disables that a Thumb-version is build, disables that a shared libary is build and disables syscalls inside newlib. The latter is important because I do (did) not want to write Interrupt Service Routines for the Raspberry Pi (yet).

Syscalls

Newlib is a C-library. A C-library contains call which need syscalls for working correctly, for example printf needs a write-syscall so it can output anything to stdout. It is our job to implement write, so newlib can use the "syscall" and printf can output anything. David Welch also has code prepared for that job.

Porting Micropython

Porting Micropython to the Raspberry Pi was esentially quite easy - at least to get [any]{style="text-decoration: underline;"} response from the Micropython. Esentially I took a lot of working code from David Welch, his AUX_UART-code to be exact and used that for the serial connection between the Raspberry Pi running Micropython and my PC. I was working on a clean rewrite of that code, but that did not work out that well yet, so I continue using this code for the time being.

I starting with the folder bare-arm and started tweaking its Makefile until it built binaries for the Raspberry Pi. The bare-arm-port is written for an Cortex-M4 processor, which in turn I needed to replace with the ARM1176jzf-s processor of the Raspberry Pi.

Port structure

The folder contains the following files, which are or may be important to your port:

  • Makefile - duh!
  • mphalport.h - I did not really use that file, so I don't know what it was for
  • mphalconfigport.h - defines loads of options how to build Micropython. You can change a lot of builtin-stuff here
  • qstrdefsport.h - defines QStrings, which are used by your port specifically
  • kernel.ld - The Linker skript

On QStrings

The QStrings are strings, which are held only once in Memory even if they are used by two completely separate parts of the code. The reason why you have to write them there in the following way, is that the build-process creates constants from them and builds them into Micropython only once. It needs to check whether any string is used twice and build it only once.

Q(hello)
Q(inc)
Q(dec)
Q(offset)
// ...

More on these QStrings in a future article.

Configuring the builtins of the port

Using preprocessor-statements in the file mphalconfigport.h you can change what parts of Micropython are built into the binary. This may be essential to ports to hardware with very limited resources, as every module, which is built into CPython by default can increase the binary size and in turn the memory footprint of the interpreter at run-time.

The options are endless. For a (I believe not complete) list of options and a very short description on what they do you can consult the file /py/mpconfig.h in the project repository [5].

There are very important other options there. For example you have to define what modules are built into Micropython, maybe you want to write module for it yourself to use in your Python code. But more on this in a later article. Also there is the macro [MP_PLAT_PRINT_STRN]{.pl-en}([str, len]{.pl-v}) which you have to define as a print-function. I used it as macro for a print-string-function, which prints a string through the UART-interface of the Raspberry Pi.

The linker script

The linker script tells our linker where a particular part of our code should be placed in the binary. I used the kernel.ld linker script of the Baking Pi-Tutorial [6]. This is a quite simple script but defines all the needed areas which are used by our compiler and states where they should be placed.

The more complete picture

After adding some important information, like that I want to link the binary against newlibs libc and libm-libraries and the libgcc I need to add the code-files that should be built into Micropython. This can be done by adding them to the SRC_C and the SRC_S-Variables (for Assembler-files).

At first I had only one Assembler-file (startup_rpi.s) which directly jumped into the main-routine which is defined in a C-file:

.section .init
.globl _start
.globl blinkloop

_start:
    mov sp, #0x8000
    bl main

blinkloop:
    // ...
    b blinkloop

The main-routine then sets up the Micrpython and runs little test-skripts. It also needs to set the stack accordingly, so that Micropython will not run out of memory instandly. On the first run I expect the code to just run and work perfectly, but instead the statements fail, which I find very interesting. The follwing code was basically copied from the bare-arm-folder.

void do_str(const char *src, mp_parse_input_kind_t input_kind) {
    mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
    if (lex == NULL) {
        return;
    }

    nlr_buf_t nlr;
    if (nlr_push(&nlr) == 0) {
        qstr source_name = lex->source_name;
        mp_parse_tree_t parse_tree = mp_parse(lex, input_kind);
        mp_obj_t module_fun = mp_compile(&parse_tree, source_name, MP_EMIT_OPT_NONE, true);
        mp_call_function_0(module_fun);
        nlr_pop();
    } else {
        // uncaught exception
        mp_obj_print_exception(&mp_plat_print, (mp_obj_t)nlr.ret_val);
    }
}

int main(int argc, char **argv) {
    mp_init();
    do_str("print('hellp world')", MP_PARSE_SINGLE_INPUT);
    do_str("print('hello world!', list(x+1 for x in range(10)), end='eol\\n')", MP_PARSE_SINGLE_INPUT);
    do_str("for i in range(10):\n  print(i)", MP_PARSE_FILE_INPUT);
    mp_deinit();
    return 0;
}

* do_str takes two parameters. The second parameter is in accordance to the Python-function compile*. Using single-input force Micropython to interpret the statement as single statement and therefore will only interpret a single statement at a time, whereas file-input allows for bigger scripts.

The bigger picture

Now we have a running Micropython interpreter on Raspberry Pi running without an Operating System. Of course this Micropython can not be used for anything at the moment, there are basically no drivers, there is no video output, no GPIO-controlling and no input or output. So there is still a lot of work to do for the Raspberry Pi port to become finished or usable. But this is a start, even if a very small one.

But I do not only aim at porting Micropython but also at documenting it along the way as far as I'm comfortable. The code of Micropython is largely without any useful comments, functions are used, and a new developer has no chance but to guess what a function name may mean or what this function could possibly do.

ToDo

  • Build some sort of REPL or interactive mode, so automatic testing becomes trivial
  • Run longer Python code and find out why it fails and/or crashes

[1] micropython.org
[2] sourceware.org/newlib
[3] github.com/naums/micropython
[4] github.com/naums/raspbootin
[5] github.com/micropython/micropython/blob/master/py/mpconfig.h
[6] cl.cam.ac.uk/projects/raspberrypi/tutorials/os/

Last edit: 18.05.2017 23:07