MicroPython核心:用C扩展MicroPython

本文介绍了在C语言中实现附加功能的选项,但这些功能是通过在MicroPython主资源库之外编写的代码实现的。第一种方法适用于构建定制固件,其中包含一些可从Python访问的特定项目附加模块或函数。第二种方法用于构建可在运行时加载的模块。

有关构建MicroPython主资源库中核心模块的更多信息,请参阅库部分。

MicroPython外部C语言模块

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

如果限制无法通过MicroPython性能调优中的建议来解决,那么用C语言编写部分或全部模块是一个可行的选择。

如果模块旨在访问或使用常见的硬件或库,建议在MicroPython源代码树中与类似模块一起实现,并将其作为拉取请求提交。但是,如果目标是晦涩难懂或专有的系统,将其保留在MicroPython主资源库之外可能更有意义。

本文将介绍如何将这些外部模块编译到MicroPython可执行文件或固件映像中。Make和CMake两种编译工具都支持,在编写外部模块时,最好为这两种工具添加编译文件,这样模块就可以在所有移植上使用。但在编译某个特定的移植时,您只需使用一种编译方法,即 Make或CMake。

另一种方法是在.mpy文件中使用本地机器代码,这样就可以编写放置在.mpy文件中的自定义C代码,并将其动态导入运行中的MicroPython系统而无需重新编译主固件。

外部C语言模块结构

MicroPython用户C模块是一个包含以下文件的目录:

  • 模块的 *.c / *.cpp / *.h 源代码文件。
    这些文件通常包括正在实现的底层功能以及用于公开函数和模块的MicroPython绑定函数。
    目前,编写这些函数/模块的最佳参考方法是在MicroPython目录中查找类似模块并将其作为示例。

  • micropython.mk包含该模块的Makefile 片段。
    microropython.mk中,$(USERMOD_DIR)是模块目录的路径。由于每个c模块都要重新定义它,因此应在microropython.mk中将其扩展为一个本地make变量,例如EXAMPLE_MOD_DIR := $(USERMOD_DIR)
    microropython.mk必须将模块源文件添加到SRC_USERMOD_CSRC_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_CXXSRC_USERMOD_LIB_CXX
    如果有自定义编译器选项(如-I用于添加搜索头文件的目录),则应将这些选项添加到用于C代码的CFLAGS_USERMOD和用于C++代码的CXXFLAGS_USERMOD中。

  • micropython.cmake包含该模块的CMake配置。
    micropython.cmake中,可以使用${CMAKE_CURRENT_LIST_DIR}作为当前模块的路径。
    microropython.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(请参阅源码获取、编译构建),并进行两处修改:

  1. 设置联编时标志USER_C_MODULES,使其指向希望包含的模块。对于使用 Make的移植,这个变量应该是一个可以自动搜索模块的目录。对于使用CMake的移植, 这个变量应该是一个包含要联编的模块的文件,详情请参阅下文。

  2. 将相应的C预处理器宏设为1以启用模块。只有在联编的模块不是自动启用的情况下,才需要这样做。

要编译MicroPython附带的示例模块,对于Make,需要把USER_C_MODULES设置为examples/usercmodule目录,对于CMake,需要把USER_C_MODULES设置为examples/usercmodule/micropython.cmake

例如, 以下是使用示例模块联编unix移植的方法:

cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule

如果在联编过程中加入了新的用户模块,可能需要在开始时运行一次make clean。编译输出将显示找到的模块:

...
Including UserCModule from ../../examples/usercmodule/cexample
Including UserCModule from ../../examples/usercmodule/cppexample
...

对于基于CMake的port,如rp2,看起来会有些不同(注意 CMake 实际上是由 make 调用的):

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

同样,需要先运行make clean才能让CMake提取用户模块。CMake的编译输出会按名称列出模块:

...
Including UserCModule(s) from ../../examples/usercmodule/micropython.cmake
Found UserCModule(s): usermod_cexample, usermod_cppexample
...

顶层 microropython.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 port:

cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules

使用CMake构建时,micropython.cmake(可直接在my_project/modules目录中找到)的顶层应包含要使用的所有模块:

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 port由于其主要CMakeLists.txt文件的位置不同,因此相对路径需要额外的..。也可以指定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.hmpconfigboard.h以添加:

