MicroPython 原生 .mpy 模块开发指南:用 C 编写可动态加载的本地机器码模块
【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython
导读
本文基于 MicroPython 官方开发文档《Native machine code in .mpy files》,系统讲解如何用 C(或其他可编译为独立机器码的语言)编写、编译并链接生成包含原生机器码的.mpy文件,使其可以被 MicroPython 像普通 Python 模块一样动态import,而无需重新编译整个固件。读完本文,你将掌握mpy_ld.py链接工具与py/dynruntime.h动态运行时 API 的使用方法、各目标架构的选取与限制、可链接的运行时库策略,并能从零搭建一个可复现的 C 原生模块工程(如官方examples/natmod/下的factorial示例)。
为什么需要原生 .mpy 模块
MicroPython 的.mpy文件是一种预编译代码的二进制容器格式,可以通过import foo像普通.py模块一样被导入(关于格式细节见 MicroPython .mpy 文件说明)。绝大多数.mpy文件由 Python 源码经mpy-cross编译为字节码,但对于部分架构,.mpy文件还可以携带原生机器码,最典型的来源就是 C 源码。
原生 .mpy 模块的核心价值在于:
- 动态加载,无需重建固件:原生机器码可以在运行时被脚本动态导入,这是它与 C 模块(cmodules) 的本质区别——C 模块必须被编译进固件镜像,而原生 .mpy 模块像普通 Python 文件一样按需部署。
- 性能关键代码用 C 实现:适合实现计算密集、延迟敏感的功能。
- 复用既有 C 库:可以把现成的第三方 C 库打包进
.mpy文件直接使用。
需要强调的边界是:架构绑定。原生 .mpy 文件带有特定的目标架构标识,编译出的文件只能在该架构(且在携带架构标志时必须与之匹配)上导入。相比之下,纯字节码.mpy是可移植的。
构建工具链与工作流程
核心工具:mpy_ld.py
原生 .mpy 模块的构建核心是mpy_ld.py,位于仓库 tools/mpy_ld.py。它接收一组目标文件(.o),将其链接为原生.mpy文件。其前置依赖为:
- CPython 3
- pyelftools 库 v0.25 或更高,安装方式如
pip install 'pyelftools>=0.25'
从 Makefile 片段看构建流程
官方在 py/dynruntime.mk 中提供了完整的构建规则,一个原生模块的构建大致分四步:
- 预处理:
mpy_ld.py --arch $(ARCH) --preprocess对源文件做预处理,生成模块配置文件$(MOD).config.h。 - 编译:用目标架构的交叉编译器把
.c/.S源文件编译为位置无关代码(PIC)目标文件,编译参数由CFLAGS_ARCH提供(含-fpic -fno-common)。 - 编译 Python 部分:
.py源文件由mpy-cross以-march=$(ARCH)编译为字节码.mpy。 - 链接合并:
mpy_ld.py --arch $(ARCH) --qstrs $(CONFIG_H)把目标文件链接为原生.mpy,再经mpy-tool.py --merge与字节码部分合并为最终模块文件。
构建产物默认为$(MOD).mpy,构建中间目录默认为build-$(ARCH),工具链前缀(CROSS)、浮点实现(MICROPY_FLOAT_IMPL)等都由ARCH决定。
支持的架构与 ARCH 变量
ARCH变量是 Makefile 中必须正确设置的核心配置,它同时决定了交叉编译器前缀、编译参数、浮点实现与可导入的目标平台。当前仓库支持的合法取值如下(对照 py/dynruntime.mk 中ARCH分支):
| ARCH | 含义 | 典型目标 | 交叉编译器前缀 | 默认浮点实现 |
|---|---|---|---|---|
x86 | 32 位 x86 | 32 位 Linux 主机 | i686-linux-gnu- | double |
x64 | 64 位 x86 | 64 位主机 | x86_64-linux-gnu- | double |
armv6m | ARM Thumb(Cortex-M0) | STM32F0 等 | arm-none-eabi- | float |
armv7m | ARM Thumb 2(Cortex-M3) | STM32F1/F2/F4 等 | arm-none-eabi- | float |
armv7emsp | ARM Thumb 2,单精度浮点(Cortex-M4F、Cortex-M7) | STM32F4/F7 等 | arm-none-eabi- | float |
armv7emdp | ARM Thumb 2,双精度浮点(Cortex-M7) | 带双精度 FPU 的器件 | arm-none-eabi- | double |
xtensa | 非窗口化 Xtensa | ESP8266 | xtensa-lx106-elf- | none |
xtensawin | 窗口化 Xtensa(窗口大小 8) | ESP32、ESP32S3 | xtensa-esp32-elf- | float |
rv32imc | RISC-V 32 位带压缩指令 | ESP32C3、ESP32C6 | riscv64-unknown-elf- | none |
rv64imc | RISC-V 64 位带压缩指令 | RISC-V 64 器件 | riscv64-unknown-elf- | none |
架构标志 ARCH_FLAGS
部分平台支持显式架构标志。若希望输出.mpy文件携带这些标志的取值(例如 RISC-V 处理器扩展),构建时必须通过ARCH_FLAGS变量传给mpy_ld.py:
$ make ARCH=rv32imc ARCH_FLAGS=zba在 py/dynruntime.mk 中可以看到,ARCH_FLAGS会被转换成MPY_LD_FLAGS += --arch-flags "$(ARCH_FLAGS)";相应地,tools/mpy_ld.py 的--arch-flags选项负责把该值写入.mpy文件头部。.mpy头部第 3 字节的 bit #6 用于标记其后是否跟随一个架构专属标志 vuint(详见 MicroPython .mpy 文件说明 的"Architecture-specific flags"一节)。目前该机制主要用于记录 RISC-V 需要的除 I、M、C、Zicsr 之外的处理器扩展;RV32/RV64 的模块若无需特殊扩展,可省略标志(省 1 字节)。导入时若架构标志与目标不兼容,会抛出ValueError('incompatible .mpy arch')。
链接器与动态加载器:能力与限制
支持的重定位特性
原生代码必须以位置无关代码(PIC)编译并使用全局偏移表(GOT)。导入含原生代码的.mpy时,导入机制会执行基本重定位,支持:
- 可执行代码(text)
- 只读数据(rodata),包括字符串与常量数据(数组、结构体等)
- 清零数据(BSS)
- text 中指向 text、rodata、BSS 的指针
- rodata 中指向 text、rodata、BSS 的指针
已知限制与规避方法
| 限制 | 规避方法 |
|---|---|
| 不支持 data 段(已初始化数据) | 改用 BSS 数据并在函数内显式初始化 |
| 不支持静态 BSS 变量 | 改用全局 BSS 变量 |
| rv32imc 不支持线程局部存储(TLS)变量 | 改用全局 BSS 变量或在堆上分配存储 |
因此,C 代码中的可写数据应全局定义、不带初始化器、只在函数内写入。
运行时库链接
原生模块默认不会自动链接标准静态库(如libm.a、libgcc.a),可能导致undefined symbol错误。解决办法:
- 在 Makefile 中设置
LINK_RUNTIME = 1链接运行时库(实际是把 libgcc、libm/libc 通过MPY_LD_FLAGS += -l <path>传入链接器,见 py/dynruntime.mk)。 - 自定义静态库通过
MPY_LD_FLAGS += -l path/to/library.a追加。
注意这些库是链接进原生模块本身的,不会与其他模块或系统共享。
符号表边界:mp_fun_table
原生模块并不链接整个固件的符号表,而是链接到一张显式导出的符号表mp_fun_table(定义于 py/nativeglue.h),该表在固件构建时固定。因此,不能随意调用任意的 HAL/OS/RTOS/系统函数,除非它位于固定地址。对于固定地址符号,可通过mpy_ld.py的--externs命令行参数传入包含符号名与固定地址的链接脚本,例如 ESP8266 端口的 ROM 符号表 ports/esp8266/boards/eagle.rom.addr.v6.ld。链接脚本中出现的符号会优先于目标文件中的实现,但目前目标文件中的实现仍会保留在最终.mpy中。链接脚本解析器能力有限,目前仅用于 ESP8266 ROM 符号表。
如需向mp_fun_table添加新符号,需要三步:
- 在表末尾追加新符号并重建固件;
- 在 tools/mpy_ld.py 的
fun_table字典同一位置添加同名符号,使mpy_ld.py能为其生成导入时的重定位; - 若该符号是函数,在 py/dynruntime.h 中添加宏或桩函数,方便调用。
编写第一个原生模块:factorial 完整实战
下面从零实现官方文档中的factorial模块。目录结构:
factorial/ ├── factorial.c └── MakefileC 源文件 factorial.c
// Include the header file to get access to the MicroPython API #include "py/dynruntime.h" // Helper function to compute factorial static mp_int_t factorial_helper(mp_int_t x) { if (x == 0) { return 1; } return x * factorial_helper(x - 1); } // This is the function which will be called from Python, as factorial(x) static mp_obj_t factorial(mp_obj_t x_obj) { // Extract the integer from the MicroPython input object mp_int_t x = mp_obj_get_int(x_obj); // Calculate the factorial mp_int_t result = factorial_helper(x); // Convert the result to a MicroPython integer object and return it return mp_obj_new_int(result); } // Define a Python reference to the function above static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial); // This is the entry point and is called when the module is imported mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) { // This must be first, it sets up the globals dict and other things MP_DYNRUNTIME_INIT_ENTRY // Make the function available in the module's namespace mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj)); // This must be last, it restores the globals dict MP_DYNRUNTIME_INIT_EXIT }Makefile
# Location of top-level MicroPython directory MPY_DIR = ../../.. # Name of module MOD = factorial # Source files (.c or .py) SRC = factorial.c # Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc) ARCH = x64 # Include to get the rules for compiling and linking the module include $(MPY_DIR)/py/dynruntime.mk关键代码解读
py/dynruntime.h动态 API:模块的 C 代码必须#include "py/dynruntime.h",它以宏和 static-inline 函数的形式把静态运行时 API(py/obj.h、py/runtime.h中的定义)重定向到mp_fun_table中的动态实现,例如m_malloc()实际调用m_malloc_dyn(),而m_malloc_dyn()通过mp_fun_table.realloc_()完成内存分配。注意该头文件要求MICROPY_ENABLE_DYNRUNTIME开启(py/dynruntime.mk的CFLAGS会自动添加-DMICROPY_ENABLE_DYNRUNTIME),并要求禁用MICROPY_MALLOC_USES_ALLOCATED_SIZE。
入口函数mpy_init:每个原生模块必须至少定义一个名为mpy_init的函数,它是模块导入时的入口。函数体必须以MP_DYNRUNTIME_INIT_ENTRY开头(它会通过mp_fun_table.swap_globals()切换到模块自己的 globals 字典,并构造一个表示原生 raw-code 的占位结构),以MP_DYNRUNTIME_INIT_EXIT结尾(恢复旧 globals 并返回mp_const_none)。
导出名字:在MP_DYNRUNTIME_INIT_ENTRY与MP_DYNRUNTIME_INIT_EXIT之间,用mp_store_global(MP_QSTR_xxx, obj)把函数、常量等放入模块命名空间。MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial)则定义一个带 1 个位置参数的 Python 可见函数对象。
编译命令
构建前确认目标架构,然后直接:
$ make不改 Makefile 时可通过命令行覆盖架构:
$ make ARCH=armv7m同样可覆盖架构标志:
$ make ARCH=rv32imc ARCH_FLAGS=zba在 MicroPython 中使用
构建成功后得到factorial.mpy,将其拷贝到 MicroPython 设备文件系统中位于sys.path的目录(例如根目录),即可导入使用:
import factorial print(factorial.factorial(10)) # should display 3628800.py文件优先于.mpy文件被查找;若导入失败,可通过sys.implementation._mpy检查系统支持的 .mpy 版本与架构,具体排错方法见 MicroPython .mpy 文件说明。
混合 Python 与多文件 C 模块
原生模块并非只能有一个 C 文件:
- 模块可以拆分为多个 C 源文件;
- 部分代码也可以用 Python 实现。
所有源文件(.c、.S、.py)都要列在 Makefile 的SRC变量中。官方示例 examples/natmod/features2/Makefile 展示了混合形态:SRC = main.c prod.c test.py,其中.py文件会被mpy-cross编译为字节码,再与 C 目标文件链接出的原生.mpy通过mpy-tool.py --merge合并(参见 py/dynruntime.mk 的构建规则)。
深入特性:从官方 features 系列示例看能力边界
examples/natmod/ 目录收录了覆盖原生模块绝大多数特性的示例,值得逐一研读:
整数运算与局部辅助函数(features0)
examples/natmod/features0/features0.c 就是本文 factorial 示例的原型,演示了定义 Python 可见函数、局部 C 辅助函数、通过mp_obj_get_int/mp_obj_new_int获取与创建整数对象。
常量数据、BSS、重定位指针、内存分配、异常(features1)
examples/natmod/features1/features1.c 覆盖了:
- 全局 BSS 数据
uint16_t data16[4](不带初始化器); - rodata 常量数组
table8[]、table16[]; - rodata 中的重定位指针:
uint16_t *const table_ptr16a[]指向 BSS,const uint16_t *const table_ptr16b[]指向 rodata——这些指针在导入时由动态加载器重定位; - 用
m_new分配内存、创建 bytearray; - 用
mp_raise_ValueError抛异常; - 用
mp_obj_new_list创建列表; - 导出模块常量(
MP_OBJ_NEW_SMALL_INT、MP_OBJ_NEW_QSTR)。
浮点运算(features2)
features2 演示了浮点支持,但仅当目标支持硬件浮点时才可用——这正对应ARCH表中的浮点实现差异(如armv7emsp为 float、armv7emdp为 double、xtensa/rv32imc为 none)。
类型、常量对象与字典(features3)
features3 演示使用 MicroPython 类型系统、创建字典实例等。
定义类与自定义异常(features4)
examples/natmod/features4/features4.c 展示了完整的类定义流程:
- 定义
mp_obj_full_type_t mp_type_factorial并通过mp_obj_malloc(mp_obj_factorial_t, type)为实例分配状态; - 用
MP_OBJ_TYPE_SET_SLOT(&mp_type_factorial, make_new, factorial_make_new, 0)绑定__new__逻辑; - 用
MP_DEFINE_CONST_DICT声明方法表并绑定locals_dict槽位; - 用
mp_obj_exception_init(&mp_type_FactorialError, MP_QSTR_FactorialError, &mp_type_Exception)初始化自定义异常类型; - 最后在
mpy_init中通过mp_store_global导出类型。
内置模块的动态化移植(heapq、random、re、deflate、btree、framebuf)
examples/natmod/中还提供了一批"动态版内置模块",其原理是#include原始模块源码并完成模块 globals 字典的初始化。例如固件若以MICROPY_PY_FRAMEBUF关闭的方式编译(为节省 flash),framebuf原生模块即可动态补回该能力。
用 Picolibc 构建模块时的注意事项
使用 Picolibc 作为 C 标准库不仅受支持,而且是 rv32imc 与 rv64imc 平台的默认选择(py/dynruntime.mk 中会显式探测并选用picolibc.specs)。但需要注意:
- 部分预编译的 Picolibc 版本(如 Ubuntu 提供的
picolibc-arm-none-eabi、picolibc-riscv64-unknown-elf、picolibc-xtensa-lx106-elf包)假定运行时存在线程局部存储(TLS),而 MicroPython 模块在rv32imc、rv64imc上不支持 TLS,导致部分 Picolibc 功能默认走 TLS,在编译或链接时报错。 - 典型对策示例见 examples/natmod/btree/Makefile:通过
CFLAGS += -D__PICOLIBC_ERRNO_FUNCTION=__errno使errno正常工作的 workaround。
链接外部 C 库的实践:btree 示例
examples/natmod/btree/Makefile 是链接外部 C 库的完整范例:
- 通过
BTREE_DIR = $(MPY_DIR)/lib/berkeley-db-1.xx引用仓库内的 Berkeley DB 源码; - 用
SRC += $(addprefix ...)把bt_close.c、bt_put.c、mpool.c等十几源文件追加进SRC,随模块一起编译链接(即"自定义静态库"的源码级等价做法); - 通过
CFLAGS += -I$(BTREE_DIR)/include添加头文件路径; - 针对不同架构做条件配置:
xtensa时设置MPY_EXTERN_SYM_FILE指向 ESP8266 ROM 符号表;armv6m时LINK_RUNTIME = 1以链接libgcc.a的除法辅助函数;clang 工具链下为armv7m链接libclang_rt.builtins.a以提供 memset/memcpy; - 用
MPY_LD_FLAGS = "--source-name=$(MOD_BASE).mpy"剥离架构名,保证模块内部源文件名一致。
总结与下一步
原生 .mpy 模块为 MicroPython 提供了一条"不改固件、即插即用"的性能敏感代码扩展路径。核心要点可归纳为:
- 用
ARCH精确选择目标架构,必要时用ARCH_FLAGS携带架构扩展标志; - C 代码遵循 PIC + 全局 BSS + 无初始化数据的约束,规避不支持 data 段、静态 BSS、TLS 的限制;
- 通过
py/dynruntime.h的动态 API 与mp_fun_table导出符号表交互,不直接调用任意系统函数; - 需要 libm/libgcc 等运行时库时设置
LINK_RUNTIME = 1,外部库用MPY_LD_FLAGS += -l链接(或直接编译进SRC); - 构建产物
.mpy只需拷贝到设备sys.path即可import。
继续深入可阅读:Native machine code in .mpy files(本文依据)、MicroPython .mpy 文件说明(版本兼容与二进制格式)、py/dynruntime.h 与 py/nativeglue.h(动态 API 与符号表)、py/dynruntime.mk(构建规则与架构配置)、tools/mpy_ld.py(链接工具),以及 examples/natmod/ 下的全部示例工程。
【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考