نقل MicroPython (Porting)

يحتوي مشروع MicroPython على عدة منافذ (ports) لعائلات وبنى معمارية مختلفة من المتحكمات الدقيقة. يحتوي مستودع المشروع على دليل ports يضم دليلاً فرعياً لكل منفذ مدعوم.

عادةً ما يحتوي المنفذ على تعريفات لعدة "لوحات" (boards)، كل منها قطعة عتاد محددة يمكن لذلك المنفذ العمل عليها، مثل طقم تطوير أو جهاز.

يتوفر المنفذ الأدنى كتنفيذ مرجعي مبسّط لمنفذ MicroPython. يمكنه العمل على كل من النظام المضيف ومتحكم STM32F4xx.

بشكل عام، يتطلب بدء منفذ ما يلي:

  • إعداد سلسلة الأدوات (تكوين ملفات Makefile وما إلى ذلك).

  • تنفيذ تكوين الإقلاع وتهيئة وحدة المعالجة المركزية (CPU).

  • تهيئة المُشغّلات (drivers) الأساسية اللازمة للتطوير وتصحيح الأخطاء (مثل 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

تذكّر استخدام علامات الجدولة (tabs) الصحيحة لإزاحة محتوى ملف 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) يعطّل الميزة.

تشمل التكوينات الأخرى تعريفات الأنواع، والمؤشرات الجذرية (root pointers)، واسم اللوحة، واسم المتحكم الدقيق وما إلى ذلك.

وبالمثل، يبدو مثال أدنى لملف 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 mode):

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