MicroPython has evolved recently to become a great platform for quick prototyping of microcontroller software. The expressiveness of the Python language and its rich library ecosystem make it extremely useful and allow the testing of embedded software in a matter of minutes.

However, when a real-time solution is needed, care has to be taken as there are inherent limitations imposed by the Python virtual machine. MicroPython tries to work around the lack of memory and the low speed of microcontrollers and is designed with these constrains in mind.

There are ways to maximize the speed at which MicroPython runs. Our tests are simple as they do not involve complex memory allocations, garbage collection or asynchronous programming. We are interested in exhibiting just the limitations of the MicroPython virtual machine and unavoidable architectural issues like cache loading when jumping to code stored in flash. These quick tests provide us with a rough idea of the performance that can be reached when accessing the GPIOs on some of the MicroPython reference boards. These figures are obtained in a system with no additional CPU load, without mounting the mass storage device and trying different code emitters.

GPIO toggling

A line of MicroPython code takes a few μsecs to execute on a PyBoard @168MHz. Toggling a GPIO pin should of course be faster than most instructions. Tests have been performed using the following code:

from pyb import Pin p_out = Pin('X3', mode=Pin.OUT_PP) while True: p_out(1) p_out(0) p_out(1) p_out(0)

An oscilloscope can easily show the time it takes to execute the GPIO set/clear functions as well as the time taken by the loop branch. The results are quite stable, with no appreciable jitter:

code emitter loop branch (μs) GPIO set/clear (μs) bytecode 5.2 2.7 native 3.4 2.3 viper 0.35 2.2

These tests have also been performed on a Nucleo F429ZI board @180MHz where we observe slightly worse values (GPIO set instructions take around 10% longer).

GPIO external interrupts

We measure the time it takes for the PyBoard to set an output GPIO pin in response to an interrupt triggered on an input GPIO. The source of interrupts is a pulse generator that triggers an interrupt every millisecond. The following code has been used to measure the interrupt latency with the help of an oscilloscope:

from pyb import Pin def gpio_cb(e): p_out(1) p_out(0) p_out = Pin('X3', mode=Pin.OUT_PP) ExtInt(Pin('X4'), ExtInt.IRQ_RISING, Pin.PULL_DOWN, gpio_cb)

The interrupt latency results on a PyBoard v1.0 @168MHz are stable and exhibit low jitter, with these results obtained after running for one hour:

code emitter interrupt latency (μs) interrupt jitter (μs) bytecode 7.3 < 5.0 native 5.7 < 4.0 viper 3.3 < 4.0

The signal was captured with an oscilloscope running in persistent mode. The constant execution time of the GPIO pin toggling instructions gives us confidence that the observed variability is due only to the interrupt jitter.

Tests have also been performed in a Nucleo F429ZI board @180MHz, where we observe a slightly worse performance (GPIO interrupt latency is similar with bytecode but around 20% worse with native and viper emitters). However, the Nucleo board shows a much more stable latency value, with jitter bounded at 2μs.

We have written some code that produces an histogram of the measured latency variability.

Shall we use MicroPython in our real-time projects?

MicroPython is mature, elegant, and offers great productivity advantages over other embedded software environments. MicroPython is quite capable of running real-time code as long as we put some care around its limitations; after all we have to remember that real-time is not about being fast but about being deterministic. Providing the memory/speed resources are sufficient and we can meet our real-time deadlines, it is a great platform which we intend to use more and more in the future.