Портування MicroPython

Проект MicroPython містить кілька портів для різних сімейств мікроконтролерів та архітектур. Репозиторій проекту має каталог ports, що містить підкаталог для кожного підтримуваного порту.

Порт, як правило, містить визначення для кількох «плат», кожна з яких є конкретним фізичним пристроєм, на якому може працювати цей порт, наприклад, налагоджувальний комплект або пристрій.

Мінімальний порт доступний як спрощена еталонна реалізація порту MicroPython. Він може працювати як на хост-системі, так і на MCU STM32F4xx.

Загалом, початок роботи над портом передбачає:

  • Налаштування ланцюжка інструментів (конфігурація Makefile тощо).

  • Реалізацію конфігурації завантаження та ініціалізації CPU.

  • Ініціалізацію базових драйверів, необхідних для розробки та налагодження (наприклад, GPIO, UART).

  • Виконання специфічних для плати конфігурацій.

  • Реалізацію специфічних для порту модулів.

Мінімальна мікропрограма MicroPython

Найкращий спосіб розпочати портування MicroPython на нову плату — це інтегрувати мінімальний інтерпретатор MicroPython. Для цього прикладу створіть підкаталог для нового порту в каталозі ports:

$ cd ports
$ mkdir example_port

Базова мікропрограма MicroPython реалізована в основному файлі порту, наприклад, 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);
}

На цьому етапі також необхідний Makefile для порту:

# 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

Пам’ятайте використовувати символи табуляції для відступів у Makefile.

Конфігурації MicroPython

Після інтеграції мінімального коду, описаного вище, наступний крок — створення конфігураційних файлів MicroPython для порту. Конфігурації часу компіляції вказуються у файлі mpconfigport.h, а додаткові функції апаратної абстракції, наприклад, відстеження часу, — у mphalport.h.

Нижче наведено приклад файлу 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

Цей файл конфігурації містить специфічні для машини налаштування, включаючи, чи мають бути ввімкнені різні функції MicroPython, наприклад, #define MICROPY_ENABLE_GC (1). Встановлення значення (0) вимикає цю функцію.

Інші конфігурації включають визначення типів, кореневі вказівники, назву плати, назву мікроконтролера тощо.

Аналогічно, мінімальний приклад файлу mphalport.h виглядає так:

static inline void mp_hal_set_interrupt_char(char c) {}

Підтримка стандартного введення/виведення

MicroPython потребує принаймні можливості виводити символи, а для наявності REPL — також можливості вводити символи. Функції для цього можна реалізувати у файлі mphalport.c, наприклад:

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

Ці функції введення та виведення потрібно адаптувати залежно від конкретного API плати. У цьому прикладі використовується стандартний потік введення/виведення.

Збирання та запуск

На цьому етапі каталог нового порту повинен містити:

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

Тепер порт можна зібрати, виконавши make (або іншим способом, залежно від вашої системи).

Якщо ви використовуєте стандартні налаштування компілятора у наведеному вище Makefile, буде створено виконуваний файл build/firmware.elf, який можна запустити безпосередньо. Щоб отримати функціональний REPL, можливо, спочатку потрібно налаштувати термінал у режим raw:

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

Це має забезпечити REPL MicroPython. Після цього можна виконувати команди, наприклад:

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

Використовуйте Ctrl-D для виходу, а потім виконайте reset для скидання налаштувань терміналу.

Додавання модуля до порту

Щоб додати власний модуль, наприклад myport, спочатку додайте визначення модуля у файл 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);

Також необхідно відредагувати Makefile, щоб додати modmyport.c до списку SRC_C та новий рядок, що додає той самий файл до SRC_QSTR (щоб у цьому новому файлі виконувався пошук qstrs), ось так:

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

SRC_QSTR += modmyport.c

Якщо все пройшло успішно, то після перезбирання ви зможете імпортувати новий модуль:

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