news 2026/9/21 16:37:05

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MicroPython 嵌入指南:在 C 应用中集成 MicroPython(embed port 实战)

MicroPython 嵌入指南:在 C 应用中集成 MicroPython(embed 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/esp32ports/stm32),而 ports/embed 是一个特殊的 port:它不面向任何具体硬件,而是面向 C 语言本身。它把 MicroPython 运行时编译成一整套可嵌入的.c/.h源文件,供宿主项目直接纳入编译,从而让现有 C/C++ 应用获得执行 Python 脚本的能力。

从 ports/embed/README.md 可以看到,在项目中使用 embed port 主要有三个步骤:

  1. 通过一个mpconfigport.h文件为项目提供 MicroPython 配置;
  2. 用官方提供的embed.mk针对该配置构建 embed port,输出一套自包含的 MicroPython 源文件(这些文件可以放到仓库之外);
  3. 编译项目,这一步要求把第 2 步生成的所有.c文件一并编译。

examples/embedding目录正是这三步的最小可运行示范,其文件构成如下:

  • main.c:宿主 C 程序,即“被嵌入 MicroPython 的应用”;
  • micropython_embed.mk:调用 embed port 构建逻辑的 make 片段;
  • Makefile:示例工程自身的构建脚本(可用你自己的构建系统替代);
  • mpconfigport.h:MicroPython 功能配置头文件;
  • 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.hgchelper_generic.cGC 辅助代码(寄存器与栈扫描)
genhdrbuild-embed/genhdr/生成头文件:moduledefs.hmpversion.hqstrdefs.generated.hroot_pointers.h
portports/embed/port/*.[ch]嵌入专用 API 实现(micropython_embed.hembed_util.c等)

构建过程中,embed.mk会先通过include $(MICROPYTHON_TOP)/py/mkenv.mkpy/py.mk引入核心环境与 make 定义,再以-std=c99 -Wall -Werror等标志准备编译环境,并默认关闭 ROM 文本压缩(MICROPY_ROM_TEXT_COMPRESSION ?= 0,可通过变量覆盖)。生成的四个头文件属于运行时的“元数据头”,它们由仓库工具链(如py/makeqstrdata.pypy/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.c

main.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)), end='eol\\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 核心流程:初始化 → 执行 → 反初始化

代码展示了嵌入使用的标准生命周期:

  1. mp_embed_init:传入三个参数——GC 堆起始地址、堆大小、栈顶地址。示例中堆是一块static char heap[8 * 1024]的静态数组(8 KB),完全由宿主程序提供内存,不依赖平台 malloc 策略;
  2. mp_embed_exec_str:把 Python 源码字符串就地编译并执行(内部会先编译再运行,见下文实现剖析);
  3. mp_embed_deinit:反初始化运行时,释放内部状态。

关于栈顶参数,main.c 的注释给出重要提示:&stack_top在多数场景下够用,但根据运行环境,可能有更合适的取栈顶方式,例如pthread_get_stackaddr_nppthread_getattr_np,或__builtin_frame_address/__builtin_stack_address。在单线程的裸机/嵌入式场景,取局部变量地址即可;在多线程环境中则应使用与线程关联的栈信息 API。

3.2 嵌入 API 全貌:micropython_embed.h

port/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.c

port/embed_util.c 给出了上述 API 的底层实现,我们可以借此看清“嵌入”背后的真实调用链。

初始化mp_embed_init):

void 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 栈扫描)、用宿主提供的堆区间初始化 GC(gc_init的第二个参数是堆区间的结束地址)、最后mp_init()启动整个运行时。

执行字符串脚本mp_embed_exec_str):

void 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把源码字符串包装为词法器(源名记为<stdin>),mp_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完成寄存器与栈的根对象扫描,供宿主在合适的时机手动触发完整 GC;
  • nlr_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_GCMICROPY_PY_GC(GC 运行时及其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_embedmicropython_embed/port)。

此外,embed.mkPACKAGE_DIR ?= micropython_embed可通过变量覆盖,从而自定义生成的源码包目录名;生成的包由于只含普通.c/.h文件,可以自由放置到仓库之外的任何位置,甚至复制进宿主工程的源码树。

六、从示例到生产:嵌入实战要点

综合官方 README 与源码实现,把 embed port 用于真实项目时建议关注以下几点:

  1. 内存规划:GC 堆由宿主静态数组或专用内存区提供,示例用 8 KB,实际大小需按脚本复杂度与对象分配量评估;栈顶参数必须正确传递,多线程环境优先使用线程栈 API;
  2. 配置先行:先确定需要哪些功能(编译器、GC、持久化字节码、sys模块等),在mpconfigport.h中显式声明,避免携带无用功能膨胀固件;
  3. 脚本执行方式:源码字符串用mp_embed_exec_str;追求体积与启动速度时,用mpy-cross预编译脚本并以mp_embed_exec_mpy加载.mpy数据;
  4. 异常边界mp_embed_exec_str/mp_embed_exec_mpy内部用 NLR 捕获 Python 异常并打印,宿主在调用前后应保持自身的 C 异常/错误处理约定;
  5. 构建集成:编译所有micropython_embed下的.c,头文件搜索路径至少包含micropython_embedmicropython_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 16:26:23

PVE中Intel核显直通LXC的真相与绕过方案

1. 为什么 Intel 核显直通 LXC 在 PVE 7.1–8 上是个“伪需求陷阱”你搜到这篇指南&#xff0c;大概率是因为——刚在 PVE Web 界面里点开 LXC 容器设置页&#xff0c;发现“设备”栏下赫然写着“GPU 设备直通”&#xff0c;旁边还配了个小图标&#xff1b;再一查 Intel UHD Gr…

作者头像 李华
网站建设 2026/9/21 16:25:48

React Native跨平台图片圆角处理与OpenHarmony适配方案

1. 跨平台图片处理的技术挑战在移动应用开发中&#xff0c;图片显示是最基础也最频繁使用的功能之一。而圆角裁剪作为UI设计中的常见需求&#xff0c;看似简单实则暗藏玄机。当我们需要在React Native框架中对接OpenHarmony系统时&#xff0c;这个问题就变得更加复杂。我最近在…

作者头像 李华