MicroPython 外部 C 模块¶
在为 MicroPython 开发模块时,你可能会发现自己受到 Python 环境的限制,这通常是由于无法访问某些硬件资源或受到 Python 速度的限制。
如果你的限制无法通过 最大化 MicroPython 运行速度 中的建议解决,那么用 C(和/或 如果你的移植版本实现了 C++)编写部分或全部模块是一个可行的选择。
如果你的模块旨在访问或配合常见硬件或库使用,请考虑在 MicroPython 源代码树中与类似模块一起实现,并以拉取请求(pull request)的形式提交。但如果你针对的是冷门或专有系统,那么将其保留在主 MicroPython 仓库之外可能更合理。
本章介绍如何将这类外部模块编译进 MicroPython 可执行文件或固件镜像中。Make 和 CMake 两种构建工具都受支持,在编写外部模块时,最好同时为这两种工具添加构建文件,以便该模块可以在所有移植版本上使用。但在编译某个特定移植版本时,你只需使用其中一种构建方式,Make 或 CMake 均可。
另一种方法是使用 .mpy 文件中的原生机器码,它允许编写自定义 C 代码并放入 .mpy 文件中,该文件可以动态导入到正在运行的 MicroPython 系统中,而无需重新编译主固件。
外部 C 模块的结构¶
一个 MicroPython 用户 C 模块是一个包含以下文件的目录:
*.c/*.cpp/*.h模块的源代码文件。这些文件通常包含正在实现的底层功能,以及用于暴露这些函数和模块的 MicroPython 绑定函数。
目前,编写这些函数/模块的最佳参考方式是在 MicroPython 源码树中找到类似的模块,并以它们作为示例。
micropython.mk包含本模块的 Makefile 片段。$(USERMOD_DIR)在micropython.mk中可用,表示你的模块目录路径。由于它会为每个 C 模块重新定义,因此应在你的micropython.mk中将其展开为一个局部 make 变量,例如EXAMPLE_MOD_DIR := $(USERMOD_DIR)你的
micropython.mk必须将模块的源文件添加到SRC_USERMOD_C或SRC_USERMOD_LIB_C变量中。前者会被处理以查找MP_QSTR_和MP_REGISTER_MODULE定义,后者则不会(例如非 MicroPython 专用的辅助代码和库代码)。这些路径应包含你展开后的$(USERMOD_DIR)副本,例如:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
类似地,对于 C++ 源文件,使用
SRC_USERMOD_CXX和SRC_USERMOD_LIB_CXX。如果要包含汇编文件,请使用SRC_USERMOD_LIB_ASM。如果你有自定义的编译器选项(例如用
-I添加搜索头文件的目录),对于 C 代码应将它们添加到CFLAGS_USERMOD,对于 C++ 代码则添加到CXXFLAGS_USERMOD。micropython.cmake包含本模块的 CMake 配置。在
micropython.cmake中,你可以使用${CMAKE_CURRENT_LIST_DIR}作为当前模块的路径。你的
micropython.cmake应定义一个INTERFACE库,并将你的源文件、编译定义和包含目录与其关联。然后应将该库链接到usermod目标。add_library(usermod_cexample INTERFACE) target_sources(usermod_cexample INTERFACE ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c ) target_include_directories(usermod_cexample INTERFACE ${CMAKE_CURRENT_LIST_DIR} ) target_link_libraries(usermod INTERFACE usermod_cexample)
完整使用示例见下文。
基本示例¶
cexample 模块提供了一个函数和一个类的示例。cexample.add_ints(a, b) 函数将两个整数参数相加并返回结果。cexample.Timer() 类型创建计时器,可用于测量自对象实例化以来经过的时间。
该模块位于 MicroPython 源码树的 examples 目录中,包含一个源文件和一个 Makefile 片段,其内容如上所述:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
请参阅这些文件中的注释以获得更多说明。在 cexample 模块旁边还有一个 cppexample,它的工作方式相同,但展示了在 MicroPython 中混合使用 C 和 C++ 代码的一种方式。
将 cmodule 编译进 MicroPython¶
要构建这样一个模块,请编译 MicroPython(参见 入门指南),并进行 2 处修改:
设置构建时标志
USER_C_MODULES,使其指向你想要包含的模块。对于使用 Make 的移植版本,该变量应是一个目录,系统会自动在其中搜索模块。对于使用 CMake 的移植版本,该变量应是一个包含待构建模块的文件。详见下文。通过将相应的 C 预处理器宏设置为 1 来启用模块。仅当你正在构建的模块未被自动启用时才需要这样做。
要构建 MicroPython 自带的示例模块,对于 Make,请将 USER_C_MODULES 设置为 examples/usercmodule 目录;对于 CMake,则设置为 examples/usercmodule/micropython.cmake。
例如,下面演示了如何为 unix 移植版本构建带示例模块的版本:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
在构建中包含新的用户模块时,你可能需要在开始时运行一次 make clean。构建输出会显示找到的模块:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
对于基于 CMake 的移植版本(例如 rp2),情况会略有不同(注意 CMake 实际上是由 make 调用的):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
同样,你可能需要先运行 make clean 以便 CMake 识别用户模块。CMake 构建输出会按名称列出这些模块:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
顶层 micropython.cmake 的内容可用于控制启用哪些模块。
对于你自己的项目,将自定义代码保留在主 MicroPython 源码树之外会更方便,因此典型的项目目录结构如下所示:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
使用 Make 构建时,将 USER_C_MODULES 设置为 my_project/modules 目录。例如,构建 stm32 移植版本:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
使用 CMake 构建时,顶层 micropython.cmake(直接位于 my_project/modules 目录中)应 include 你想要提供的所有模块:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
然后使用以下命令构建:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
你也可以为 USER_C_MODULES 指定绝对路径。
由 USER_C_MODULES 变量指定的所有模块(使用 Make 时在该目录中找到的,或使用 CMake 时通过 include 添加的)都会被编译,但只有那些已启用的模块才可供导入。用户模块通常默认启用(由模块开发者决定),在这种情况下,除了如上所述设置 USER_C_MODULES 之外无需再做任何事情。
如果某个模块默认未启用,则必须启用相应的 C 预处理器宏。可以通过在模块源代码中搜索 MP_REGISTER_MODULE 这一行来找到该宏名(它通常出现在主源文件的末尾)。该宏应被一对 #if X / #endif 包围,并且必须使用 CFLAGS_EXTRA 将配置选项 X 设置为 1 才能使该模块可用。如果没有 #if X / #endif 这一对,则该模块默认启用。
例如,examples/usercmodule/cexample 模块默认启用,因此其源代码中有如下一行:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
或者,要使该模块默认禁用但可通过预处理器配置选项进行选择,则应为:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
在这种情况下,通过在 make 命令中添加 CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1,或编辑 mpconfigport.h 或 mpconfigboard.h 来添加以下内容,即可启用该模块
#define MODULE_CEXAMPLE_ENABLED (1)
请注意,确切的方法取决于移植版本,因为它们具有不同的结构。如果操作不正确,它仍能编译通过,但导入时将找不到该模块。
在 MicroPython 中使用模块¶
一旦编译进你的 MicroPython 副本中,该模块现在就可以像任何其他内置模块一样在 Python 中访问,例如
import cexample
print(cexample.add_ints(1, 3))
# should display 4
from cexample import Timer
from time import sleep_ms
watch = Timer()
sleep_ms(1000)
print(watch.time())
# should display approximately 1000
C 动态内存分配¶
MicroPython 为 内存管理 使用其自己的“Python 堆”,它与 C 库函数 malloc()、free() 等所使用的“C 堆”不同。并非每个 MicroPython 移植版本都自带“C 堆”。
第 1 级和第 2 级移植版本对通过“C 堆”进行 C 动态内存分配的支持程度各不相同:
unix、windows、esp32 和 webassembly 移植版本支持 C 动态内存分配。
rp2 移植版本在运行时将无法分配任何内存,除非使用
MICROPY_C_HEAP_SIZE=n构建固件,为 C 堆预留n个字节的内存。这块内存将无法供 Python 代码使用。alif, mimxrt, nrf, renesas-ra, samd, and stm32 port builds that include dynamic C allocation will fail at link-time with errors such as
undefined reference to `malloc'. MicroPython has no built-in support for dynamic C allocation on these ports. Any solution requires manually adding a C heap implementation to the custom build.zephyr 移植版本目前不支持使用用户模块进行构建。
将 Python 堆用作 C 堆¶
对 C 代码来说,改为调用诸如 m_malloc()、m_malloc0() 和 m_free() 之类的“Python 堆”动态分配函数可能更为实用。
有关此方法的更多信息,请参阅 从 C 代码使用 MicroPython 内存。
C++ 模块¶
大多数第 1 级和第 2 级 MicroPython 移植版本(以及部分第 3 级)支持构建 C++ 用户模块,使用上文所述的 C++ 专用环境变量。
成功集成 C++ 和 MicroPython 需要考虑一些额外的因素:
C++ 动态内存分配¶
C++ 程序(以及 C++ 标准库特性)通常使用动态内存分配。C++ 默认内存分配器(即 new 和 delete 运算符)通常实现为 C 动态内存分配 之上的一层。
对于不包含 C 动态内存分配支持的 MicroPython 移植版本,可以通过以下两种方式之一支持 C++ 动态内存分配:
在你的自定义构建中实现 C 动态内存分配。
在你的自定义构建中实现自定义 C++ 分配器。
链接相关注意事项¶
由于 MicroPython 是基于 C 的项目,任何链接到 MicroPython 或从 MicroPython 链接出去的符号在 C++ 代码中都需要用 extern "C" 限定。
强烈建议遵循 examples/usercmodule/cppexample 中演示的模式,即 Python 模块在一个极简的 C 文件中实现,该文件对 C++ 代码进行了封装。