本帖最后由 正点原子 于 2024-8-16 15:09 编辑
1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729
第七章 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组件扩展的原理及编写流程,还介绍了R.T-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-Windows2,正点原子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-Windows3,摄像头与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软件Shell交互环境下调用正点原子C模块组件并没有提示错误,证明这些C模块的方法及变量可在程序中使用了。
|