Portando o MicroPython

O projeto MicroPython contém várias portas para diferentes famílias de microcontroladores e arquiteturas. O repositório do projeto possui um diretório ports contendo um subdiretório para cada porta suportada.

Uma porta normalmente conterá definições para várias “placas”, cada uma das quais é uma peça específica de hardware na qual essa porta pode rodar, por exemplo, um kit de desenvolvimento ou dispositivo.

A porta mínima está disponível como uma implementação de referência simplificada de uma porta do MicroPython. Ela pode rodar tanto no sistema host quanto em um MCU STM32F4xx.

Em geral, iniciar uma porta requer:

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

  • Implementar a configuração de boot e a inicialização da CPU.

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

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

  • Implementar os módulos específicos da porta.

Firmware mínimo do MicroPython

A melhor maneira de começar a portar o MicroPython para uma nova placa é integrando um interpretador MicroPython mínimo. Para este passo a passo, crie um subdiretório para a nova porta no diretório ports:

$ cd ports
$ mkdir example_port

O firmware básico do MicroPython é implementado no arquivo principal da porta, 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 a porta:

# 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 adequadas para indentar o Makefile.

Configurações do MicroPython

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

A seguir está um exemplo de um arquivo 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 arquivo de configuração contém configurações específicas da máquina, incluindo aspectos como se diferentes recursos do MicroPython devem ser habilitados, ex. #define MICROPY_ENABLE_GC (1). Definir isso como (0) desabilita o recurso.

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

De forma semelhante, um exemplo mínimo de arquivo mphalport.h se parece com isto:

static inline void mp_hal_set_interrupt_char(char c) {}

Suporte a entrada/saída padrão

O MicroPython requer pelo menos uma forma de gerar caracteres na saída e, para ter um REPL, também requer uma forma de receber caracteres na entrada. As funções para isso podem ser implementadas no arquivo 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;
}

Essas funções de entrada e saída precisam ser modificadas dependendo da API específica da placa. Este exemplo usa o fluxo de entrada/saída padrão.

Compilando e executando

Neste estágio, o diretório da nova porta deve conter:

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

A porta agora pode ser compilada executando make (ou de outra forma, dependendo do seu sistema).

Se você estiver usando as configurações padrão do compilador no Makefile fornecido acima, então isso criará um executável chamado build/firmware.elf que pode ser executado diretamente. Para obter um REPL funcional, talvez você precise primeiro configurar o terminal para o modo raw:

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

Isso deve fornecer um REPL do MicroPython. Você 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 então execute reset para reiniciar o terminal.

Adicionando um módulo à porta

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

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

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

SRC_QSTR += modmyport.c

Se tudo correu corretamente, então, após recompilar, você deverá conseguir importar o novo módulo:

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