#define MODULE_CEXAMPLE_ENABLED (1)

请注意,具体方法取决于port,因为它们有不同的结构。如果操作不当,虽然可以编译,但导入时会找不到模块。

MicroPython中模块的使用

一旦内置到MicroPython副本中,就可以像访问其他内置模块一样在Python中访问该模块,例如:

import cexample
print(cexample.add_ints(1, 3))
# 应该显示4
from cexample import Timer
from time import sleep_ms

watch = Timer()
sleep_ms(1000)
print(watch.time())
# 应该显示大约1000左右

.mpy文件中的本地机器代码

这部分介绍如何构建和处理包含Python以外语言的本地机器代码的.mpy文件。这样,您就可以用C语言编写代码,将其编译并链接到.mpy文件中,然后像导入普通Python模块一样导入该文件。这可用于实现对性能要求较高的功能,或包含用其他语言编写的现有库。

使用本地.mpy文件的主要优势之一是,本地机器代码可由脚本动态导入,而无需重新构建MicroPython主固件。这与MicroPython外部C模块形成鲜明对比,后者也可以用C语言定义自定义模块,但必须编译到主固件映像中。

这里的重点是使用C语言构建本地模块,但原则上,任何可编译为独立机器代码的语言都可以放入.mpy文件。

本地.mpy模块使用 mpy_ld.py 工具构建,该工具位于项目的tools/目录中。该工具获取一组对象文件(.o 文件),并将它们链接在一起,创建本地.mpy文件。它需要CPython 3和pyelftools v0.25 或更高版本的库。

支持的功能和限制

.mpy文件可以包含MicroPython字节码和/或本地机器码。如果包含本地机器代码,.mpy文件将与特定的体系结构相关联。目前支持的架构有(这些是 ARCH 变量的有效选项,见下文):

  • x86(32 位)
  • x64(64 位 x86)
  • armv6m(ARM Thumb,例如 Cortex-M0)
  • armv7m(ARM Thumb 2,如 Cortex-M3)
  • armv7emsp(ARM Thumb 2,单精度浮点运算,例如 Cortex-M4F、Cortex-M7)
  • armv7emdp(ARM Thumb 2,双精度浮点运算,例如 Cortex-M7)
  • xtensa(非窗口,例如 ESP8266)
  • xtensawin(有窗口,窗口大小为 8,例如 ESP32)

编译和链接本地.mpy文件时,必须选择体系结构,并且只能在该体系结构上导入相应文件。有关.mpy文件的更多详情,请参阅MicroPython.mpy文件。

本地代码必须编译为位置无关代码 (PIC position independent code),并使用全局偏移表 (GOT global offset table),但具体细节因体系结构而异。在导入带有本地代码的.mpy文件时,导入机制可以对本地代码进行一些基本的重定位,包括重新定位文本、rodata 和 BSS 部分。

链接器和动态加载器支持的功能包括:

  • 可执行代码(text)
  • 只读数据(rodata),包括字符串和常量数据(数组、结构体等)
  • 归零数据(BSS)
  • 文本中指向文本、rodata和 BSS 的指针
  • rodata中指向文本、啮合数据和 BSS 的指针

已知的限制有:

  • 不支持数据段;解决方法:使用 BSS 数据并明确初始化数据值
  • 不支持静态 BSS 变量;解决方法:使用全局 BSS 变量

因此,如果C代码有可写数据,要确保数据是全局定义的,没有初始化器,并且只能在函数中写入。

链接器限制:本地模块不是根据完整MicroPython固件的符号表链接的。相反,它是根据mp_fun_table(位于py/nativeglue.h)中的导出符号明确表链接的,该表在固件构建时已固定。因此,不能简单地调用一些任意的HAL/OS/RTOS/system 函数。

可以在表格末尾添加新符号,然后重新构建固件,这些符号也需要添加到相同位置的tools/mpy_ld.pyfun_table dict中。这样,在导入mpy时,mpy_ld.py就能捕捉到新符号,并为其重新定位。最后,如果符号是函数,则应在py/dynruntime.h中添加一个宏或存根,以方便调用函数。

定义本地模块

