十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MicroPython 原生 .mpy 模块开发指南:用 C 编写可动态加载的本地机器码模块

MicroPython 原生 .mpy 模块开发指南:用 C 编写可动态加载的本地机器码模块 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 3pyelftools 库 v0.25 或更高安装方式如pip install pyelftools0.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含义典型目标交叉编译器前缀默认浮点实现x8632 位 x8632 位 Linux 主机i686-linux-gnu-doublex6464 位 x8664 位主机x86_64-linux-gnu-doublearmv6mARM ThumbCortex-M0STM32F0 等arm-none-eabi-floatarmv7mARM Thumb 2Cortex-M3STM32F1/F2/F4 等arm-none-eabi-floatarmv7emspARM Thumb 2单精度浮点Cortex-M4F、Cortex-M7STM32F4/F7 等arm-none-eabi-floatarmv7emdpARM Thumb 2双精度浮点Cortex-M7带双精度 FPU 的器件arm-none-eabi-doublextensa非窗口化 XtensaESP8266xtensa-lx106-elf-nonextensawin窗口化 Xtensa窗口大小 8ESP32、ESP32S3xtensa-esp32-elf-floatrv32imcRISC-V 32 位带压缩指令ESP32C3、ESP32C6riscv64-unknown-elf-nonerv64imcRISC-V 64 位带压缩指令RISC-V 64 器件riscv64-unknown-elf-none架构标志 ARCH_FLAGS部分平台支持显式架构标志。若希望输出.mpy文件携带这些标志的取值例如 RISC-V 处理器扩展构建时必须通过ARCH_FLAGS变量传给mpy_ld.py$ make ARCHrv32imc ARCH_FLAGSzba在 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包括字符串与常量数据数组、结构体等清零数据BSStext 中指向 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 modules 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 函数的形式把静态运行时 APIpy/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 ARCHarmv7m同样可覆盖架构标志$ make ARCHrv32imc ARCH_FLAGSzba在 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/ 目录收录了覆盖原生模块绝大多数特性的示例值得逐一研读整数运算与局部辅助函数features0examples/natmod/features0/features0.c 就是本文 factorial 示例的原型演示了定义 Python 可见函数、局部 C 辅助函数、通过mp_obj_get_int/mp_obj_new_int获取与创建整数对象。常量数据、BSS、重定位指针、内存分配、异常features1examples/natmod/features1/features1.c 覆盖了全局 BSS 数据uint16_t data16[4]不带初始化器rodata 常量数组table8[]、table16[]rodata 中的重定位指针uint16_t *const table_ptr16a[]指向 BSSconst 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。浮点运算features2features2 演示了浮点支持但仅当目标支持硬件浮点时才可用——这正对应ARCH表中的浮点实现差异如armv7emsp为 float、armv7emdp为 double、xtensa/rv32imc为 none。类型、常量对象与字典features3features3 演示使用 MicroPython 类型系统、创建字典实例等。定义类与自定义异常features4examples/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、framebufexamples/natmod/中还提供了一批动态版内置模块其原理是#include原始模块源码并完成模块 globals 字典的初始化。例如固件若以MICROPY_PY_FRAMEBUF关闭的方式编译为节省 flashframebuf原生模块即可动态补回该能力。用 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),仅供参考
返回列表