C extension for MicroPython on Raspberry Pi Pico

This is a guide on how to write and run a basic C extension for MicroPython on the Raspberry Pi Pico. I will cover reasons for extending MicroPython, how to write the extension module, how to compile it and how to install it on the Pico. If you want to read more about this topic, please refer to https://docs.micropython.org/en/latest/develop/extendingmicropython.html

Introduction

Why extend MicroPython?

There can be multpiple reasons why you might want to extend MicroPython. Since it is a trimmed down version of Python, MicroPython is not performing very well when it comes to computationally heavy tasks (similar to Python). C on the other hand is really well suited for such tasks. A C extension for Micropython enables you to perform those computations through C language and use the results in MicroPython. This can speed up some processes by an order of magnitudes.

A second reason might be, that you want to interface some hardware which uses a C API from within MicroPython. 

Types of MicroPython C extensions

There are to different ways to extend MicroPython : external C modules and native machine code in .mpy files.

To run an external C module you need to recompile it into the MicroPython firmware. This is a rather complicated process, which is why this guide will focus on the second alternative. If you are however interested in this approach, you can read more about it on these webpages:                https://docs.micropython.org/en/latest/develop/cmodules.html                      https://www.raspberrypi.org/forums/viewtopic.php?t=300352

The .mpy is a file that stores compiled native machine code and can be linked dynamically. This makes it much more flexible since you don’t need to recompile the entire MicroPython firmware.

Write the extension module

Writing the extension module is the easiest part of extending MicroPython. I will show you how to do it by looking at a simple example. To start, we will create a new directory called factfib. Open a new terminal window, cd into the new directory and create a new file called factfib.c. If you created the directory on a Raspberry Pi under Dekstop, the command looks like this.

cd ~/Desktop/factfib

touch factfib.c

Open factfib.c in your favorite text editor and enter the following code:

#include "py/dynruntime.h

STATIC mp_int_t factorial_helper(mp_int_t x) {
   if (x < 1) {
      return 1;
   }
   return x*factorial_helper(x-1);
}

STATIC mp_obj_t factorial(mp_obj_t x_obj) {
   mp_int_t x = mp_obj_get_int(x_obj);
   mp_int_t rslt = factorial_helper(x);
   return rslt;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

STATIC mp_int_t fibonacci_helper(mp_int_t x) {
   if (x <= 0) {
      return 0;
   }
   else if (x == 1) {
      return 1;
   }
   else {
      return (fibonacci_helper(x-1) + fibonacci_helper(x-2));
   }
}

STATIC mp_obj_t fibonacci(mp_obj_t x_obj) {
   mp_int_t x = mp_obj_get_int(x_obj);
   mp_int_t rslt = fibonacci_helper(x);
   return mp_obj_new_int(rslt);
}

STATIC MP_DEFINE_CONST_FUN_OBJ_1(fibonacci_obj, fibonacci);

mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
   MP_DYNRUNTIME_INIT_ENTRY

   mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));
   mp_store_global(MP_QSTR_fibonacci, MP_OBJ_FROM_PTR(&fibonacci_obj));

   MP_DYNRUNTIME_INIT_EXIT
}

Let me explain the code above: In MicroPython and Python, every variable is an object. Therefore in C we represent them as mp_obj_t. But a C program can only work with C type variables, which is why we need to transform the object into a C type. This is called Marshalling and MicroPython provides functions to extract any C type from an object.

This webpage by Oliver Robson is really helpful for Marshalling https://mpy-c-gen.oliverrobson.tech/. Furthermore you can find Marshalling examples in the MicroPython documentation https://docs.micropython.org/en/latest/develop/natmod.html. I advise you to read this article if you need further explanation.

Build the extension

We build the extension into a .mpy file by using a Makefile. But we need to set up a few things first. Make sure your terminal is still opened in the factfib directory. Then clone the MicroPython GitHub repository.

sudo apt-get install build-essential libreadline-dev libffi-dev git pkg-config gcc-arm-none-eabi libnewlib-arm-none-eabi

git clone --recurse-submodules https://github.com/micropython/micropython.git

pip3 install 'pyelftools>=0.25'

These installations will take a while, if you want to save time and you know what you are doing, you only need to clone the /py and /tools directory from the micropython repository and put them inside a micropython folder inside the factfib folder (skip the second command).

While the installation is running we can use the time to write the Makefile. Simply execute

touch Makefile

and open the newly created file in a text editor.

MPY_DIR = micropython

MOD = factfib

SRC = factfib.c

ARCH = armv7m

include $(MPY_DIR)/py/dynruntime.mk

You might have noticed that we set the ARCH variable to armv7m although the pico is armv6m. We have to do this because the armv6m architecture is (at the moment I’m writing this article) not supported by the dynruntime.mk tool.

