MicroPython 外部 C 模块

当开发 MicroPython 的模块时,你会发现可能会遇到一些 Python 环境的限制,这通常是由于无法访问某些硬件资源或 Python 速度限制。

如果你的限制无法通过使用 最大化 MicroPython 速度 中的建议解决的话,可以尝试使用 C (如果你的适配端支持的话甚至可以是 C++ ) 编写你的模块。

如果你的模块是旨在访问或与常用的硬件或库协作,请考虑在 MicroPython 源码树中内嵌一些相似的模块,并提交 pull request 。如果你的目标是模糊的或专有的系统,则可能更适合将这个模块保持在 主 MicroPython 仓库之外。

这一章节描述了如何将这样的外部模块编译到 MicroPython 可执行文件或固件镜像。不论 Make 和 CMake 的编译工具都是支持的,并且当写一个外部模块时,建议同时为这两个工具添加的编译配置文件,以便于让这个模块就可在所有的适配端上使用。但是,当只为一个特定的适配端构建时,你可能只需要使用一种方法来编译,可以是 Make 或 CMake。

另一种方法是使用 Native machine code in .mpy files ,这一方式允许编写自定义的、被放置在 .mpy 文件中的 C 代码,这些代码可以在运行的 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` `中,添加你的模块源代码,其路径应当是你赋值的对应 ``SRC_USERMOD 的变量的相对路径,例如 SRC_USERMOD += $(EXAMPLE_MOD_DIR)/example.c

    如果你有自定义的编译选项(例如 -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) ,该函数把两个整数相加并返回结果。它可以在 MicroPython 源代码树中的 examples 目录 中找到,其中包含一个源码文件和一个 Makefile 片段内容如上所述。

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

参考这些文件的注释以获取额外的说明。紧接 cexample 模块的还有 cppexample 模块,该模块与 cexample 模块执行相同的行为,但展示了一种方式,说明在 MicroPython 中如何使用 C 和 C++ 代码混合开发。

将 c 模块编译到 MicroPython

要构建这样的模块,编译 MicroPython (参考 快速开始 ),并执行 2 个修改:

  1. 将构建时标识 USER_C_MODULES 指向来你要包含的模块。 如果使用 Make ,这个变量应该是一个指向自动搜索模块的目录。如果使用 CMake ,这个变量应该是一个包含要构建的模块的文件。详情见下文。

  2. 当且仅当你要构建的模块不被自动启用时,需要将对应的 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 构建的适配端,例如树莓派,这种方法会有些不同(请注意 CMake 最终是调用 make 来完成构建的):

cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake

你需要先为 CMake 再运行一遍 make clean 来启用用户模块。 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/esp32
make USER_C_MODULES=../../../../modules/micropython.cmake

请注意, esp32 适配端需要额外的 .. 来表示相对路径,因为它的主 CMakeLists.txt 文件在项目根目录中。你也可以用 USER_C_MODULES 指定到绝对路径。

所有通过 USER_C_MODULES 变量指定的模块(使用 Make 时,在这个变量指定的目录中查找;使用 CMake 时,则通过 include 添加)都会被编译,但只有启用的模块才会被 使用。用户模块通常会被默认启用(这是由模块开发者决定的),所以如果你不 想要某些模块,只需要将 USER_C_MODULES 变量设为空即可。

如果模块不默认启用,则必须启用对应的 C 预处理器宏。这个宏名称可以在模块的源代码中搜索到(通常是模块的主源文件最后一行)。MP_REGISTER_MODULE 的第三个参数是宏名称,这个宏名称必须使用 CFLAGS_EXTRA 来设置为 1 ,以使模块可用。如果第三个参数是 1 ,则模块默认启用。

例如,examples/usercmodule/cexample 模块默认启用,所以它的源代码中有如下一行:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule, 1);

另外,如果想要使这个模块不默认启用,但通过预处理器配置选项来选择,则是:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule, MODULE_CEXAMPLE_ENABLED);

这种情况下,若要启用模块,可通过在 make 命令添加 CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 ,或者编辑 mpconfigport.hmpconfigboard.h 添加以下行

#define MODULE_CEXAMPLE_ENABLED (1)

请注意,这种方法取决于适配端,因为不同适配端有不同的结构。如果没有按照正确做法配置,也会加入编译,但导入将失败。

在 MicroPython 中使用模块

一旦编译到你的 MicroPython 固件中,模块就可以在 Python 中被直接访问,例如:

import cexample
print(cexample.add_ints(1, 3))
# should display 4