第七章 MicroPython组件扩展
使用原生的MicroPython进行开发时,可能会感觉束手束脚,就像在狭窄的小路上行走,前方有着各种陡峭的山坡和深邃的沟渠。有时,你会觉得官方提供的功能不足以满足你的需求,或者你发现这些功能并不能完全符合你的工作场景。这时,你就可以选择亲手打造自己的 C 模块,将其融入MicroPython中,仿佛在狭窄的小路上开辟出一条自己的道路,前方是开阔的平原和明亮的阳光。你可以按照自己的想法和需求,设计和实现适合自己的Python函数调用,让它们像你手中的利剑一样,挥舞在代码的世界中。
本章分为如下几个小节:
7.1组件扩展原理
7.2 组件扩展辅助工具
7.3 正点原子的扩展组件
7.4 添加C模块驱动
7.1 组件扩展原理
7.1.1 组件扩展方式
很多人会疑惑,C语言与Python是两种不同的语言,MicroPython如何调用C语言实现的函数在Python下调用的呢?这个问题关键在于,如何使用C语言的形式在MicroPython源代码中表述函数的入参与出参,比如Python实现一个A变量与B变量相加函数,它的实现代码如下:
def add(a, b): return a + b
这个函数有两个入参和一个返回参数,此时如果使用C语言表示该函数的输入输出参数,就可以使用C函数对接到MicroPython当中。在我们讲解C模块原理之前,我们先了解MicroPython C模块的组件扩展方式。
MicroPython C模块有两种组件扩展方式,它们分别为模块扩展,模块+类扩展,这两种形式有什么区别呢?下面作者使用一个示意图来讲解。
图7.1.1.1 组件扩展方式
从上图可知,在main.py文件下可调用两种类型的Python引擎,它们分别为模块扩展,模块+类扩展,下面作者详细说明这两种组件扩展有何区别,如下所示:
①:模块扩展:在这种方式下,扩展组件以模块的形式提供,用户在使用时直接导入模块即可。这种方式比较简单,只需要在模块中定义所需的函数和变量。模块扩展调用示例如下所示:
import cexample cexample.cppfunc() # 调用模块的方法
这种Python引擎类似于在Python环境下新建一个cexample.py脚本,然后,在脚本下利用“de cppfunc”方式(Python定义函数的流程)定义模块的方法,接着,在main.py脚本下导入cexample模块,最后,引导这个模块的方法(函数)。
②:模块+类扩展:在这种方式下,扩展组件通过模块和类的组合进行扩展。用户在使用时需要通过模块导入特定的类,然后使用这个类提供的功能。这种方式相对于模块扩展更加灵活,可以更好地组织代码和提供面向对象的功能。模块+类扩展调用示例如下所示:
from cexample import Timer tr = Timer() # 实例化Timer类对象 tr. time() # 调用Timet对象的方法
这种Python引擎类似于在Python环境下新建一个cexample.py脚本。然后,在脚本下定义一个Timer类对象,在这个类中定义属性与方法,接着,在main.py脚本下引入cexample模块中的Timer类,并实例化Timer类对象,最后,使用实例化Timer对象调用该类的方法与属性。
这两种扩展方式的主要区别在于代码组织和使用方式上。模块扩展方式更简单直接,适合提供一些基础的功能和函数。而模块+类扩展方式更加灵活,可以更好地组织和封装代码,提供更高级的功能和接口。需要注意的是,这两种扩展方式并不是互斥的,可以根据需要同时使用。例如,可以在一个扩展模块中同时使用模块扩展和模块+类扩展方式,以提供更加丰富和灵活的功能。
7.1.2 组件扩展原理解析
到了这里,我们已经了解了C模块组件的扩展方式。接下来,将重点讲解MicroPython提供的三个C模块实例,以便用户将来编写自己的C模块组件。这些实例位于micropython\examples\usercmodule目录下,它们分别是cexample、cppexample和subpackage C模块。
下面,就以cexample C模块为例。首先,打开micropython\examples\usercmodule\cexample目录。该目录包含三个文件,它们共同构成了一个简单的MicroPython C模块。如下图所示。
图7.1.2.1 cexample文件夹目录
上图中的micropython.mk是一个包含此模块的Makefile片段的脚本文件。其中,USERMOD_DIR可用作micropython.mk模块目录的路径。在Makefile中,它应该被扩展为本地make变量,例如:CEXAMPLE_MOD_DIR:=( USERMOD_DIR)。同时,需要将模块源文件添加到SRC_USERMOD的扩展副本中,例如:SRC_USERMOD += $(CEXAMPLE_MOD_DIR)/examplemodule.c。如果存在自定义的“CFLAGS”设置或包括文件夹来定义,这些应该添加到“CFLAGS_USERMOD”中。
# USERMOD_DIR模块目录的路径,如cexample/ CEXAMPLE_MOD_DIR := $(USERMOD_DIR) # 将所有C文件添加到 SRC_USERMOD SRC_USERMOD += $(CEXAMPLE_MOD_DIR)/examplemodule.c # 如果有自定义编译器选项(例如 -I 添加目录以搜索头文件), # 则应将这些选项添加到C代码的CFLAGS_USERMOD和C++代码的CXXFLAGS_USERMOD CFLAGS_USERMOD += -I$(CEXAMPLE_MOD_DIR)
在上面的源码中,我们看到了一个链接命令,它链接了cexample对象文件,最终生成了一个的共享库。这个共享库可以被MicroPython解释器加载和使用。总的来说,micropython.mk对于MicroPython C模块的作用主要是定义构建规则和编译选项,以用于构建和编译C模块。
micropython.cmake是一个CMake配置文件,用于构建MicroPython模块。它包含了构建模块所需的CMake配置指令,例如定义库、添加源文件、设置编译选项等。通过使用CMake,可以方便地构建和管理MicroPython模块的构建过程。如下代码所示:
# 创建一个INTERFACE库 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} ) # 将我们的INTERFACE库链接到usermod目标 target_link_libraries(usermod INTERFACE usermod_cexample)
在CMake配置文件micropython.cmake中,需要定义一个INTERFACE库并将源文件关联起来,然后将其链接到usermod目标。该文件对于MicroPython C模块的作用是定义CMake配置,以确保C模块能够正确地编译和链接成可执行文件或库,并能够在MicroPython环境中正确地运行。
examplemodule.c文件使用了模块和模块+类两种组件扩展方式。下面,作者分别来讲解这两种方式的实现原理。
1,模块扩展实现原理
examplemodule.c文件模块扩展部分代码如下所示:
/* 第一部分:添加所需API的头文件 */ #include "py/runtime.h" #include "py/mphal.h" /* 第二部分:实现功能 */ STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { /* 通过 Python 获取的第一个整形参数 arg_1 */ int a = mp_obj_get_int(a_obj); /* 通过 Python 获取的第二个整形参数 arg_2 */ int b = mp_obj_get_int(b_obj); /* 处理入参 arg_1 和 arg_2,并向 python 返回整形参数 ret_val */ return mp_obj_new_int(a + b); } /* 使用MP_DEFINE_CONST_FUN_OBJ_2宏将函数添加到模块中 */ STATIC MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints); /* 第三部分:将模块注册到模块列表 */ STATIC const mp_rom_map_elem_t example_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) }, { MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) }, }; /* 把模块注册列表注册到example_module_globals字典对象当中 */ STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table); /* 第四部分:定义模块对象 */ const mp_obj_module_t example_user_cmodule = { .base = { &mp_type_module }, .globals = (mp_obj_dict_t *)&example_module_globals,/* 指向字典对象 */ }; /* 第五部分:注册模块以使其在Python中可用 */ MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
上述是examplemodule.c文件的部分代码,它主要阐述了简单MicroPython C模块的模块扩展架构,首先作者把它这个架构划分为四个部分,如下解析:
第一部分:在.c文件下添加microPython相关头文件,可用来引用相关的API函数。
第二部分:加法函数,使用C语言的方式实现功能,但需要使用MicroPython API函数对入参和出参进行转换,入参转换完成后,才能让C语言识别,出参转换成功后才能被MicroPython调用。最后在MP_DEFINE_CONST_FUN_OBJ_2宏的作用下,将函数添加到模块中,以便在 MicroPython 环境中调用。
第三部分:将模块注册到模块列表,然后在MP_DEFINE_CONST_DICT宏的作用下在C 语言中创建一个常量字典,并将其添加到 MicroPython 模块中,以便在 Python 环境中直接访问和使用这些字典。这对于需要在 MicroPython 模块中定义和共享常量字典的情况非常有用。大家不妨回顾一下字典的作用,它通过键来访问字典中的值(这个值可用来指向某个函数的地址,这样我们根据这个地址调用函数了)。
第四部分:创建一个模块对象,然后对象的成员变量globals指向字典对象,接着在MP_REGISTER_MODULE宏的作用下将模块注册到 MicroPython 系统中,使其可以在 Python 环境中被导入和使用。
上述组件扩展方式是模块扩展,我们可在Py脚本下使用模块扩展的形式调用加法函数,如下示例所示:
# 导入模块 import cexample """ * @brief 程序入口 * @param 无 * @retval 无 """ if __name__ == "__main__": a = 5 b = 10 c = cexample.add_ints(a,b) # 调用自定义模块的加法函数 print(c) # 输出C为15
这种模块扩展类似于自己定义一个cexample.py文件,然后,在此文件下实现多个函数,最后,在main.py文件下导入cexample模块,并以模块名引用函数。
2,模块+类扩展原理
examplemodule.c文件模块+类扩展部分代码如下所示:
/* 添加所需API的头文件 */ #include "py/runtime.h" #include "py/mphal.h" /* (2)定义Timer对象实例 */ typedef struct _example_Timer_obj_t { /* 所有对象的基础地址 */ mp_obj_base_t base; /* 开始时间的变量 */ mp_uint_t start_time; } example_Timer_obj_t; /* (3)对象的实现方法(函数) */ STATIC mp_obj_t example_Timer_time(mp_obj_t self_in) { /* 获取Timer句柄 */ example_Timer_obj_t *self = MP_OBJ_TO_PTR(self_in); /* 获取经过的时间,并将其作为MicroPython整数返回 */ mp_uint_t elapsed = mp_hal_ticks_ms() - self->start_time; return mp_obj_new_int_from_uint(elapsed); } /* 使用MP_DEFINE_CONST_FUN_OBJ_1宏将函数添加到模块中 */ STATIC MP_DEFINE_CONST_FUN_OBJ_1(example_Timer_time_obj, example_Timer_time); /* 构造函数 */ STATIC mp_obj_t example_Timer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { /* 分配新对象并设置类型 */ example_Timer_obj_t *self = mp_obj_malloc(example_Timer_obj_t, type); /* 获取开始时间 */ self->start_time = mp_hal_ticks_ms(); /* 返回Timer句柄 */ return MP_OBJ_FROM_PTR(self); } /* 将模块注册到对象的模块列表 */ STATIC const mp_rom_map_elem_t example_Timer_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_time), MP_ROM_PTR(&example_Timer_time_obj) }, }; /* 把字典转换成对象 */ STATIC MP_DEFINE_CONST_DICT(example_Timer_locals_dict, example_Timer_locals_dict_table); /* (1)定义了Timer类 */ MP_DEFINE_CONST_OBJ_TYPE( example_type_Timer, MP_QSTR_Timer, MP_TYPE_FLAG_NONE, make_new, example_Timer_make_new, locals_dict, &example_Timer_locals_dict ); /* 将模块注册到模块列表 */ STATIC const mp_rom_map_elem_t example_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) }, { MP_ROM_QSTR(MP_QSTR_Timer), MP_ROM_PTR(&example_type_Timer) }, }; /* 把模块注册列表注册到example_module_globals字典对象当中 */ STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table); /* 定义模块对象 */ const mp_obj_module_t example_user_cmodule = { .base = { &mp_type_module }, .globals = (mp_obj_dict_t *)&example_module_globals,/* 指向字典对象 */ }; /* 注册模块以使其在Python中可用 */ MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
上述代码也不难理解,我们知道Python语言是面向对象的语言。正因为如此,在Python中创建一个类和对象是很容易的。类是用来描述具有相同的属性(变量)和方法(函数)的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。明白了这点,我们可以去看上述的代码到底怎么实现一个Python类的。
(1)它定义了一个名为“Timer”的类,这个类包含了一个对实例对象的操作example_Timer_make_new,以及一个类的方法(函数)example_Timer_locals_dict;(2)处定义了Timer对象实例,它用来获取开始的运行时间;(3)处就是类的实现方法example_Timer_time即运行的函数。如下是模组+类扩展调用示例,如下代码所示:
from cexample import Timer """ * @brief 程序入口 * @param 无 * @retval 无 """ if __name__ == "__main__": tr = Timer() # 对象实例(调用example_Timer_make_new函数) timer = tr. time() # 调用对象的方法example_Timer_time获取运行时间 print(timer) # 打印运行的时间
从上述源代码可以看出,C模块的模块+类扩展实现方式类似于在定义一个cexample.py文件,并在该文件中实现一个Timer类,然后在该类中实现计算运行时间的方法,最后在main.py文件中导入cexample模块下的Timer类,并使用类名来引用该函数。
7.2 组件扩展辅助工具
R.T-Thread提供的MicroPython C绑定代码自动生成器 是一个非常有用的工具,可以帮助开发者快速将C语言函数或模块集成到MicroPython环境中。通过这个工具,开发者可以轻松添加自己编写的C语言函数或者模块到MicroPython中,并被Python脚本调用。这大大扩展了原版MicroPython的能力,并且可以快速实现任何功能。
使用这个工具,开发者只需要简单几步操作,即可实现添加C绑定的功能。自动生成的C代码形式可以在R.T-Thread的官方文档中找到。这个工具已经经过多次迭代,变得越来越完善,可以轻松加入到工程中。C 绑定代码自动生成器如下所示:
图7.2.1 RTT提供的MicroPython C绑定代码自动生成器
这个辅助工具很简单,只需填写要实现的函数名、传入的参数和函数的返回值类型,即可生成一个MicroPython函数模板。当然,核心代码需要开发者自行编写。
7.3 正点原子的扩展组件
上述章节中,作者简单阐述了MicroPython组件扩展的原理及编写流程,还介绍了RT-Thread提供的MicroPython C绑定代码自动生成器。通过这个工具,开发者可以轻松添加自己编写的C语言函数或者模块到MicroPython中。下面作者介绍正点原子提供的扩展组件有哪些,如下表所示:
表7.3.1 正点原子提供MicroPython 扩展组件功能描述
在前面的讨论中,我们已经了解到,当MicroPython官方实现的功能无法满足开发者的需求时,或者当这些实现不符合工作需求时,可以使用扩展组件的方式设计开发者的功能。为了满足正点原子ESP32-S3开发板的特定外设以及让读者更好地熟悉C模块,正点原子制定了一套自己的扩展组件,以适应开发板的需求。这些扩展组件是根据板载器件的特点和需求而设计的。
在前面的章节中,作者已经介绍了扩展组件的架构,并阐述了实现原理。正点原子提供的扩展组件代码也是基于这个框架进行编写的。因此,在这里作者不再重复讲解。这些扩展组件已经放置在资料盘中,有兴趣的读者可以参考借鉴。
7.4 添加C模块驱动
在上小节中,作者简单讲解了正点原子MicroPython扩展组件,那么我们怎么把这些组件编译到MicroPython固件当中呢?为了让读者更好地熟悉这个流程,作者以流程的方式来讲述。
1,把扩展组件复制到MicroPython源代码
正点原子的扩展组件在A盘à6,软件资料→1,软件→2,MicroPython开发工具→01-Windows→2,正点原子MicroPython驱动àCModules_Lib路径下找到,这些组件如下图所示。
图7.4.1 正点原子MicroPython扩展组件
在上图中,我们复制CAMERA、ESP-WHO、IIC、LCD、RGBLCD、SENSOR文件夹至D:\Ubuntu_WSL\rootfs\root\micropython\examples\usercmodule\BSP目录下,如下图所示。
图7.4.2 添加扩展组件(BSP文件夹需用户创建)
2,修改usercmodule目录下的micropython.cmake
修改micropython.cmake文件,添加编译扩展组件,如下代码所示。
# This top-level micropython.cmake is responsible for listing # the individual modules we want to include. # Paths are absolute, and ${CMAKE_CURRENT_LIST_DIR} can be # used to prefix subdirectories. # Add the C example. #include(${CMAKE_CURRENT_LIST_DIR}/cexample/micropython.cmake) # Add the CPP example. #include(${CMAKE_CURRENT_LIST_DIR}/cppexample/micropython.cmake) # 添加 IIC驱动 include(${CMAKE_CURRENT_LIST_DIR}/BSP/IIC/micropython.cmake) # 添加 CAMERA驱动 include(${CMAKE_CURRENT_LIST_DIR}/BSP/CAMERA/micropython.cmake) # 添加 SPI LCD驱动 include(${CMAKE_CURRENT_LIST_DIR}/BSP/LCD/micropython.cmake) # 添加ESP_WHO乐鑫AI驱动 #include(${CMAKE_CURRENT_LIST_DIR}/BSP/ESP-WHO/micropython.cmake) # 添加 SENSOR驱动 include(${CMAKE_CURRENT_LIST_DIR}/BSP/SENSOR/micropython.cmake) # 添加 RGBLCD驱动 include(${CMAKE_CURRENT_LIST_DIR}/BSP/RGBLCD/micropython.cmake)
请注意,当用户添加正点原子扩展C模块组件时,有一个重要的点需要关注:ESP_WHO和LCD扩展组件不能同时编译。这是因为ESP_WHO已经包含了LCD驱动,所以这两者不能同时使用。考虑到ESP_WHO编译出来的固件体积较大,作者提供了两种固件:一种是未使用AI库的固件,另一种是包含AI库的固件。
3,添加乐鑫摄像头驱动库和乐鑫AI库
前面我们知道,MicroPython固件是从ESP-IDF库的基础上编译出来的,由于正点原子ESP32-S3开发板板载了摄像头接口及又想实现AI检测,所以MicroPython固件需编译ESP-IDF摄像头驱动库和AI库。这两个库,开发者可在Github仓库搜索找到。如下图所示:
图7.4.3 摄像头驱动库和AI库
这两个库下载完成之后,把它们移植到子系统esp-idf库的components目录中,如下图所示。
图7.4.4 添加驱动库
值得注意的是,作者对上图的modules文件夹进行了删减。读者可以对比原始的modules文件,在esp-dl/include/路径下,添加esp_timer.h文件,以防止编译报错。该文件可以在esp-idf/components/esp_timer/include/路径下找到。
上图的五个文件夹已经放置在A盘→6,软件资料→1,软件→2,MicroPython开发工具→01-Windows→3,摄像头与AI库目录下,开发者可直接解压并拷贝到自己的子系统esp-idf/components目录下。
3,编译固件
首先,打开Ubuntu 22.04.2 LTS子系统,然后使用cd命令进入micropython/ports/esp32/目录,如下图所示。
图7.4.5 进入esp32编译目录
然后在此目录下输入“make USER_C_MODULES=~/micropython/examples/usercmodule/micropython.cmake BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT”命令编译固件。编译成功后如下图所示。
图7.4.6 添加C模块固件编译完成
4,测试C模块
把固件下载至开发板,下载流程请看前面的章节。下载完成后,导入C模块驱动库,如下图所示。
图7.4.7 测试C模块是否导入成功
从上图可以看到,在 Thonny 软件 S h ell 交互环境下调用正点原子 C 模块组件并没有提示错误,证明这 些 C 模块的方法及变量可在程序中使用 了 。