移植 MicroPython¶
MicroPython 项目包含多个不同微控制器系列和架构的端口。项目存储库有一个端口 目录,其中包含每个受支持端口的子目录。
一个端口通常会包含多个“板”的定义,每个“板”都是该端口可以运行的特定硬件,例如开发套件或设备。
这 最小端口可以作为一个简化的参考实现一个MicroPython端口。它可以在主机系统和 STM32F4xx MCU 上运行。
一般来说,启动一个端口需要:
设置工具链(配置 Makefile 等)。
实现引导配置和 CPU 初始化。
初始化开发和调试所需的基本驱动程序(例如 GPIO、UART)。
执行特定于板的配置。
实现特定于端口的模块。
最小的 MicroPython 固件¶
开始将 MicroPython 移植到新板的最佳方法是集成一个最小的 MicroPython 解释器。对于本演练,在目录中为新端口创建一个子目录ports
:
$ cd ports
$ mkdir example_port
基本的 MicroPython 固件在主端口文件中实现,例如 main.c
:
#include "py/compile.h"
#include "py/gc.h"
#include "py/mperrno.h"
#include "py/stackctrl.h"
#include "lib/utils/gchelper.h"
#include "lib/utils/pyexec.h"
// Allocate memory for the MicroPython GC heap.
static char heap[4096];
int main(int argc, char **argv) {
// Initialise the MicroPython runtime.
mp_stack_ctrl_init();
gc_init(heap, heap + sizeof(heap));
mp_init();
mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_path), 0);
mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_argv), 0);
// 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(const char *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
# Set CFLAGS and libraries.
CFLAGS = -I. -I$(BUILD) -I$(TOP)
LIBS = -lm
# Define the required source files.
SRC_C = \
main.c \
mphalport.c \
lib/mp-readline/readline.c \
lib/utils/gchelper_generic.c \
lib/utils/pyexec.c \
lib/utils/stdout_helpers.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 intptr_t mp_int_t; // must be pointer size
typedef uintptr_t mp_uint_t; // must be pointer size
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
#define MICROPY_PORT_ROOT_POINTERS \
const char *readline_hist[8];
此配置文件包含特定于机器的配置,包括是否应启用不同的 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,您可能需要先将终端配置为原始模式:
$ stty raw opost -echo
$ ./build/firmware.elf
那应该给出一个 MicroPython REPL。然后,您可以运行以下命令:
MicroPython v1.13 on 2021-01-01; example-board with unknown-cpu
>>> import usys
>>> usys.implementation
('micropython', (1, 13, 0))
>>>
使用 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, 1);
注意:作为第三个参数的“1”MP_REGISTER_MODULE
无条件启用这个新模块。要允许其有条件地启用,取代了“1”
MICROPY_PY_MYPORT
,然后添加在 相应。#define MICROPY_PY_MYPORT (1)
mpconfigport.h
您还需要编辑 Makefile 以添加 modmyport.c
到 SRC_C
列表中,并在新行中添加相同的文件SRC_QSTR
(因此在此新文件中搜索 qstrs),如下所示:
SRC_C = \
main.c \
modmyport.c \
mphalport.c \
...
SRC_QSTR += modport.c
如果一切顺利,那么在重建之后,您应该能够导入新模块:
>>> import myport
>>> myport.info()
info about my port
>>>