Portowanie MicroPython

Projekt MicroPython zawiera kilka portów na różne rodziny mikrokontrolerów i architektury. Repozytorium projektu posiada katalog ports zawierający podkatalog dla każdego obsługiwanego portu.

Port zazwyczaj zawiera definicje wielu „płytek”, z których każda jest konkretnym fragmentem sprzętu, na którym dany port może działać, np. zestaw deweloperski lub urządzenie.

Port minimalny jest dostępny jako uproszczona implementacja referencyjna portu MicroPython. Może działać zarówno w systemie hosta, jak i na mikrokontrolerze STM32F4xx.

Ogólnie rzecz biorąc, rozpoczęcie portu wymaga:

  • Skonfigurowania łańcucha narzędzi (konfigurowanie plików Makefile itp.).

  • Zaimplementowania konfiguracji rozruchu i inicjalizacji CPU.

  • Zainicjalizowania podstawowych sterowników wymaganych do rozwoju i debugowania (np. GPIO, UART).

  • Wykonania konfiguracji specyficznych dla płytki.

  • Zaimplementowania modułów specyficznych dla portu.

Minimalne oprogramowanie układowe MicroPython

Najlepszym sposobem na rozpoczęcie portowania MicroPython na nową płytkę jest zintegrowanie minimalnego interpretera MicroPython. Na potrzeby tego przewodnika utwórz podkatalog dla nowego portu w katalogu ports:

$ cd ports
$ mkdir example_port

Podstawowe oprogramowanie układowe MicroPython jest implementowane w głównym pliku portu, np. 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);
}

Na tym etapie potrzebujemy również pliku Makefile dla portu:

# 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

Pamiętaj, aby do wcięć w pliku Makefile używać właściwych tabulatorów.

Konfiguracje MicroPython

Po zintegrowaniu powyższego minimalnego kodu kolejnym krokiem jest utworzenie plików konfiguracyjnych MicroPython dla portu. Konfiguracje czasu kompilacji są określone w mpconfigport.h, a dodatkowe funkcje abstrakcji sprzętu, takie jak odmierzanie czasu, w mphalport.h.

Poniżej znajduje się przykład pliku 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

Ten plik konfiguracyjny zawiera konfiguracje specyficzne dla maszyny, w tym aspekty takie jak to, czy różne funkcje MicroPython powinny być włączone, np. #define MICROPY_ENABLE_GC (1). Ustawienie tego na (0) wyłącza daną funkcję.

Inne konfiguracje obejmują definicje typów, wskaźniki główne (root pointers), nazwę płytki, nazwę mikrokontrolera itp.

Podobnie minimalny przykładowy plik mphalport.h wygląda tak:

static inline void mp_hal_set_interrupt_char(char c) {}

Obsługa standardowego wejścia/wyjścia

MicroPython wymaga przynajmniej sposobu na wyprowadzanie znaków, a aby mieć REPL, wymaga również sposobu na wprowadzanie znaków. Funkcje do tego można zaimplementować w pliku mphalport.c, na przykład:

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

Te funkcje wejścia i wyjścia muszą zostać zmodyfikowane w zależności od konkretnego API płytki. Ten przykład używa standardowego strumienia wejścia/wyjścia.

Budowanie i uruchamianie

Na tym etapie katalog nowego portu powinien zawierać:

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

Port można teraz zbudować, uruchamiając make (lub w inny sposób, w zależności od Twojego systemu).

Jeśli używasz domyślnych ustawień kompilatora w podanym powyżej pliku Makefile, to utworzy to plik wykonywalny o nazwie build/firmware.elf, który można uruchomić bezpośrednio. Aby uzyskać funkcjonalny REPL, może być konieczne uprzednie skonfigurowanie terminala w tryb surowy (raw):

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

To powinno dać REPL MicroPython. Możesz następnie uruchamiać polecenia takie jak:

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

Użyj Ctrl-D, aby wyjść, a następnie uruchom reset, aby zresetować terminal.

Dodawanie modułu do portu

Aby dodać własny moduł, taki jak myport, najpierw dodaj definicję modułu w pliku 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);

Będziesz musiał również edytować plik Makefile, aby dodać modmyport.c do listy SRC_C oraz nową linię dodającą ten sam plik do SRC_QSTR (aby qstr-y były wyszukiwane w tym nowym pliku), w ten sposób:

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

SRC_QSTR += modmyport.c

Jeśli wszystko poszło prawidłowo, to po ponownym zbudowaniu powinieneś móc zaimportować nowy moduł:

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