ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MicroPython 嵌入指南:在 C 应用中集成 MicroPython(embed port 实战)

MicroPython 嵌入指南:在 C 应用中集成 MicroPython(embed port 实战) MicroPython 嵌入指南在 C 应用中集成 MicroPythonembed port 实战【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython导读本文基于 MicroPython 官方提供的嵌入示例examples/embedding系统讲解如何把 MicroPython 作为一个 C 库嵌入到独立的 C 应用程序中从构建 embed port、生成自包含的micropython_embed源码包到编写宿主 C 程序、初始化运行时、执行 Python 脚本再到脱离仓库树进行“树外out-of-tree”构建。读完本文你将掌握 embed port 的完整工作流、三个核心 C API 的用法与源码级实现原理并能在自己的项目中独立完成嵌入式脚本引擎的集成。一、embed port 是什么MicroPython 官方将“面向特定硬件架构或平台”的实现称为 port如ports/esp32、ports/stm32而 ports/embed 是一个特殊的 port它不面向任何具体硬件而是面向 C 语言本身。它把 MicroPython 运行时编译成一整套可嵌入的.c/.h源文件供宿主项目直接纳入编译从而让现有 C/C 应用获得执行 Python 脚本的能力。从 ports/embed/README.md 可以看到在项目中使用 embed port 主要有三个步骤通过一个mpconfigport.h文件为项目提供 MicroPython 配置用官方提供的embed.mk针对该配置构建 embed port输出一套自包含的 MicroPython 源文件这些文件可以放到仓库之外编译项目这一步要求把第 2 步生成的所有.c文件一并编译。examples/embedding目录正是这三步的最小可运行示范其文件构成如下main.c宿主 C 程序即“被嵌入 MicroPython 的应用”micropython_embed.mk调用 embed port 构建逻辑的 make 片段Makefile示例工程自身的构建脚本可用你自己的构建系统替代mpconfigport.hMicroPython 功能配置头文件README.md本文所依据的官方说明文档。二、构建示例从零生成可运行程序2.1 第一步生成自包含的嵌入源码包在examples/embedding目录下执行$ make -f micropython_embed.mk这一命令会生成micropython_embed目录。它是一份self-contained自包含的 MicroPython 拷贝专门用于嵌入场景目录中的.c文件需要以你的项目所能接受的方式编译进工程示例工程本身使用 make 加Makefile来完成这件事。那么这条命令到底做了什么关键在于 micropython_embed.mk它的内容极短# Set the location of the top of the MicroPython repository. MICROPYTHON_TOP ../.. # Include the main makefile fragment to build the MicroPython component. include $(MICROPYTHON_TOP)/ports/embed/embed.mk它只做两件事定义MICROPYTHON_TOP仓库根目录然后引入 ports/embed/embed.mk。而 embed.mk 中定义了核心目标micropython-embed-package它会把以下内容拷贝进micropython_embed包子目录内容来源说明pypy/*.[ch]核心运行时解释器、编译器、GC、对象模型等extmodextmod/modplatform.h平台模块头shared/runtimeshared/runtime/gchelper.h、gchelper_generic.cGC 辅助代码寄存器与栈扫描genhdrbuild-embed/genhdr/生成头文件moduledefs.h、mpversion.h、qstrdefs.generated.h、root_pointers.hportports/embed/port/*.[ch]嵌入专用 API 实现micropython_embed.h、embed_util.c等构建过程中embed.mk会先通过include $(MICROPYTHON_TOP)/py/mkenv.mk和py/py.mk引入核心环境与 make 定义再以-stdc99 -Wall -Werror等标志准备编译环境并默认关闭 ROM 文本压缩MICROPY_ROM_TEXT_COMPRESSION ? 0可通过变量覆盖。生成的四个头文件属于运行时的“元数据头”它们由仓库工具链如py/makeqstrdata.py、py/makemoduledefs.py派生是包内源码能够编译的前提。2.2 第二步编译示例工程生成micropython_embed目录后直接构建示例可执行程序$ make这一步依据 Makefile 完成。该 Makefile 被刻意保持得极其简单用于演示“只需编译micropython_embed目录下所有.c文件”这一核心要求EMBED_DIR micropython_embed PROG embed CFLAGS -I. CFLAGS -I$(EMBED_DIR) CFLAGS -I$(EMBED_DIR)/port CFLAGS -Wall -Og -fno-common SRC main.c SRC $(wildcard $(EMBED_DIR)/*/*.c) $(wildcard $(EMBED_DIR)/*/*/*.c) OBJ $(SRC:.c.o) $(PROG): $(OBJ) $(CC) -o $ $^注意三个头文件搜索路径当前目录为了找到mpconfigport.h、micropython_embed根目录、以及micropython_embed/port为了找到port/micropython_embed.h。-fno-common可避免嵌入多个编译单元时出现符号合并问题是嵌入式集成中值得保留的防御性选项。官方注释也明确说明这个 Makefile 只是演示实际项目中应替换为你自己的构建系统。2.3 第三步运行$ ./embed程序会依次执行两段 Python 脚本并输出到标准输出例如第一段脚本输出hello world!及一个由生成器表达式构造的列表第二段脚本演示循环、字符串格式化、异常捕获与显式 GC 回收。三、宿主 C 程序剖析main.cmain.c 是整个示例的灵魂完整展示了嵌入 MicroPython 的最小骨架#include port/micropython_embed.h // This is example 1 script, which will be compiled and executed. static const char *example_1 print(hello world!, list(x 1 for x in range(10)), endeol\\n); // This is example 2 script, which will be compiled and executed. static const char *example_2 for i in range(10):\n print(iter {:08}.format(i))\n \n try:\n 1//0\n except Exception as er:\n print(caught exception, repr(er))\n \n import gc\n print(run GC collect)\n gc.collect()\n \n print(finish)\n ; // This array is the MicroPython GC heap. static char heap[8 * 1024]; int main() { int stack_top; mp_embed_init(heap[0], sizeof(heap), stack_top); mp_embed_exec_str(example_1); mp_embed_exec_str(example_2); mp_embed_deinit(); return 0; }3.1 核心流程初始化 → 执行 → 反初始化代码展示了嵌入使用的标准生命周期mp_embed_init传入三个参数——GC 堆起始地址、堆大小、栈顶地址。示例中堆是一块static char heap[8 * 1024]的静态数组8 KB完全由宿主程序提供内存不依赖平台 malloc 策略mp_embed_exec_str把 Python 源码字符串就地编译并执行内部会先编译再运行见下文实现剖析mp_embed_deinit反初始化运行时释放内部状态。关于栈顶参数main.c 的注释给出重要提示stack_top在多数场景下够用但根据运行环境可能有更合适的取栈顶方式例如pthread_get_stackaddr_np、pthread_getattr_np或__builtin_frame_address/__builtin_stack_address。在单线程的裸机/嵌入式场景取局部变量地址即可在多线程环境中则应使用与线程关联的栈信息 API。3.2 嵌入 API 全貌micropython_embed.hport/micropython_embed.h 定义了完整的公开 API一共四个函数void mp_embed_init(void *gc_heap, size_t gc_heap_size, void *stack_top); void mp_embed_deinit(void); // Only available if MICROPY_ENABLE_COMPILER is enabled. void mp_embed_exec_str(const char *src); // Only available if MICROPY_PERSISTENT_CODE_LOAD is enabled. void mp_embed_exec_mpy(const uint8_t *mpy, size_t len);其中mp_embed_exec_str依赖MICROPY_ENABLE_COMPILER示例配置中已开启而mp_embed_exec_mpy用于执行预编译的.mpy字节码需要开启MICROPY_PERSISTENT_CODE_LOAD——这是把脚本预编译后分发、避免目标设备上携带编译器的重要路径。3.3 实现原理embed_util.cport/embed_util.c 给出了上述 API 的底层实现我们可以借此看清“嵌入”背后的真实调用链。初始化mp_embed_initvoid mp_embed_init(void *gc_heap, size_t gc_heap_size, void *stack_top) { mp_stack_set_top(stack_top); gc_init(gc_heap, (uint8_t *)gc_heap gc_heap_size); mp_init(); }依次完成三件事用宿主提供的栈顶设置 MicroPython 的栈指针跟踪用于栈溢出保护与 GC 栈扫描、用宿主提供的堆区间初始化 GCgc_init的第二个参数是堆区间的结束地址、最后mp_init()启动整个运行时。执行字符串脚本mp_embed_exec_strvoid mp_embed_exec_str(const char *src) { nlr_buf_t nlr; if (nlr_push(nlr) 0) { // Compile, parse and execute the given string. mp_lexer_t *lex mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0); qstr source_name lex-source_name; mp_parse_tree_t parse_tree mp_parse(lex, MP_PARSE_FILE_INPUT); mp_obj_t module_fun mp_compile(parse_tree, source_name, true); mp_call_function_0(module_fun); nlr_pop(); } else { // Uncaught exception: print it out. mp_obj_print_exception(mp_plat_print, (mp_obj_t)nlr.ret_val); } }其执行管线与 MicroPython REPL 高度一致用mp_lexer_new_from_str_len把源码字符串包装为词法器源名记为stdinmp_parse生成解析树mp_compile编译为模块函数对象最后mp_call_function_0调用执行。整个编译执行过程被包在nlr_push/nlr_pop的非本地跳转NLR保护区中一旦 Python 侧抛出异常跳转到else分支并调用mp_obj_print_exception把未捕获异常打印到mp_plat_print——这正是示例脚本里1//0能被try/except正常捕获、且未捕获异常不会弄崩宿主进程的原因。embed_util.c还补全了嵌入场景必需的平台胶水代码gc_collect()当MICROPY_ENABLE_GC开启时通过gc_helper_collect_regs_and_stack完成寄存器与栈的根对象扫描供宿主在合适的时机手动触发完整 GCnlr_jump_fail()NLR 机制在无异常保护区域外失败时的兜底处理__assert_func()仅NDEBUG未定义即调试构建时断言失败的兜底处理。也就是说embed port 不仅交付了解释器还替宿主承担了异常处理、GC 根扫描、断言等底层细节宿主只需提供堆、栈顶和编译环境。四、配置裁剪mpconfigport.h嵌入场景通常对体积敏感因此 embed port 强调通过mpconfigport.h做配置裁剪。示例的 mpconfigport.h 如下// Include common MicroPython embed configuration. #include port/mpconfigport_common.h // Use the minimal starting configuration (disables all optional features). #define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_MINIMUM) // MicroPython configuration. #define MICROPY_ENABLE_COMPILER (1) #define MICROPY_ENABLE_GC (1) #define MICROPY_PY_GC (1) #define MICROPY_PY_SYS (0)要点解读首先包含 port/mpconfigport_common.h获得 embed port 的公共默认配置基线MICROPY_CONFIG_ROM_LEVEL设为MICROPY_CONFIG_ROM_LEVEL_MINIMUM即“最小起始配置”一次性关闭全部可选功能作为从零裁剪的起点随后按需开启MICROPY_ENABLE_COMPILER编译器mp_embed_exec_str的前置条件、MICROPY_ENABLE_GC与MICROPY_PY_GCGC 运行时及其gc模块MICROPY_PY_SYS显式关闭sys模块进一步节省 ROM。这套“先全关、再按需开”的配置方式是控制嵌入后二进制体积的关键手段。需要mp_embed_exec_mpy时只需在MICROPY_PERSISTENT_CODE_LOAD上开启对应功能即可。五、树外Out-of-tree构建把 MicroPython 作为子模块示例默认在 MicroPython 仓库树内即可开箱即用但真实项目中宿主应用通常位于仓库之外。官方 README 明确指出唯一需要改动的地方是把micropython_embed.mk中的MICROPYTHON_TOP指向 MicroPython 仓库的位置。例如# Set the location of the top of the MicroPython repository. MICROPYTHON_TOP /path/to/your/checkout/of/micropython官方还建议了一种典型集成方式把 MicroPython 仓库作为你项目的 git submodule然后在自己的顶层 Makefile 中include $(MICROPYTHON_TOP)/ports/embed/embed.mk复用其micropython-embed-package目标生成micropython_embed包再将其纳入你的构建系统CMake、Meson、手写 Makefile 皆可只要保证所有.c参与编译且头文件搜索路径覆盖micropython_embed与micropython_embed/port。此外embed.mk中PACKAGE_DIR ? micropython_embed可通过变量覆盖从而自定义生成的源码包目录名生成的包由于只含普通.c/.h文件可以自由放置到仓库之外的任何位置甚至复制进宿主工程的源码树。六、从示例到生产嵌入实战要点综合官方 README 与源码实现把 embed port 用于真实项目时建议关注以下几点内存规划GC 堆由宿主静态数组或专用内存区提供示例用 8 KB实际大小需按脚本复杂度与对象分配量评估栈顶参数必须正确传递多线程环境优先使用线程栈 API配置先行先确定需要哪些功能编译器、GC、持久化字节码、sys模块等在mpconfigport.h中显式声明避免携带无用功能膨胀固件脚本执行方式源码字符串用mp_embed_exec_str追求体积与启动速度时用mpy-cross预编译脚本并以mp_embed_exec_mpy加载.mpy数据异常边界mp_embed_exec_str/mp_embed_exec_mpy内部用 NLR 捕获 Python 异常并打印宿主在调用前后应保持自身的 C 异常/错误处理约定构建集成编译所有micropython_embed下的.c头文件搜索路径至少包含micropython_embed与micropython_embed/port并建议保留-fno-common把MICROPYTHON_TOP指向仓库根目录例如 git submodule即可完全脱离仓库树工作。七、总结examples/embedding以最小可运行的形式演示了 embed port 的完整闭环make -f micropython_embed.mk生成自包含源码包 →make编译宿主程序 →./embed运行 Python 脚本。其背后的 ports/embed 提供了一套面向 C 语言而非具体硬件的移植层配合mp_embed_init/mp_embed_exec_str/mp_embed_exec_mpy/mp_embed_deinit四个 API以及“最小配置 按需开启”的mpconfigport.h裁剪哲学让任何 C/C 项目都能以可预期、可裁剪的方式获得脚本执行能力。对读者而言把micropython_embed.mk中的MICROPYTHON_TOP指向自己的 MicroPython 检出目录即可把整套流程平移到自己的工程中。【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进