Portage de MicroPython

Le projet MicroPython contient plusieurs portages vers différentes familles de microcontrôleurs et architectures. Le dépôt du projet possède un répertoire ports contenant un sous-répertoire pour chaque portage pris en charge.

Un portage contient généralement des définitions pour plusieurs « cartes », chacune correspondant à un matériel spécifique sur lequel ce portage peut s’exécuter, par exemple un kit de développement ou un appareil.

Le portage minimal est disponible comme implémentation de référence simplifiée d’un portage MicroPython. Il peut s’exécuter à la fois sur le système hôte et sur un microcontrôleur STM32F4xx.

En général, démarrer un portage nécessite :

  • de mettre en place la chaîne d’outils (configurer les Makefiles, etc.).

  • d’implémenter la configuration de démarrage et l’initialisation du processeur.

  • d’initialiser les pilotes de base requis pour le développement et le débogage (par exemple GPIO, UART).

  • d’effectuer les configurations spécifiques à la carte.

  • d’implémenter les modules spécifiques au portage.

Micrologiciel MicroPython minimal

La meilleure façon de commencer à porter MicroPython vers une nouvelle carte est d’intégrer un interpréteur MicroPython minimal. Pour ce guide pas à pas, créez un sous-répertoire pour le nouveau portage dans le répertoire ports :

$ cd ports
$ mkdir example_port

Le micrologiciel MicroPython de base est implémenté dans le fichier principal du portage, par exemple main.c :

#include "py/builtin.h"
#include "py/compile.h"
#include "py/gc.h"
#include "py/mperrno.h"
#include "shared/runtime/gchelper.h"
#include "shared/runtime/pyexec.h"

// Allocate memory for the MicroPython GC heap.
static char heap[4096];

int main(int argc, char **argv) {
    // Initialise the MicroPython runtime.
    mp_cstack_init_with_sp_here(2048);
    gc_init(heap, heap + sizeof(heap));
    mp_init();

    // Start a normal REPL; will exit when ctrl-D is entered on a blank line.
    pyexec_friendly_repl();

    // Deinitialise the runtime.
    gc_sweep_all();
    mp_deinit();
    return 0;
}

// Handle uncaught exceptions (should never be reached in a correct C implementation).
void nlr_jump_fail(void *val) {
    for (;;) {
    }
}

// Do a garbage collection cycle.
void gc_collect(void) {
    gc_collect_start();
    gc_helper_collect_regs_and_stack();
    gc_collect_end();
}

// There is no filesystem so stat'ing returns nothing.
mp_import_stat_t mp_import_stat(const char *path) {
    return MP_IMPORT_STAT_NO_EXIST;
}

// There is no filesystem so opening a file raises an exception.
mp_lexer_t *mp_lexer_new_from_file(qstr filename) {
    mp_raise_OSError(MP_ENOENT);
}

À ce stade, nous avons aussi besoin d’un Makefile pour le portage :

# Include the core environment definitions; this will set $(TOP).
include ../../py/mkenv.mk

# Include py core make definitions.
include $(TOP)/py/py.mk
include $(TOP)/extmod/extmod.mk

# Set CFLAGS and libraries.
CFLAGS += -I. -I$(BUILD) -I$(TOP)
LIBS += -lm

# Define the required source files.
SRC_C = \
    main.c \
    mphalport.c \
    shared/readline/readline.c \
    shared/runtime/gchelper_generic.c \
    shared/runtime/pyexec.c \
    shared/runtime/stdout_helpers.c \

# Define source files containing qstrs.
SRC_QSTR += shared/readline/readline.c shared/runtime/pyexec.c

# Define the required object files.
OBJ = $(PY_CORE_O) $(addprefix $(BUILD)/, $(SRC_C:.c=.o))

# Define the top-level target, the main firmware.
all: $(BUILD)/firmware.elf

# Define how to build the firmware.
$(BUILD)/firmware.elf: $(OBJ)
    $(ECHO) "LINK $@"
    $(Q)$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
    $(Q)$(SIZE) $@

# Include remaining core make rules.
include $(TOP)/py/mkrules.mk

N’oubliez pas d’utiliser de vraies tabulations pour indenter le Makefile.

Configurations de MicroPython

Après avoir intégré le code minimal ci-dessus, l’étape suivante consiste à créer les fichiers de configuration MicroPython pour le portage. Les configurations à la compilation sont spécifiées dans mpconfigport.h et les fonctions supplémentaires d’abstraction matérielle, comme la gestion du temps, dans mphalport.h.

Voici un exemple de fichier mpconfigport.h :

#include <stdint.h>

// Python internal features.
#define MICROPY_ENABLE_GC                       (1)
#define MICROPY_HELPER_REPL                     (1)
#define MICROPY_ERROR_REPORTING                 (MICROPY_ERROR_REPORTING_TERSE)
#define MICROPY_FLOAT_IMPL                      (MICROPY_FLOAT_IMPL_FLOAT)

// Fine control over Python builtins, classes, modules, etc.
#define MICROPY_PY_ASYNC_AWAIT                  (0)
#define MICROPY_PY_BUILTINS_SET                 (0)
#define MICROPY_PY_ATTRTUPLE                    (0)
#define MICROPY_PY_COLLECTIONS                  (0)
#define MICROPY_PY_MATH                         (0)
#define MICROPY_PY_IO                           (0)
#define MICROPY_PY_STRUCT                       (0)

// Type definitions for the specific machine.

typedef long mp_off_t;

// We need to provide a declaration/definition of alloca().
#include <alloca.h>

// Define the port's name and hardware.
#define MICROPY_HW_BOARD_NAME "example-board"
#define MICROPY_HW_MCU_NAME   "unknown-cpu"

#define MP_STATE_PORT MP_STATE_VM

Ce fichier de configuration contient des configurations spécifiques à la machine, incluant des aspects comme l’activation ou non de différentes fonctionnalités de MicroPython, par exemple #define MICROPY_ENABLE_GC (1). Définir cette valeur à (0) désactive la fonctionnalité.

D’autres configurations incluent les définitions de types, les pointeurs racines, le nom de la carte, le nom du microcontrôleur, etc.

De même, un exemple minimal de fichier mphalport.h ressemble à ceci :

static inline void mp_hal_set_interrupt_char(char c) {}

Prise en charge de l’entrée/sortie standard

MicroPython requiert au minimum un moyen de produire des caractères en sortie, et pour disposer d’un REPL il lui faut aussi un moyen de saisir des caractères. Les fonctions correspondantes peuvent être implémentées dans le fichier mphalport.c, par exemple :

#include <unistd.h>
#include "py/mpconfig.h"

// Receive single character, blocking until one is available.
int mp_hal_stdin_rx_chr(void) {
    unsigned char c = 0;
    int r = read(STDIN_FILENO, &c, 1);
    (void)r;
    return c;
}

// Send the string of given length.
void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) {
    int r = write(STDOUT_FILENO, str, len);
    (void)r;
}

Ces fonctions d’entrée et de sortie doivent être modifiées en fonction de l’API spécifique de la carte. Cet exemple utilise le flux d’entrée/sortie standard.

Construction et exécution

À ce stade, le répertoire du nouveau portage devrait contenir

ports/example_port/
├── main.c
├── Makefile
├── mpconfigport.h
├── mphalport.c
└── mphalport.h

Le portage peut maintenant être construit en exécutant make (ou autrement, selon votre système).

Si vous utilisez les paramètres de compilateur par défaut dans le Makefile donné ci-dessus, cela créera un exécutable nommé build/firmware.elf qui peut être exécuté directement. Pour obtenir un REPL fonctionnel, vous devrez peut-être d’abord configurer le terminal en mode brut :

$ stty raw opost -echo
$ ./build/firmware.elf

Cela devrait vous donner un REPL MicroPython. Vous pouvez alors exécuter des commandes comme :

MicroPython v1.26.0-preview on 2025-08-01; minimal with unknown-cpu
>>> def sum(n, m):
...     return n + m
...
>>> 3, 4, sum(3, 4)
(3, 4, 7)
>>>

Utilisez Ctrl-D pour quitter, puis exécutez reset pour réinitialiser le terminal.

Ajouter un module au portage

Pour ajouter un module personnalisé comme myport, ajoutez d’abord la définition du module dans un fichier modmyport.c :

#include "py/runtime.h"

static mp_obj_t myport_info(void) {
    mp_printf(&mp_plat_print, "info about my port\n");
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_0(myport_info_obj, myport_info);

static const mp_rom_map_elem_t myport_module_globals_table[] = {
    { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_myport) },
    { MP_ROM_QSTR(MP_QSTR_info), MP_ROM_PTR(&myport_info_obj) },
};
static MP_DEFINE_CONST_DICT(myport_module_globals, myport_module_globals_table);

const mp_obj_module_t myport_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&myport_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_myport, myport_module);

Vous devrez également modifier le Makefile pour ajouter modmyport.c à la liste SRC_C, ainsi qu’une nouvelle ligne ajoutant le même fichier à SRC_QSTR (afin que les qstrs soient recherchés dans ce nouveau fichier), comme ceci :

SRC_C = \
    main.c \
    modmyport.c \
    mphalport.c \
    ...

SRC_QSTR += modmyport.c

Si tout s’est déroulé correctement, après reconstruction, vous devriez pouvoir importer le nouveau module :

>>> import myport
>>> myport.info()
info about my port
>>>