一个本地.mpy模块由一组用于构建.mpy的文件定义。文件系统布局由源文件和 Makefile两大部分组成:

在最简单的情况下,只需要一个C源文件,其中包含将被编译到.mpy模块中的所有代码。该C源代码必须包含py/dynruntime.h文件,以便访问MicroPython的动态API,并且必须至少定义一个名为mpy_init的函数。该函数将作为模块的入口点,在模块被导入时调用。

如果需要,可以将模块拆分成多个C源文件。模块的部分内容也可以用 Python 实现。所有源文件都应通过添加到SRC变量(见下文),在 Makefile 中列出。这既包括C源文件,也包括将包含在生成的.mpy文件中的任何 Python 文件。

Makefile包含模块的联编配置,并列出用于联编.mpy模块的源文件。它应将 MPY_DIR定义为MicroPython资源库的位置(用于查找头文件、相关 Makefile 片段和mpy_ld.py工具),将MOD定义为模块名称,将SRC定义为源文件列表,可选择通过ARCH指定机器架构,然后包含py/dynruntime.mk

最小示例

本例提供了一个名为factorial的简单模块的完整运行示例,该模块提供一个函数factorial.factorial(x)用于计算输入值的阶乘并返回结果。

目录布局:

factorial/
├── factorial.c
└── Makefile

文件 factorial.c 包含:

// 包含了头文件以便访问MicroPython API
#include "py/dynruntime.h"

// 计算阶乘的辅助函数
STATIC mp_int_t factorial_helper(mp_int_t x) {
    if (x == 0) {
        return 1;
    }
    return x * factorial_helper(x - 1);
}

// 这是 Python 将调用的函数,即 factor(x)
STATIC mp_obj_t factorial(mp_obj_t x_obj) {
    // 从MicroPyton输入的对象中提取整数
    mp_int_t x = mp_obj_get_int(x_obj);
    // 计算阶乘
    mp_int_t result = factorial_helper(x);
    // 把结果转换为MicroPython整数对象并返回
    return mp_obj_new_int(result);
}
// 定义上述函数的 Python 引用
STATIC MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

// 这是入口点,在导入模块时被调用
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    //这必须是第一项,因为它设置了 globals dict 和其他东西

    MP_DYNRUNTIME_INIT_ENTRY

    //使函数在模块的命名空间中可用
    mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));

    // T这必须是最后一个,它恢复全局dict
    MP_DYNRUNTIME_INIT_EXIT
}

Makefile 文件包含:

# MicroPython的根目录
MPY_DIR = ../../..

# 模块名称
MOD = factorial

# 源代码文件 (.c or .py)
SRC = factorial.c

# 要构建的架构 (x86, x64, armv6m, armv7m, xtensa, xtensawin)
ARCH = x64

# Include以获取编译和链接模块的规则
include $(MPY_DIR)/py/dynruntime.mk

编译模块

构建本地.mpy文件所需的先决工具有:

  • MicroPython资源库(至少包括 py/tools/ 目录)。
  • CPython 3 和 pyelftools 库(例如 pip install 'pyelftools>=0.25')。
  • GNU make。
  • 目标架构的C编译器(如果使用C源代码)。
  • 可选 mpy-cross,从MicroPython资源库构建(如果使用 .py 源代码)。

确保为要运行的目标选择正确的ARCH。然后构建:

$ make

在不修改 Makefile 的情况下,可以通过以下方式指定目标架构:

$ make ARCH=armv7m

MicroPython中使用模块

模块构建完成后,会出现一个名为factorial.mpy的文件。复制该文件,使其可在MicroPython系统的文件系统中访问,并可在导入路径中找到。现在可以像访问其他模块一样在 Python 中访问该模块,例如:

import factorial
print(factorial.factorial(10))
# 应该显示 3628800

更多实例

参阅examples/natmod/可以获取更多示例,这些示例展示了本地.mpy模块的许多可用功能。这些功能包括:

  • 使用多个C源文件
  • 包括 Python 代码和C代码
  • 杆状数据和 BSS 数据
  • 内存分配
  • 使用浮点数
  • 异常处理
  • 包括外部C语言库

你可能感兴趣的:(硬件,micropython,python,单片机,嵌入式硬件,物联网)