本文介绍了在C语言中实现附加功能的选项,但这些功能是通过在MicroPython主资源库之外编写的代码实现的。第一种方法适用于构建定制固件,其中包含一些可从Python访问的特定项目附加模块或函数。第二种方法用于构建可在运行时加载的模块。
有关构建MicroPython主资源库中核心模块的更多信息,请参阅库部分。
在开发用于MicroPython的模块时,可能会遇到Python环境的限制,这通常是由于无法访问某些硬件资源或Python速度限制造成的。
如果限制无法通过MicroPython性能调优中的建议来解决,那么用C语言编写部分或全部模块是一个可行的选择。
如果模块旨在访问或使用常见的硬件或库,建议在MicroPython源代码树中与类似模块一起实现,并将其作为拉取请求提交。但是,如果目标是晦涩难懂或专有的系统,将其保留在MicroPython主资源库之外可能更有意义。
本文将介绍如何将这些外部模块编译到MicroPython可执行文件或固件映像中。Make和CMake两种编译工具都支持,在编写外部模块时,最好为这两种工具添加编译文件,这样模块就可以在所有移植上使用。但在编译某个特定的移植时,您只需使用一种编译方法,即 Make或CMake。
另一种方法是在.mpy
文件中使用本地机器代码,这样就可以编写放置在.mpy
文件中的自定义C代码,并将其动态导入运行中的MicroPython系统而无需重新编译主固件。
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_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
。
如果有自定义编译器选项(如-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++ 代码的一种方法。
要构建这样一个模块,需要编译MicroPython(请参阅源码获取、编译构建),并进行两处修改:
设置联编时标志USER_C_MODULES
,使其指向希望包含的模块。对于使用 Make的移植,这个变量应该是一个可以自动搜索模块的目录。对于使用CMake的移植, 这个变量应该是一个包含要联编的模块的文件,详情请参阅下文。
将相应的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.h
或mpconfigboard.h
以添加:
#define MODULE_CEXAMPLE_ENABLED (1)
请注意,具体方法取决于port,因为它们有不同的结构。如果操作不当,虽然可以编译,但导入时会找不到模块。
一旦内置到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左右
这部分介绍如何构建和处理包含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 变量的有效选项,见下文):
编译和链接本地.mpy
文件时,必须选择体系结构,并且只能在该体系结构上导入相应文件。有关.mpy
文件的更多详情,请参阅MicroPython.mpy
文件。
本地代码必须编译为位置无关代码 (PIC position independent code),并使用全局偏移表 (GOT global offset table),但具体细节因体系结构而异。在导入带有本地代码的.mpy
文件时,导入机制可以对本地代码进行一些基本的重定位,包括重新定位文本、rodata 和 BSS 部分。
链接器和动态加载器支持的功能包括:
已知的限制有:
因此,如果C代码有可写数据,要确保数据是全局定义的,没有初始化器,并且只能在函数中写入。
链接器限制:本地模块不是根据完整MicroPython固件的符号表链接的。相反,它是根据mp_fun_table
(位于py/nativeglue.h
)中的导出符号明确表链接的,该表在固件构建时已固定。因此,不能简单地调用一些任意的HAL/OS/RTOS/system 函数。
可以在表格末尾添加新符号,然后重新构建固件,这些符号也需要添加到相同位置的tools/mpy_ld.py
的fun_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
文件所需的先决工具有:
py/
和 tools/
目录)。pyelftools
库(例如 pip install 'pyelftools>=0.25'
)。mpy-cross
,从MicroPython资源库构建(如果使用 .py 源代码)。确保为要运行的目标选择正确的ARCH
。然后构建:
$ make
在不修改 Makefile 的情况下,可以通过以下方式指定目标架构:
$ make ARCH=armv7m
模块构建完成后,会出现一个名为factorial.mpy
的文件。复制该文件,使其可在MicroPython系统的文件系统中访问,并可在导入路径中找到。现在可以像访问其他模块一样在 Python 中访问该模块,例如:
import factorial
print(factorial.factorial(10))
# 应该显示 3628800
参阅examples/natmod/
可以获取更多示例,这些示例展示了本地.mpy
模块的许多可用功能。这些功能包括: