Portabilização do MicroPython

O projecto MicroPython contém vários portos para diferentes famílias de microcontroladores e arquitecturas. O repositório do projecto tem um directório ports contendo um subdirectório para cada porto suportado.

Um porto irá tipicamente conter definições para múltiplas «placas», cada uma das quais é um hardware específico no qual o porto pode ser executado, p. ex. um kit de desenvolvimento ou dispositivo.

O porto minimal está disponível como uma implementação de referência simplificada de um porto MicroPython. Pode ser executado tanto no sistema anfitrião como num MCU STM32F4xx.

Em geral, iniciar um porto requer:

  • Configurar a cadeia de ferramentas (configurar Makefiles, etc.).

  • Implementar a configuração de arranque e a inicialização do CPU.

  • Inicializar os drivers básicos necessários para o desenvolvimento e depuração (p. ex. GPIO, UART).

  • Efectuar as configurações específicas da placa.

  • Implementar os módulos específicos do porto.

Firmware MicroPython mínimo

A melhor forma de começar a portar o MicroPython para uma nova placa é integrando um interpretador MicroPython mínimo. Para este guia passo a passo, crie um subdirectório para o novo porto no directório ports:

$ cd ports
$ mkdir example_port

O firmware MicroPython básico é implementado no ficheiro principal do porto, p. ex. 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);
}

Também precisamos de um Makefile neste ponto para o porto:

# 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

Lembre-se de usar tabulações correctas para indentar o Makefile.

Configurações do MicroPython

Após integrar o código mínimo acima, o próximo passo é criar os ficheiros de configuração do MicroPython para o porto. As configurações em tempo de compilação são especificadas em mpconfigport.h e funções adicionais de abstracção de hardware, como a gestão do tempo, em mphalport.h.

O seguinte é um exemplo de um ficheiro 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

Este ficheiro de configuração contém configurações específicas da máquina, incluindo aspectos como se diferentes funcionalidades do MicroPython devem ser activadas, p. ex. #define MICROPY_ENABLE_GC (1). Definir isto como (0) desactiva a funcionalidade.

Outras configurações incluem definições de tipos, ponteiros de raiz, nome da placa, nome do microcontrolador, etc.

Do mesmo modo, um ficheiro mphalport.h de exemplo mínimo tem este aspecto:

static inline void mp_hal_set_interrupt_char(char c) {}

Suporte para entrada/saída padrão

O MicroPython requer pelo menos uma forma de enviar caracteres, e para ter um REPL também requer uma forma de receber caracteres. As funções para isso podem ser implementadas no ficheiro mphalport.c, por exemplo:

#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;
}

Estas funções de entrada e saída têm de ser modificadas consoante a API específica da placa. Este exemplo utiliza o fluxo de entrada/saída padrão.

Compilação e execução

Nesta fase, o directório do novo porto deve conter:

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

O porto pode agora ser compilado executando make (ou de outra forma, dependendo do seu sistema).

Se estiver a utilizar as configurações padrão do compilador no Makefile indicado acima, isto criará um executável chamado build/firmware.elf que pode ser executado directamente. Para obter um REPL funcional, poderá ser necessário primeiro configurar o terminal para o modo raw:

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

Isso deverá fornecer um REPL do MicroPython. Pode então executar comandos como:

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)
>>>

Use Ctrl-D para sair e depois execute reset para reiniciar o terminal.

Adicionar um módulo ao porto

Para adicionar um módulo personalizado como myport, primeiro adicione a definição do módulo num ficheiro 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);

Também precisará de editar o Makefile para adicionar modmyport.c à lista SRC_C, e uma nova linha adicionando o mesmo ficheiro a SRC_QSTR (para que os qstrs sejam pesquisados neste novo ficheiro), assim:

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

SRC_QSTR += modmyport.c

Se tudo correu correctamente, após a recompilação, deverá conseguir importar o novo módulo:

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