Embedding Micropython on ESP32

TL;dr To jump straight to the section on the solution I came up with follow this link

I’ve been working hard to help create a new version of a brilliant little robot called Marty which is made by a company based in Edinburgh (UK) called Robotical – shameless plug is here: http://robotical.io

One of the coolest features of the latest Marty is the ability to run Python programs directly on the robot and this has already been used to implement screen-free coding where the robot can detect coloured cards placed in front of it and walk between them according to “instructions” denoted by the colour of the card.

This post documents the way that I’ve (eventually after several attempts) managed to embed the superb MicroPython created by Damien George into an existing ESP32 IDF codebase so Marty can run programs from its file-system. It’s really amazing how powerful the Espressif ESP32 which powers Marty has proven to be – it is handling WiFi and Bluetooth connectivity, nine intelligent servo motors, accelerometer and battery management, interfaces to apps, python and hosting a web-server all without missing a beat.

The approach I’ve taken may be helpful to others and it has the benefit, hopefully, of making it fairly easy to accommodate updates to MicroPython because I use both the standard MicroPython build system and my own in parallel.

Embedding MicroPython Aint Easy!

When I started looking for a language for Marty, Python was at the top of the list because it is so widely used and easy to learn. But my first attempts at embedding proved fruitless – it just seemed too darned hard. It seems that MicroPython is designed to take over the entire processor – maybe because the initial targets had very limited resources – and it is amazing that Micropython can run with only 8KB (perhaps even less?) RAM and 64KB Flash.

The ESP32 is much more capabable than this (it has over 200KB RAM and several MB of Flash available to application programs even when using Bluetooth and WiFi stacks). But unfortunately the ESP32 port also takes full control and forces other code to fit into the MicroPython build-system.

I started with a fully functional Marty using the ESP IDF build-system – which is based on CMake and quite different from the MicroPython build-system (which uses GNU-make but also dynamically builds some source files).

Fail Fast

My first two attempts at integration focused on the MicroPython ESP32 Embedding Example and this pull request for a better approach but unfortunately I failed miserably. I got the examples to work but got stuck on these limitations:

  • MicroPython runs in the main execution thread so there isn’t much chance to run any non-Python code at the same time
  • The port uses a complex build process and ultimately creates a final firmware image – not a library – so integration into another build-system is pretty difficult
  • MicroPython uses “definitions” file called mpconfig.h and mpconfigport.h to turn on/off Python features and shoe-horn MicroPython into a small enough footprint for each target platform. This made it hard to combine with features already implemented on the robot.

First I tried to “break-into” the build system and pick apart the layers. It is pretty complex and it became clear that I’d have to do a lot of editing before I’d have any idea whether the build would actually work. I really don’t like working like this – you can spend hours on trivial problem only to find there is a much bigger show-stopper that means all the work is wasted. While I think MicroPython is well designed it isn’t really clear what is possible when trying to fit it into another codebase.

Starting Small

For my third attempt I did something very different. Instead of starting at the “top” and trying to mash together the two build-systems, I started at the “bottom” of the MicroPython code and tried to find the tiniest bits of functionality that I could move into the existing codebase and get working. I guess a suitable metaphore is to “find a thread and start pulling”.

At each stage I added the source files to my existing build-system (ESP IDF CMake) in a separate component (I like the way ESP IDF does this) and did a lot of temporary commenting-out to minimise dependencies.

It actually too me several sessions of a few hours each to pull in all of the code required to make a Python program execute in this way – but I got the added benefit of having to read a lot of the MicroPython core codebase and I felt a lot more confident in ultimately getting a working solution as a result.

Sidestepping the Tricky Issues

There remained one big “elephant in the room” however. Those darned dynamically generated files I referred to earlier. They are mainly to MicroPython strings (called Q-strings) which are embedded into the C source code and include the following:

  • Strings that represent some (but not all?) Python keywords like lower, class, bool, etc
  • Strings that come from Python libraries implemented in C such as time, unpack, max, etc
  • Strings that Python uses for special purposes like __init__, __name__, __str__, etc

These strings are dynamically generated during the build process into files including:

  • qstrdefs.collected.h
  • qstrdefs.generated.h
  • qstrdefs.preprocessed.h

Building MicroPython isn’t possible without generating these files and the build scripts that generate them are pretty complex. One option would have been to drag the parts of the build scripts into my own scripts but that would have made pulling later MicroPython revisions into the codebase much harder in future.

Fortunately I was able to come up with another approach which side-steps this issue by creating two different “builds” of MicroPython side-by-side. One build uses the standard MicroPython system and one uses ESP IDF. By progressively modifying the MicroPython mpconfigport.h file (described earlier) in each of the “builds” to exactly match one-another I was able to generate the Q-string definitions files in the standard build and then copy them over to the ESP IDF build. Hence I could generate a functional MicroPython port without having to modify the dynamic generation process.

A Workable Hybrid Solution

The final version is on GitHub

Having pursued this approach far enough to get a working MicroPython implementation I have now brought the two build systems together into a single process. To do this I’ve done the following:

  • Added MicroPython as a git submodule of the main firmware in a folder called “external”
  • Added a ports/ric folder into the main firmware which mirrors the structure of the other MicroPython ports sub-folders.
  • Added the required MicroPython source files from the “external” folder in the build of the MicroPython ESP IDF component (along with some “glue” code that allows access to the created MicroPython interpreter within the main application)

The ESP IDF follows this procedure:

  • Copy the ports/ric folder in the main tree into a new external/ports/ric sub-folder
  • Include the build of the MicroPython firmware using the classic MicroPython build system in the ESP IDF build-system using CMake add_custom_command()
  • Include the folder containing the generated files in the ESP IDF build-system for the MicroPython component so that the generated Q-string files are found when included from the MicroPython source code