Developing your first module¶
Having seen, what the python objects look like to the interpreter, we can start with our explorations in earnest. We begin by adding a simple module to micropython. The module will have a single function that takes two numbers, and adds them. I know that this is the most exciting thing since sliced bread, and you have always wondered, why there isn’t a built-in python function for such a fascinating task. Well, wonder no more! From this moment, your micropython will have one.
First I show the file in its entirety (20 something lines all in all), and then discuss the parts.
https://github.com/v923z/micropython-usermod/tree/master/snippets/simplefunction/simplefunction.c
#include "py/obj.h"
#include "py/runtime.h"
STATIC mp_obj_t simplefunction_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
int a = mp_obj_get_int(a_obj);
int b = mp_obj_get_int(b_obj);
return mp_obj_new_int(a + b);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(simplefunction_add_ints_obj, simplefunction_add_ints);
STATIC const mp_rom_map_elem_t simplefunction_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_simplefunction) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&simplefunction_add_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(simplefunction_module_globals, simplefunction_module_globals_table);
const mp_obj_module_t simplefunction_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&simplefunction_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_simplefunction, simplefunction_user_cmodule, MODULE_SIMPLEFUNCTION_ENABLED);
Header files¶
A module will not be too useful without at least two includes:
py/obj.h
, where all the relevant constants and macros are defined,
and py/runtime.h
, which contains the declaration of the interpreter.
Many a time you will also need py/builtin.h
, where the python
built-in functions and modules are declared.
Defining user functions¶
After including the necessary headers, we define the function that is
going to do the heavy lifting. By passing variables of mp_obj_t
type, we make sure that the function will be able to accept values from
the python console. If you happen to have an internal helper function in
your module that is not exposed in python, you can pass whatever type
you need. Similarly, by returning an object of mp_obj_t
type, we
make the results visible to the interpreter, i.e., we can assign the
value returned to variables.
The downside of passing mp_obj_t
s around is that you cannot simply
assign them to usual C variables, i.e., when you want to operate on
them, you have to extract the values first. This is why we have to
invoke the mp_obj_get_int()
function, and conversely, before
returning the results, we have to do a type conversion to mp_obj_t
by calling mp_obj_new_int()
. These are the decoding/encoding steps
that we discussed above.
Referring to user functions¶
We have now a function that should be sort of OK (there is no error checking whatsoever, so you are at the mercy of the firmware, when, e.g., you try to pass a float to the function), but the python interpreter still cannot work with. For that, we have to turn our function into a function object. This is what happens in the line
STATIC MP_DEFINE_CONST_FUN_OBJ_2(simplefunction_add_ints_obj, simplefunction_add_ints);
The first argument of the macro is the name of the function object to
which our actual function, the last argument, will be bound. Now, these
MP_DEFINE_CONST_FUN_OBJ_*
macros, defined in the header file
py/obj.h
(one more reason not to forget about py/obj.h
), come in
seven flavours, depending on what kind of, and how many arguments the
function is supposed to take. In the example above, our function is
meant to take two arguments, hence the 2 at the end of the macro name.
Functions with 0 to 4 arguments can be bound in this way.
But what, if you want a function with more than four arguments, as is the case many a time in python? Under such circumstances, one can make use of the
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(obj_name, n_args_min, fun_name);
macro, where the second argument, an integer, gives the minimum number of arguments. The number of arguments can be bound from above by wrapping the function with
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(obj_name, n_args_min, n_args_max, fun_name);
Later we will see, how we can define functions that can also take keyword arguments.
At this point, we are more or less done with the C implementation of our
function, but we still have to expose it. This we do by adding a table,
an array of key/value pairs to the globals of our module, and bind the
table to the _module_globals
variable by applying the
MP_DEFINE_CONST_DICT
macro. This table should have at least one
entry, the name of the module, which is going to be stored in the string
MP_QSTR___name__
.
These MP_QSTR_
items are the C representation of the python strings
that come at the end of them. So, MP_QSTR_foo_bar
in C will be
turned into a name, foo_bar
, in python. foo_bar
can be a
constant, a function, a class, a type, etc., and depending on what is
associated with it, different things will happen on the console, when
foo_bar
is invoked. But the crucial point is that, if you want
foo_bar
to have any meaning in python, then somewhere in your C
code, you have to define MP_QSTR_foo_bar
.
The second key-value pair of the table is the pointer to the function
that we have just implemented, and the name that we want to call the
functions in python itself. So, in the example below, our
simplefunction_add_ints
function will be invoked, when we call
add_ints
in the console.
STATIC const mp_rom_map_elem_t simplefunction_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_simplefunction) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&simplefunction_add_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(simplefunction_module_globals, simplefunction_module_globals_table);
This three-step pattern is common to all function implementations, so I repeat it here:
- implement the function
- then turn it into a function object (i.e., call the relevant form of MP_DEFINE_CONST_FUN_OBJ_*)
- and finally, register the function in the name space of the module (i.e., add it to the module’s globals table, and turn the table into a dictionary by applying MP_DEFINE_CONST_DICT)
It doesn’t matter, whether our function takes positional arguments, or keyword argument, or both, these are the required steps.
Having defined the function object, we have finally to register the module with
MP_REGISTER_MODULE(MP_QSTR_simplefunction, simplefunction_user_cmodule, MODULE_SIMPLEFUNCTION_ENABLED);
This last line is particularly useful, because by setting the
MODULE_SIMPLEFUNCTION_ENABLED
variable in mpconfigport.h
, you
can selectively exclude modules from the linking, i.e., if in
mpconfigport.h
, which should be in the root directory of the port
you want to compile for,
#define MODULE_SIMPLEFUNCTION_ENABLED (1)
then simplefunction
will be included in the firmware, while with
#define MODULE_SIMPLEFUNCTION_ENABLED (0)
the module will be dropped, even though the source is in your modules folder. (N.B.: the module will still be compiled, but not linked.)
Compiling our module¶
The implementation is done, and we would certainly like to see some
results. First we generate a makefile, which will be inserted in the
module’s own directory, simplefunction/
.
https://github.com/v923z/micropython-usermod/tree/master/snippets/simplefunction/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/simplefunction.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
If mpconfigport.h
is augmented with
#define MODULE_SIMPLEFUNCTION_ENABLED (1)
you should be able to compile the module above by calling
!make clean
!make USER_C_MODULES=../../../usermod/snippets all
As mentioned earlier, if you do not want to touch anything in the micropython code base, you can simply pass the definition to make as
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SIMPLEFUNCTION_ENABLED=1 all
You will also note that we ran make clean
before the compilation.
This is always good practice, when you are developing your own modules.
We can then test the module as
%%micropython
import simplefunction
print(simplefunction.add_ints(123, 456))
579
What a surprise! It works! It works!
Compiling for the microcontroller¶
As pointed out at the very beginning, our first module was compiled for
the unix port, and that it, why we set ../../micropython/ports/unix/
as our working directory. In case, we would like to compile for the
microcontroller, we would have to modify the mpconfigport.h
file of
the port (e.g., in micropython/ports/stm32/
) as shown in Section
User modules.
Next, in the compilation command, one has to specify the target board,
e.g., pyboard, version 1.1, and probably the path to the cross-compiler,
if that could not be installed system-wide. You would issue the make
command in the directory of the port, e.g.,
micropython/ports/stm32/
, and the path in the CROSS_COMPILE
argument must be either absolute, or given relative to
micropython/ports/stm32/
.
make BOARD=PYBV11 CROSS_COMPILE=<Path where you uncompressed the toolchain>/bin/arm-none-eabi-
You will find your firmware under
micropython/ports/stm32/build-PYBV11/firmware.dfu
, and you can
upload it by issuing
!python ../../tools/pydfu.py -u build-PYBV11/firmware.dfu
on the command line. More detailed explanation can be found under https://github.com/micropython/micropython/wiki/Pyboard-Firmware-Update.