《ESP32-S3使用指南—MicroPython版 V1.0》第七章 MicroPython组件扩展

第七章 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 模块的方法及变量可在程序中使用


请使用浏览器的分享功能分享到微信等