But luckily we can still compile a working extension for the Pico. We just need to modify the dynruntime.mk file, which should be located under micropython/py/dynruntime.mk. Open it and under Architecture configuration in armv7m find the CFLAGS variable (should be line 64). Change -mcpu=cortex-m3 to -mcpu=cortex-m0 and save the file.

chnge -mcpu=cortex-m3 to -mcpu=cortex-m0

Then after everything is installed execute

make

and the factfib.mpy file will be created.

Install and test the extension

The last step is installing and testing. For this we utilize the filesystem that Micropython creates on the Raspberry Pi Pico. First we need to flash the Pico with the MicroPython firmware. the easiest way to do this is to use the Thonny IDE (preinstalled on Raspbian/Raspberry Pi OS).

If you are not using a Raspberry Pi to programm your Pico or if you never used MicroPython on the Pico before then please refer to the getting started with MicroPython guide by Raspberry Pi https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-micropython

If you are using Windows you should check out this blog Post about the Raspberry Pi Pico and MicroPython on Windows https://picockpit.com/raspberry-pi/raspberry-pi-pico-and-micropython-on-windows/

Close Thonny after you have flashed your Pico with the MicroPython firmware and install rshell. We use rshell to access the filesystem from the command line.

sudo pip3 install rshell
rshell --version

This prints the rhsell version if everything is set up correctly. Then unplug the pico and plug it in again (without holding BOOTSEL button) to make sure nothing is trying to access the file system. Now we need to execute

rshell -p /dev/ttyACM0 --buffer-size 512

cp factfib.mpy /pyboard/factfib.mpy

To test the installation you could use either rshell or the Thonny IDE to save your .py files on the Pico but I will demonstrate how to use the MicroPython interpreter over the minicom.

So first press Ctrl+c to leave rshell and then install minicom via

sudo apt install minicom

now we can access the Picos REPL (Read-eval-print loop)

minicom -o -D /dev/ttyACM0

The three closing angle brackets >>> indicate that the MicroPython interpreter is running. Now we can import our extension and work with it as usual.

>>> import factfib as ff

>>> ff.factorial(5)

>>> 120

>>> ff.fibonacci(7)

>>> 13

Congratulations! You have created your first MicroPython C extension for the Pico! The next step is to learn how to write and compile more complex modules. A good way to start is by testing how the Marshalling of lists and dicts works (see the Marshalling webpage from above).

If you have any questions regarding this post, feel free to write a comment.

8 Comments

  1. PeterB on November 26, 2021 at 8:29 am

    Apart from the error in the c program it worked a treat. Thanks.

    STATIC mp_obj_t factorial(mp_obj_t x_obj) {
    mp_int_t x = mp_obj_get_int(x_obj);
    mp_int_t rslt = factorial_helper(x);
    return mp_obj_new_int(rslt);
    }

    • raspi berry on November 27, 2021 at 5:56 pm

      Thank you for your feedback 🙂

  2. XMC.pl on June 1, 2022 at 3:31 am

    How to configure joomla that can retrieve the data from mysql?

  3. Lynggaard on January 8, 2023 at 8:09 pm

    I tried to do the above, but various things dont work out as described:

    – I followed the instructions above, got micropython, modified the makefile as described
    – I do make (but I get a factfib.native.mpy file instead of factfib.mpy)
    – I copy the factfib.native.mpy file to the pico as factfib.mpy
    (I tried rshell but I got a “timed out or error in transfer to remote: b””, but I could copy it over using Thonny)
    – I can import the module (in thonny or rshell – the same) without errors
    – I try and run the “ff.fibonacci(5)” but then I get:

    “Traceback (most recent call last):
    File “”, line 1, in
    AttributeError: ‘module’ object has no attribute ‘fibonacci'”

    What do I do wrong?

    • paopao69 on March 22, 2023 at 5:59 pm

      Did you eventually get it to work?

  4. Paulo on February 9, 2024 at 1:44 am

    Now you can compile to armv6 so just change this line in the Makefile and no need to change the micropython code
    ARCH = armv6m

    I had a few errors compiling the factfib.c, where are the fixes:

    // missing the ” in the end.
    #include “py/dynruntime.h”

    // convert the return value to mp_obj_t
    STATIC mp_obj_t factorial(mp_obj_t x_obj) {
    mp_int_t x = mp_obj_get_int(x_obj);
    mp_int_t rslt = factorial_helper(x);
    return mp_obj_new_int(rslt);
    }

    • Adam on February 9, 2024 at 9:22 am

      Thank you for the information, Paulo!

  5. Dr_Phil on May 24, 2024 at 7:36 am

    I followed all the instruction and ended up with a built facfib.mpy file. I used
    ARCH = armv6m
    When loaded into the Pico I get this error:

    ValueError: incompatible .mpy file

    My Pico requires the following mpy values:
    mpy version: 6
    mpy sub-version: 2
    mpy flags: -march=armv6m

    Checking the first two bytes of the file give version 6 and sub-version 6.
    Is there a way to correct this please?

Leave a Comment