When using MicroPython, you have even bigger chance to need your own library written in C than in other Python implementations. You might want to use one of the existing libraries for your platform, get access to some peripherals or other features of the hardware that is not exposed to Python by default, or even just do something faster and with smaller memory overhead. To do that, you will need to extend the existing firmware with your own C code, and this article is going to show you how to do it, using the ESP8266 port as the example.
In order to add your own MicroPython module written in C, you need to create a new C file and add references to it to several files, so that the file is picked up during the compilation.
First of all, you need to add it to the Makefile
, to the list of source
files in the SRC_C
variable. Make sure to follow the same format as the
other files in there. The order doesn't matter much.
SRC_C = \
main.c \
system_stm32.c \
stm32_it.c \
...
mymodule.c
...
The second file you will need to add to is esp8266.ld
, which is the map of
memory used by the compiler. You have to add it to the list of files to be put
in the .irom0.text
section, so that your code goes into the instruction
read-only memory (iROM). If you fail to do that, the compiler will try to put
it in the instruction random-access memory (iRAM), which is a very scarce
resource, and which can get overflown if you try to put too much there.
Now just create an empty mymodule.c
file, and run the compilation to see
that it is now included in the firmware.
We have our file, but it doesn't actually do anything. It's empty, and there is no new python module that we could import. Time to change that.
From the C side, modules in MicroPython are simply structs with a certain
structure. Open the mymodule.c
file and put this code inside:
#include "py/nlr.h"
#include "py/obj.h"
#include "py/runtime.h"
#include "py/binary.h"
#include "portmodules.h"
STATIC const mp_map_elem_t mymodule_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_mymodule) },
};
STATIC MP_DEFINE_CONST_DICT(mp_module_mymodule_globals, mymodule_globals_table);
const mp_obj_module_t mp_module_mymodule = {
.base = { &mp_type_module },
.name = MP_QSTR_mymodule,
.globals = (mp_obj_dict_t*)&mp_module_mymodule_globals,
};
What does this code do? It just defines a python module, using
mp_obj_module_t
type, and then initializes some of its fields, such as the
base type, the name, and the dictionary of globals for that module. In that
dictionary, it defines one variable, __name__
, with the name of our module
in it. That's it.
Now, for this module to actually be available for import, we need to add it to
mpconfigport.h
file to MICROPY_PORT_BUILTIN_MODULES
:
extern const struct _mp_obj_module_t mp_module_mymodule;
#define MICROPY_PORT_BUILTIN_MODULES \
{ MP_OBJ_NEW_QSTR(MP_QSTR_umachine), (mp_obj_t)&machine_module }, \
...
{ MP_OBJ_NEW_QSTR(MP_QSTR_mymodule), (mp_obj_t)&mp_module_mymodule }, \
Now you can try compiling the firmware and flashing it to your board. Then you
can run import mymodule
and see it imported.
Now let's add a simple function to that module. Edit mymodule.c
again and
add this code right after the includes:
#include <stdio.h>
STATIC mp_obj_t mymodule_hello(void) {
printf("Hello world!\n");
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mymodule_hello_obj, mymodule_hello);
This creates a function object mymodule_hello_obj
which takes no arguments,
and when called, executes the C function mymodule_hello
. Also note, that
our function has to return something -- so we return None
. Now we need to
actually add that function object to our module:
STATIC const mp_map_elem_t mymodule_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_mymodule) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_hello), (mp_obj_t)&mymodule_hello_obj },
};
Now when you compile and flash the firmware, you will be able to import the module and call the function inside it.
The MP_DEFINE_CONST_FUN_OBJ_0
macro that we used to define our function is
a shortcut for defining a function with no arguments. We can also define a
function that takes a single argument with MP_DEFINE_CONST_FUN_OBJ_1
-- the
C function then needs to take an argument of type mp_obj_t
:
STATIC mp_obj_t mymodule_hello(mp_obj_t what) {
printf("Hello %s!\n", mp_obj_str_get_str(what));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mymodule_hello_obj, mymodule_hello);
Note that the mp_obj_str_get_str
function will automatically raise the
right exception on the python side if the argument we gave it is not a python
string. This is very convenient.
It's also possible to define functions with variable number of arguments, or even with keyword arguments -- you can easily find examples of that in the modules already included in MicroPython. I will not be covering this in detail.
Let's try to add a class to our module. A class is similar to a module -- it's also a C struct with certain fields:
STATIC const mp_rom_map_elem_t mymodule_hello_locals_dict_table[] = {
}
STATIC MP_DEFINE_CONST_DICT(mymodule_hello_locals_dict,
mymodule_hello_locals_dict_table);
const mp_obj_type_t mymodule_hello_type = {
{ &mp_type_type },
.name = MP_QSTR_Hello,
.print = mymodule_hello_print,
.make_new = mymodule_hello_make_new,
.locals_dict = (mp_obj_dict_t*)&mymodule_hello_locals_dict,
};
It needs two functions: one for creating the class and allocating all the
memory it needs, and one for printing the objects of that class (similar to
python's __repr__
). Let's add them near the top of our file:
typedef struct _mymodule_hello_obj_t {
mp_obj_base_t base;
uint8_t hello_number;
} mymodule_hello_obj_t;
mp_obj_t mymodule_hello_make_new(const mp_obj_type_t *type, size_t n_args,
size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, true);
pyb_spi_obj_t *self = m_new_obj(mymodule_hello_obj_t);
self->base.type = &mymodule_hello_type;
self->hello_number = mp_obj_get_int(args[0])
return MP_OBJ_FROM_PTR(self);
}
STATIC void pyb_spi_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
pyb_spi_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_printf(print, "Hello(%u)", self->hello_number);
}
We define a struct to hold all our class data, with one additional field
hello_number
. Then we define functions to create and to print it.
Of course you also need to add the class to the module's globals. Compile it and try creating and printing objects of our new class.
Methods in MicroPython are just functions in the class's locals dict. You add them the same way as you do to modules, just remember that the first argument is a pointer to the data struct:
STATIC mp_obj_t mymodule_hello_increment(mp_obj_t self_in) {
pyb_spi_obj_t *self = MP_OBJ_TO_PTR(self_in);
self->hello_number += 1;
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(mymodule_hello_increment_obj,
mymodule_hello_increment);
Also, don't forget to add them to the locals dict:
STATIC const mp_rom_map_elem_t mymodule_hello_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mymodule_hello_increment_obj) },
}
And that's all.