IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> MicroPython添加模块 -> 正文阅读

[Python知识库]MicroPython添加模块

MicroPython添加Module

官方文档MicroPython external C modules中对这部分有说明, git上也有说明。

1. 背景

摘自:MircoPython 的组件扩展方法

本节内容将详细介绍如何扩展一个新的MicroPython 的组件。组件扩展方式分两种:模块扩展模块+类扩展
右边netmgr 功能以模块的方式扩展的,在使用的时候直接导入模块进行使用。左边的ADC 是通过模块+类的方式进行扩展,使用的时候需要通过模块导入ADC类进行使用。

在这里插入图片描述

组件扩展过程中会用到函数定义、参数类型转及如何通过Python呼叫C语言代码等功能,下面是对这些功能的说明。

2.基础知识

组件扩展过程中会用到函数定义、参数类型转及如何通过Python呼叫C语言代码等功能,下面是对这些功能的说明。

大体上分为三个层次的封装, 对应了三个封装的宏操作:

MP_DEFINE_CONST_FUN_OBJ_0. 将函数封装成对象. 在python中, 一切皆是对象.
MP_DEFINE_CONST_DICT. 将所有的函数对象封装成一个操作清单.
MP_REGISTER_MODULE. 将操作清单和对象绑定在一起, 注册到python系统中.

2.1 函数和参数定义方式

将函数封装成对象

MP_DEFINE_CONST_FUN_OBJ_0(obj_name, fun_name)            //表示函数无参数
#define MP_DEFINE_CONST_FUN_OBJ_1(obj_name, fun_name)     // 表示函数有一个参数 
#define MP_DEFINE_CONST_FUN_OBJ_2(obj_name, fun_name)     // 表示函数有两个参数
#define MP_DEFINE_CONST_FUN_OBJ_3(obj_name, fun_name)     //表示函数有三个参数
#define MP_DEFINE_CONST_FUN_OBJ_VAR(obj_name, n_args_min, fun_name)    // n_args_min 最小参数个数
#define MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(obj_name, n_args_min, n_args_max, fun_name)  // n_args_min, n_args_max 表示函数的参数个数范围

2.2 类型定义和转换

MP_OBJ_NEW_SMALL_INT(small_int)         //构造int 类型的obj 对象
MP_OBJ_NEW_QSTR(qst)                   // 构造qstr data类型的obj 对象
MP_OBJ_NEW_IMMEDIATE_OBJ(val)        // 构造immediate类型的obj 对象
MP_ROM_INT(i)                            //构造Int类型的obj对象
MP_ROM_QSTR(q)                         //构造qstr data 类型的obj对象
MP_ROM_PTR                             //构造存储指针的obj对象
MP_OBJ_TO_PTR(o)                       //将obj 对象转换成obj指针
MP_OBJ_FROM_PTR(p)                    //将obj指针转换成obj对象
MP_ROM_NONE                           //构造空的obj对象
MP_ROM_FALSE                          //构造false值的obj对象
MP_ROM_TRUE                           //构造true值的obj对象

2.3 Python参数转换成C参数方法

mp_int_t mp_obj_get_int(mp_const_obj_t arg)             //将int类型的obj 参数转换成int 类型
mp_float_t mp_obj_get_float(mp_obj_t self_in)             //将float类型的obj参数转换成float类型
const char *mp_obj_get_type_str(mp_const_obj_t o_in)      //将str 类型的obj参数转换成char类型

3.模块扩展

下面以netmgr功能为例讲解模块扩展的方式具体是怎么实现的。
代码路径:
./components/py_engine/modules/netmgr/modnetmgr.c
首先通过Python 引擎的mp_obj_module_t数据结构定义netmgr 模块。其中base成员是mp_obj_base_t类型的,一般作为Python对象的第一个成员变量。
globals成员是mp_obj_dict_t类型的,里面存储的是功能映射关系。

const mp_obj_module_t netmgr_module = {                                                   	.base = {&mp_type_module},                                                             	 .globals = (mp_obj_dict_t *)&netmgr_module_globals, 
};

映射表netmgr_module_globals包含多个函数功能:

STATIC const mp_rom_map_elem_t netmgr_module_globals_table[] = {
    {MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_netmgr)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_init), MP_ROM_PTR(&netmgr_obj_init)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_getInfo), MP_ROM_PTR(&netmgr_obj_get_info)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_getType), MP_ROM_PTR(&netmgr_obj_get_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_getStatus), MP_ROM_PTR(&netmgr_obj_get_status)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_connect), MP_ROM_PTR(&netmgr_obj_connect_wifi)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_disconnect), MP_ROM_PTR(&netmgr_obj_disconnect_wifi)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_on), MP_ROM_PTR(&netmgr_obj_on)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_register_call_back), MP_ROM_PTR(&mp_wifi_register_call_back)},};

其中负责网络连线功能的函数是mp_obj_t connect_wifi,该函数有两个参数,分别为Wi-Fi路由器名称(ssid)和路由器密码(pwd),通过这两个参数将Python代码中设定的路由器名称和密码传到C代码。拿到参数后,通过mp_obj_str_get_str函数将 mp_obj 转换成 char 类型。然后呼叫 os_task_new 创建一个名为wifi_connect_task的线程完成Wi-Fi连线功能。
下面是mp_obj_t_connect_wifi的C代码的具体实现:

STATIC mp_obj_t connect_wifi(mp_obj_t ssid,mp_obj_t pwd) {
    char* _ssid = mp_obj_str_get_str(ssid);   // 获取SSID字串
    char* _pwd = mp_obj_str_get_str(pwd);  // 获取密码字串
    netmgr_wifi_connect_params_t *params;
    params = (netmgr_wifi_connect_params_t*) malloc(sizeof(netmgr_wifi_connect_params_t));
    if(params == NULL) {
        LOGE(LOG_TAG, "%s:%d malloc failed\n", __func__, __LINE__);
        return mp_obj_new_int(-1);
    }
    memset(params, 0, sizeof(netmgr_wifi_connect_params_t));
    strncpy(params->ssid, _ssid, sizeof(params->ssid)-1); 
    params->timeout = 18000;// 设置Wi-Fi连线超时时间为1800 ms
    strncpy((char* )params->pwd, _pwd, sizeof(params->pwd)-1); 
    aos_task_new("wifi_connect_task",wifi_connect_handle, params, 4*1024); // 创建新线程进行Wi-Fi连线操作
    return mp_obj_new_int(0);
}
MP_DEFINE_CONST_FUN_OBJ_2(netmgr_obj_connect_wifi, connect_wifi);

netmgr组件扩展成Python联网模块之后就可以在Python应用程序中调用其提供的PythonAPI了,具体使用方法如下面代码所示:

import netmgr as nm              # 导入netmgr模块,为该模块取别名为nm,后续访问此模块功能均通过nm
import utime as time              # 导入utime模块,为该模块取名为time
import sys                       # 导入sys库
nm.init()                         # netmgr组件初始化
connected = nm.getStatus()       # 获取Wi-Fi连接状态
def on_wifi_connected(status):    # 定义Wi-Fi连线成功的回调函数
    global connected
    print('*******wifi connected*********')
    connected = True            # 设置connected变量为True
if  not connected:               # 如果连线没有成功
    nm.register_call_back(1,on_wifi_connected)   # 向nm组件注册连线成功回调函数
    if(len(sys.argv) == 3):
        nm.connect(sys.argv[1],sys.argv[2])      # 如果执行此python脚本的时候同时输入了Wi-Fi SSID和密码作为参数则连接用户输入的指定路由器
    else:
        nm.connect("KIDS","12345678")         # 如果执行此python脚本的时候没有输入Wi-Fi SSID和密码,则连接预设的名为KIDS的路由器
while True :                                    # 等待Wi-Fi连接成功
    if connected:
        break                                 # 连线成功则调出此循环
    else:
        print('Wait for wifi connected')
        time.sleep(1)                          # 没有连线成功则打印日志并休眠1秒
if nm.getStatus():
    print('DeviceIP:' + nm.getInfo()['IP'])         # 连线成功则通过呼叫getInfo获取IP地址信息
else:
    print('DeviceIP:get failed')
print("ConnectWifi finished")

4.模块+类扩展

下面以ADC功能为例来讲解如何通过模块+类的扩展方式扩展一个Python类。
相关代码路径如下:
●模块代码:
components/py_engine/adapter/haas/moddriver.c
●类代码:
components/py_engine/modules/driver/adc.c
首先要通过Python引擎的mp_obj_module_t数据结构定义一个driver_module模块。

const mp_obj_module_t driver_module = {
    .base = {&mp_type_module},
    .globals = (mp_obj_dict_t *)&driver_locals_dict,
};

然后通过数据结构mp_rom_map_elem_t 定义此模块对应的类表,截止本文撰写的时候driver模块支持的类有:ADC、PWM、GPIO、I2C、UART、SPI、RTC、TIMER及DAC等。

STATIC const mp_rom_map_elem_t driver_locals_dict_table[] = {
    {MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_driver)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_ADC), MP_ROM_PTR(&driver_adc_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_PWM), MP_ROM_PTR(&driver_pwm_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_GPIO), MP_ROM_PTR(&driver_gpio_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&driver_i2c_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_UART), MP_ROM_PTR(&driver_uart_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&driver_spi_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_RTC), MP_ROM_PTR(&driver_rtc_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_TIMER), MP_ROM_PTR(&driver_timer_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_CAN), MP_ROM_PTR(&driver_can_type)},
    //{MP_OBJ_NEW_QSTR(MP_QSTR_DAC), MP_ROM_PTR(&driver_dac_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_IR), MP_ROM_PTR(&driver_ir_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_WDT), MP_ROM_PTR(&driver_wdt_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_KeyPad), MP_ROM_PTR(&driver_keypad_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_Location), MP_ROM_PTR(&driver_location_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_UND), MP_ROM_PTR(&driver_und_type)},
    {MP_OBJ_NEW_QSTR(MP_QSTR_Crypto), MP_ROM_PTR(&driver_crypto_type)},
};

接下来需要通过Python引擎的mp_obj_type_t数据结构定义driver_adc_type模块,此结构体的成员主要包含构造函数,打印函数,功能映射表等信息。

const mp_obj_type_t driver_adc_type = {
    .base = {&mp_type_type}, 
    .name = MP_QSTR_ADC,                        // ADC 模块名称
    .print = adc_obj_print,                            // 打印函数
    .make_new = adc_obj_make_new,                // 构造函数
    .locals_dict = (mp_obj_dict_t *)&adc_locals_dict,    // 功能映射表
};

类和模块一样,也是通过数据结构mp_rom_map_elem_t来定义函数功能表。

STATIC const mp_rom_map_elem_t adc_locals_dict_table[] = {
    {MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ADC)},
    {MP_ROM_QSTR(MP_QSTR_open), MP_ROM_PTR(&adc_obj_open)},
    {MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&adc_obj_close)},
    {MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&adc_obj_read)},
};

下面是通过ADC进行读操作的具体函数实现:

STATIC mp_obj_t obj_read(size_t n_args, const mp_obj_t *args)
{
    LOGD(LOG_TAG, "entern  %s; n_args = %d;\n", __func__, n_args);
    int ret = -1;
    adc_dev_t *adc_device = NULL;
    int32_t adc_value = -1;
    if (n_args < 1)
    {
        LOGE(LOG_TAG, "%s: args num is illegal :n_args = %d;\n", __func__, n_args);
        return mp_const_none;
    }
    mp_obj_base_t *self = (mp_obj_base_t*)MP_OBJ_TO_PTR(args[0]);
    mp_adc_obj_t* driver_obj = (mp_adc_obj_t *)self;
    if (driver_obj == NULL)
    {
        LOGE(LOG_TAG, "driver_obj is NULL\n");
        return mp_const_none;
    }      
    adc_device = py_board_get_node_by_handle(MODULE_ADC, &(driver_obj->adc_handle)); // 获取ADC设备的指针
    if (NULL == adc_device) {
        LOGE(LOG_TAG, "%s: py_board_get_node_by_handle failed;\n", __func__);
        return mp_const_none;
    }
    (void)aos_hal_adc_value_get(adc_device, (void *)&adc_value, 0);             // 呼叫C语言进行ACD读操作的API
    LOGD(LOG_TAG, "%s:out adc_value = %d;\n", __func__, adc_value);
    return MP_ROM_INT(adc_value);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(adc_obj_read, 1, obj_read);

? 在driver模块中扩展ADC类之后,在Python应用层代码中使用ADC类的案例如下:

from driver import ADC    # 从driver库中导入ADC类
adc = ADC()             # 新建一个ADC设备对象
adc.open("ADC0")       # 打开ADC的通道0
value = adc.read()       # 进行ADC读操作
print(value)
adc.close()             # 关闭ADC对象(即关闭ADC通道0)

5.分析官方的代码

官方代码

基础例程的位置

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

将C模块编译进MP

新建文件

在ports/stm32 文件夹下新建一个文件examplemodule.c , 内容为examplemodule.c

文件examplemodule.c 内创建了一个example_user_cmodule的模块,它包含了一个funtion和一个type,type定义在其他文件内。

// Include MicroPython API.
#include "py/runtime.h"

// This is the function which will be called from Python as cexample.add_ints(a, b).
STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
    // Extract the ints from the micropython input objects.
    int a = mp_obj_get_int(a_obj);
    int b = mp_obj_get_int(b_obj);

    // Calculate the addition and convert to MicroPython object.
    return mp_obj_new_int(a + b);
}
// Define a Python reference to the function above.
STATIC MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);
// 将函数封装成对象. 在python中, 一切皆是对象
// Define all properties of the module.
// Table entries are key/value pairs of the attribute name (a string)
// and the MicroPython object reference.
// All identifiers and strings are written as MP_QSTR_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
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) },
};
STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table);
//将所有的函数对象封装成一个操作清单.
// Define module object.
const mp_obj_module_t example_user_cmodule = {  //使用类型定义了一个`example_user_cmodule`
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&example_module_globals,
}; //初始化了相关的部分,如基本类型、模块的字典 在字典中,定义了`__name__` 为"example"

// Register the module to make it available in Python.
// Note: the "1" in the third argument means this module is always enabled.
// This "1" can be optionally replaced with a macro like MODULE_CEXAMPLE_ENABLED
// which can then be used to conditionally enable this module.
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule, 1);//将操作清单和对象绑定在一起, 注册到python系统中.

上面这部分代码使用mp_obj_module_t类型定义了一个example_user_cmodule,并初始化了相关的部分,如基本类型、模块的字典。在字典中,定义了__name__,为machine,这个就是我们这个module的名字。

在mpconfigport.h文件中注册新模块

为了让这个模块能在MicroPython中被import,需要将模块添加到mpconfigport.hMICROPY_PORT_BUILTIN_MODULES中。

要把定义的module注册到micropython中去,这个是在mpconfigport.h文件中修改,找到MICROPY_PORT_BUILTIN_MODULES 定义的地方按照格式添加定义的module:

extern const struct _mp_obj_module_t example_user_cmodule;

#define MICROPY_PORT_BUILTIN_MODULES \
    { MP_ROM_QSTR(MP_QSTR_cexample), MP_ROM_PTR(&example_user_cmodule) }, \

在Makefile文件中更新SRC_C和SRC_QSTR

在SRC_C中添加examplemodule.c文件, 将新增代码编译firmware中.

在SRC_QSTR中添加examplemodule.c文件, 让build过程扫描examplemodule.c文件, 从中提取字符串关键字增加到micropython的qstr列表当中.

第二步操作是根据官方文档的说明进行正式的添加qstr

按照官方开发文档的说明, 到这里就可以重新 make , 然后可以在 micropython 的交互命令行中引用example_user_cmodule模块的.

编译并烧录,在MicroPython中使用import cexample测试是否成功,不报错就是成功了。

6. 新建一个machine模块(LPC1788 无参数)

参考MicroPython移植LPC1788添加module

创建machine.c

#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"

STATIC const mp_rom_map_elem_t machine_module_globals_table[] = {
    {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_machine)},
};

STATIC MP_DEFINE_CONST_DICT(machine_module_globals, machine_module_globals_table);

const mp_obj_module_t machine_module = {
    .base = {&mp_type_module},
    .globals = (mp_obj_dict_t *)&machine_module_globals,
};

模块添加到mpconfigport

为了让这个模块能在MicroPython中被import,需要将模块添加到 mpconfigport.hMICROPY_PORT_BUILTIN_MODULES中。

// extra built in modules to add to the list of known ones
extern const struct _mp_obj_module_t machine_module;  // 注意_mp_obj_module_t是带下划线前缀的

#define MICROPY_PORT_BUILTIN_MODULES \
    {MP_ROM_QSTR(MP_QSTR_machine), MP_ROM_PTR(&machine_module)},

编译并烧录

在MicroPython中使用import machine测试是否成功,不报错就是成功了。

添加一个Function

现在,我们可以添加一个函数到这个模块里面。首先定义一个函数,这里以reset函数来具体说明。

STATIC mp_obj_t machine_reset(void)
{
    NVIC_SystemReset();
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(machine_reset_obj, machine_reset);

这段代码创建了一个名为machine_reset_obj的函数对象,该函数没有参数,被调用时会执行C函数machine_reset。添加一个函数时使用了MP_DEFINE_CONST_FUN_OBJ_0宏来定义一个不带参数的函数。同样也可以定义一个或多个参数的函数。此外,所有的Python函数都必须返回一个mp_obj_t结构对象,但是我们这没有什么可返回的,因此返回None

接下来将这个函数添加到前面创建好的module中。

STATIC const mp_rom_map_elem_t machine_module_globals_table[] = {
    {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_machine)},
    {MP_ROM_QSTR(MP_QSTR_reset), MP_ROM_PTR(&machine_reset_obj)},
};

7. 新建一个led模块(lpc5500 无参数)

为micropython添加led模块

创建一个LED灯的模块, 并包含on()和off()两个函数对应开灯和关灯两个动作。

为新模块创建一个源文件

参考官方样例的命名规范, 这里在lpc5500移植项目的目录下创建mod_led.c

PS: 原始的命名范例中没有使用下划线"_"将前缀"mod"单独分隔出来, 但我通过阅读代码发现, 增加下划线的命名方式更符合micropython的命名规范. 实际上, 在micropython的源代码中, 都是使用下划线作为命名单词的分隔符的. 我有点不明白为什么文件的命令没有使用分隔符. 按照我的开发习惯, 在规模比较大的软件项目中, 使用分隔符的命名方式可读性更好, 所以在我自己的练习代码中, 将会使用下划线作为名字之间的分隔符.

在新模块中首先编写最基本的led模块对应底层驱动的三个函数:

hw_led_init()
hw_led_on()
hw_led_off()

目前先做一个最简单的样例, 实现从python到c语言函数的调用. 目前的样例中仅仅传递函数指针, 不传入任何参数, 在后续的文章中将专门探讨传递参数的问题.

逐层封装

毕竟使用了armgcc编译器, 底层的代码还是以C语言方式运行的, 从python到底层的C就是层层调用. 反过来在开发过程中, 准备好底层的C代码之后, 想要在python层面上被识别, 就需要层层封装并注册.

从官方的样例代码中可以看到, 大体上分为三个层次的封装, 对应了三个封装的宏操作:

MP_DEFINE_CONST_FUN_OBJ_0. 将函数封装成对象. 在python中, 一切皆是对象.
MP_DEFINE_CONST_DICT. 将所有的函数对象封装成一个操作清单.
MP_REGISTER_MODULE. 将操作清单和对象绑定在一起, 注册到python系统中.

到目前为止, 完整的mod_led.c源文件内容如下:

/* mod_led.c */
#include "py/runtime.h"

#include "fsl_common.h"
#include "fsl_iocon.h"
#include "fsl_clock.h"
#include "fsl_gpio.h"

/******************************************************************************
 * hardware level functions.
 *****************************************************************************/
void hw_led_init(void)
{
    CLOCK_EnableClock(kCLOCK_Iocon);
    CLOCK_EnableClock(kCLOCK_Gpio1);

    uint32_t pinmode = IOCON_FUNC0
                     | IOCON_MODE_INACT
                     | IOCON_GPIO_MODE
                     | IOCON_DIGITAL_EN
                     ;
    IOCON_PinMuxSet(IOCON, 1u, 6u, pinmode); /* pio1_6. */

    gpio_pin_config_t gpio_pin_config;
    gpio_pin_config.pinDirection = kGPIO_DigitalOutput;
    gpio_pin_config.outputLogic = 1u;
    GPIO_PinInit(GPIO, 1u, 6u, &gpio_pin_config);
}

void hw_led_on(void)
{
    GPIO_PinWrite(GPIO, 1u, 6u, 0u);
}

void hw_led_off(void)
{
    GPIO_PinWrite(GPIO, 1u, 6u, 1u);
}

/******************************************************************************
 * object function wrappers.
 *****************************************************************************/
STATIC mp_obj_t led_init_func(void)
{
    hw_led_init();
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(led_init_obj, led_init_func);

STATIC mp_obj_t led_on_func(void)
{
    hw_led_on();
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(led_on_obj, led_on_func);

STATIC mp_obj_t led_off_func(void)
{
    hw_led_off();
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(led_off_obj, led_off_func);

/******************************************************************************
 * pack the objects into module.
 *****************************************************************************/
STATIC const mp_rom_map_elem_t led_module_globals_table[] = {
    { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_led) },
    { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&led_init_obj) },
    { MP_ROM_QSTR(MP_QSTR_on)  , MP_ROM_PTR(&led_on_obj)   },
    { MP_ROM_QSTR(MP_QSTR_off) , MP_ROM_PTR(&led_off_obj)  },
};
STATIC MP_DEFINE_CONST_DICT(led_module_globals, led_module_globals_table);

const mp_obj_module_t led_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&led_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_led, led_module, 1);
/* EOF. */

在mpconfigport.h文件中注册新模块

在mpconfigport.h文件中添加代码如下:

extern const struct _mp_obj_module_t led_module;

#define MICROPY_PORT_BUILTIN_MODULES \
    { MP_ROM_QSTR(MP_QSTR_led), MP_ROM_PTR(&led_module) }, \

在Makefile文件中更新SRC_C和SRC_QSTR

在SRC_C中添加mod_led.c文件, 将新增代码编译firmware中.

在SRC_QSTR中添加mod_led.c文件, 让build过程扫描mymodule.c文件, 从中提取字符串关键字增加到micropython的qstr列表当中.

第二步操作是根据官方文档的说明进行的操作, 应该是比较正式的添加qstr的方式.

按照官方开发文档的说明, 到这里就可以重新 make , 然后可以在 micropython 的交互命令行中引用led模块的.

编译并烧录,在MicroPython中使用import led测试是否成功,不报错就是成功了。

通过编译之后, 下载firmware到电路板上, 复位后通过终端命令行交互, 能够成功识别led模块, 并且在板子上确实看到led灯受控亮灭了.

8.新建mymodule (esp32 1个参数)

MicroPython 添加 mymodule

mymodule.c

#include "stdint.h"
#include "stdio.h"
#include "py/obj.h"
#include "py/runtime.h"

STATIC mp_obj_t mp_my_test_function(mp_obj_t myms)
 {
     uint32_t Myms = mp_obj_get_int(myms);
     vTaskDelay(Myms / portTICK_PERIOD_MS);
 	return mp_const_none;
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(My_mp_my_test_function, mp_my_test_function);

STATIC const mp_rom_map_elem_t modnormal_globals_table[] = {
    {MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_my_test_module)},   
    {MP_OBJ_NEW_QSTR(MP_QSTR_mytestfunction), MP_ROM_PTR(&My_mp_my_test_function)}, 
    {MP_OBJ_NEW_QSTR(MP_QSTR_test), MP_ROM_PTR(&modnormal_test_type)},

};

STATIC MP_DEFINE_CONST_DICT(mp_module_modnormal_globals, modnormal_globals_table);   
const mp_obj_module_t mp_module_my_test_mod = {
    .base = {&mp_type_module},    
    .globals = (mp_obj_dict_t *)&mp_module_modnormal_globals,
};

文件mymodule.c 内创建了一个mp_module_my_test_mod的模块,它包含了一个funtion和一个type,type定义在其他文件内。

注册module

我们要把我们定义的module注册到micropython中去,这个是在mpconfigport.h文件中修改,找到MICROPY_PORT_BUILTIN_MODULES 定义的地方按照格式添加我们定义的module:

// extra built in modules to add to the list of known ones
extern const struct _mp_obj_module_t esp_module;
extern const struct _mp_obj_module_t esp32_module;
extern const struct _mp_obj_module_t utime_module;
extern const struct _mp_obj_module_t uos_module;
extern const struct _mp_obj_module_t mp_module_usocket;
extern const struct _mp_obj_module_t mp_module_machine;
extern const struct _mp_obj_module_t mp_module_network;
extern const struct _mp_obj_module_t mp_module_onewire;
extern const struct _mp_obj_module_t mp_module_my_test_mod;//声明模块
#define MICROPY_PORT_BUILTIN_MODULES \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp), (mp_obj_t)&esp_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp32), (mp_obj_t)&esp32_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_utime), (mp_obj_t)&utime_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_uos), (mp_obj_t)&uos_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_usocket), (mp_obj_t)&mp_module_usocket }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_machine), (mp_obj_t)&mp_module_machine }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_network), (mp_obj_t)&mp_module_network }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR__onewire), (mp_obj_t)&mp_module_onewire }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_uhashlib), (mp_obj_t)&mp_module_uhashlib }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_my_test_module), (mp_obj_t)&mp_module_my_test_mod }, \

这里就添加两行代码,第一行是应用外部定义的结构体时要先有声明。第二个需要解释一下,MP_QSTR_my_test_module这个名字要改成自定义的,MP_QSTR_这个一定要保留,后面的my_test_module就是在python中显示的module名字。

在Makefile文件中更新SRC_C和SRC_QSTR

在SRC_C中添加mymodule.c文件, 将新增代码编译firmware中.

在SRC_QSTR中添加mymodule.c文件, 让build过程扫描mymodule.c文件, 从中提取字符串关键字增加到micropython的qstr列表当中.

第二步操作是根据官方文档的说明进行的操作, 应该是比较正式的添加qstr的方式. ??然后再修改一下Makefile文件,将mymodule.c编译进去就可以了。

按照官方开发文档的说明, 到这里就可以重新 make , 然后可以在 micropython 的交互命令行中引用led模块的.

编译并烧录,在MicroPython中使用import led测试是否成功,不报错就是成功了。

通过编译之后, 下载firmware到电路板上, 复位后通过终端命令行交互, 能够成功识别led模块, 并且在板子上确实看到led灯受控亮灭了.

9.向固件里面添加module的原理

在文章C语言开发MicroPython模块(模块框架)里面已经介绍了,编译进内核的模块(module)有四种类型,MicroPython将它们组成四个不同的模块集合,并且定义了不同的名字。

摘自:向固件里面添加module的原理

2.1、模块集合

以我们编写的和硬件平台密切相关的 module集合mp_builtin_module_map为例,在objmodule.c文件内有

MP_DEFINE_CONST_MAP(mp_builtin_module_map, mp_builtin_module_table);
1

其中mp_builtin_module_map就是这个被编译进内核的模块集合的名字,mp_builtin_module_table是存储模块的结构体数组。??MP_DEFINE_CONST_MAP的定义在obj.h文件内,内容如下:

#define MP_DEFINE_CONST_MAP(map_name, table_name) \
    const mp_map_t map_name = { \
        .all_keys_are_qstrs = 1, \
        .is_fixed = 1, \
        .is_ordered = 1, \
        .used = MP_ARRAY_SIZE(table_name), \
        .alloc = MP_ARRAY_SIZE(table_name), \
        .table = (mp_map_elem_t*)(mp_rom_map_elem_t*)table_name, \
    }

宏定义MP_DEFINE_CONST_MAP就是定义了一个结构体,这个结构体的内容为mp_map_t

// TODO maybe have a truncated mp_map_t for fixed tables, since alloc=used
// put alloc last in the structure, so the truncated version does not need it
// this would save 1 ROM word for all ROM objects that have a locals_dict
// would also need a trucated dict structure

typedef struct _mp_map_t {
    size_t all_keys_are_qstrs : 1;
    size_t is_fixed : 1;    // a fixed array that can't be modified; must also be ordered
    size_t is_ordered : 1;  // an ordered array
    size_t used : (8 * sizeof(size_t) - 3);
    size_t alloc;
    mp_map_elem_t *table;
} mp_map_t;

MP_ARRAY_SIZE的定义为:

// get the number of elements in a fixed-size array
#define MP_ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
12

说明结构体的alloc是模块集合的指针合所占的长度,table是一个指针,这个指针指向了mp_builtin_module_table这个结构体数组。在.table = (mp_map_elem_t*)(mp_rom_map_elem_t*)table_name, \这行代码里面mp_rom_map_elem_t和mp_map_elem_t的定义为:

// Underlying map/hash table implementation (not dict object or map function)

typedef struct _mp_map_elem_t {
    mp_obj_t key;
    mp_obj_t value;
} mp_map_elem_t;

typedef struct _mp_rom_map_elem_t {
    mp_rom_obj_t key;
    mp_rom_obj_t value;
} mp_rom_map_elem_t;
1234567891011

而mp_obj_t和mp_rom_obj_t的定义为:

typedef void *mp_obj_t;
typedef const void *mp_const_obj_t;
typedef mp_const_obj_t mp_rom_obj_t;
123

所以名字为mp_builtin_module_map的这个模块集合在最终包含了一系列的指针,mp_builtin_module_table这个结构体数组内元素。而mp_builtin_module_table这个结构体数组内的元素则是一个一个的模块。

2.2、模块数组

mp_builtin_module_table的定义如下:

/******************************************************************************/
// Global module table and related functions

STATIC const mp_rom_map_elem_t mp_builtin_module_table[] = {
    { MP_ROM_QSTR(MP_QSTR___main__), MP_ROM_PTR(&mp_module___main__) },
    { MP_ROM_QSTR(MP_QSTR_builtins), MP_ROM_PTR(&mp_module_builtins) },
    { MP_ROM_QSTR(MP_QSTR_micropython), MP_ROM_PTR(&mp_module_micropython) },

#if MICROPY_PY_ARRAY
    { MP_ROM_QSTR(MP_QSTR_array), MP_ROM_PTR(&mp_module_array) },
#endif
#if MICROPY_PY_IO
    { MP_ROM_QSTR(MP_QSTR_uio), MP_ROM_PTR(&mp_module_io) },
#endif
#if MICROPY_PY_COLLECTIONS
    { MP_ROM_QSTR(MP_QSTR_ucollections), MP_ROM_PTR(&mp_module_collections) },
#endif
#if MICROPY_PY_STRUCT
    { MP_ROM_QSTR(MP_QSTR_ustruct), MP_ROM_PTR(&mp_module_ustruct) },
#endif

#if MICROPY_PY_BUILTINS_FLOAT
#if MICROPY_PY_MATH
    { MP_ROM_QSTR(MP_QSTR_math), MP_ROM_PTR(&mp_module_math) },
#endif
#if MICROPY_PY_BUILTINS_COMPLEX && MICROPY_PY_CMATH
    { MP_ROM_QSTR(MP_QSTR_cmath), MP_ROM_PTR(&mp_module_cmath) },
#endif
#endif
#if MICROPY_PY_SYS
    { MP_ROM_QSTR(MP_QSTR_sys), MP_ROM_PTR(&mp_module_sys) },
#endif
#if MICROPY_PY_GC && MICROPY_ENABLE_GC
    { MP_ROM_QSTR(MP_QSTR_gc), MP_ROM_PTR(&mp_module_gc) },
#endif
#if MICROPY_PY_THREAD
    { MP_ROM_QSTR(MP_QSTR__thread), MP_ROM_PTR(&mp_module_thread) },
#endif

    // extmod modules

#if MICROPY_PY_UERRNO
    { MP_ROM_QSTR(MP_QSTR_uerrno), MP_ROM_PTR(&mp_module_uerrno) },
#endif
#if MICROPY_PY_UCTYPES
    { MP_ROM_QSTR(MP_QSTR_uctypes), MP_ROM_PTR(&mp_module_uctypes) },
#endif
#if MICROPY_PY_UZLIB
    { MP_ROM_QSTR(MP_QSTR_uzlib), MP_ROM_PTR(&mp_module_uzlib) },
#endif
#if MICROPY_PY_UJSON
    { MP_ROM_QSTR(MP_QSTR_ujson), MP_ROM_PTR(&mp_module_ujson) },
#endif
#if MICROPY_PY_URE
    { MP_ROM_QSTR(MP_QSTR_ure), MP_ROM_PTR(&mp_module_ure) },
#endif
#if MICROPY_PY_UHEAPQ
    { MP_ROM_QSTR(MP_QSTR_uheapq), MP_ROM_PTR(&mp_module_uheapq) },
#endif
#if MICROPY_PY_UTIMEQ
    { MP_ROM_QSTR(MP_QSTR_utimeq), MP_ROM_PTR(&mp_module_utimeq) },
#endif
#if MICROPY_PY_UHASHLIB
    { MP_ROM_QSTR(MP_QSTR_uhashlib), MP_ROM_PTR(&mp_module_uhashlib) },
#endif
#if MICROPY_PY_UCRYPTOLIB
    { MP_ROM_QSTR(MP_QSTR_ucryptolib), MP_ROM_PTR(&mp_module_ucryptolib) },
#endif
#if MICROPY_PY_UBINASCII
    { MP_ROM_QSTR(MP_QSTR_ubinascii), MP_ROM_PTR(&mp_module_ubinascii) },
#endif
#if MICROPY_PY_URANDOM
    { MP_ROM_QSTR(MP_QSTR_urandom), MP_ROM_PTR(&mp_module_urandom) },
#endif
#if MICROPY_PY_USELECT
    { MP_ROM_QSTR(MP_QSTR_uselect), MP_ROM_PTR(&mp_module_uselect) },
#endif
#if MICROPY_PY_USSL
    { MP_ROM_QSTR(MP_QSTR_ussl), MP_ROM_PTR(&mp_module_ussl) },
#endif
#if MICROPY_PY_LWIP
    { MP_ROM_QSTR(MP_QSTR_lwip), MP_ROM_PTR(&mp_module_lwip) },
#endif
#if MICROPY_PY_WEBSOCKET
    { MP_ROM_QSTR(MP_QSTR_websocket), MP_ROM_PTR(&mp_module_websocket) },
#endif
#if MICROPY_PY_WEBREPL
    { MP_ROM_QSTR(MP_QSTR__webrepl), MP_ROM_PTR(&mp_module_webrepl) },
#endif
#if MICROPY_PY_FRAMEBUF
    { MP_ROM_QSTR(MP_QSTR_framebuf), MP_ROM_PTR(&mp_module_framebuf) },
#endif
#if MICROPY_PY_BTREE
    { MP_ROM_QSTR(MP_QSTR_btree), MP_ROM_PTR(&mp_module_btree) },
#endif

    // extra builtin modules as defined by a port
    MICROPY_PORT_BUILTIN_MODULES
};

结构体mp_rom_map_elem_t的定义如下:

typedef struct _mp_rom_map_elem_t {
    mp_rom_obj_t key;
    mp_rom_obj_t value;
} mp_rom_map_elem_t;
1234

说明mp_builtin_module_table这个数组里面每个元素都必须是mp_rom_map_elem_t类型。当增加一个模块时,就往这个数组里面添加一个元素。下面以第一个数组元素进行分析:

2.2.1、MP_ROM_QSTR

{ MP_ROM_QSTR(MP_QSTR___main__), MP_ROM_PTR(&mp_module___main__) },
1

MP_ROM_QSTR的定义为:

#define MP_ROM_QSTR(q) MP_OBJ_NEW_QSTR(q)

而MP_OBJ_NEW_QSTR的定义为:

#define MP_OBJ_NEW_QSTR(qst) ((mp_obj_t)((((mp_uint_t)(qst)) << 2) | 2))

其中mp_uint_t的定义为

typedef unsigned int mp_uint_t; // must be pointer size

mp_obj_t的定义为

typedef void *mp_obj_t;

所以MP_ROM_QSTR(MP_QSTR___main__)的作用是将将MP_QSTR___main__强制转换为类型int,然后将结果值左移两位,在将二进制结果的值与2进行或操作,再将结果转换为无符号的指针类型。最终就变成了一个为_main__的无符号指针。

2.2.2、MP_ROM_PTR

MP_ROM_PTR的 定义为:

#define MP_ROM_PTR(p) (p)

2.3、MICROPY_PORT_BUILTIN_MODULES

MICROPY_PORT_BUILTIN_MODULES的定义在mpconfigport.h文件内为

#define MICROPY_PORT_BUILTIN_MODULES \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp), (mp_obj_t)&esp_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp32), (mp_obj_t)&esp32_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_utime), (mp_obj_t)&utime_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_uos), (mp_obj_t)&uos_module }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_usocket), (mp_obj_t)&mp_module_usocket }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_machine), (mp_obj_t)&mp_module_machine }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_network), (mp_obj_t)&mp_module_network }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR__onewire), (mp_obj_t)&mp_module_onewire }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_uhashlib), (mp_obj_t)&mp_module_uhashlib }, \

MP_OBJ_NEW_QSTR的定义与MP_ROM_QSTR相同。
??以上就是将module添加进固件的指针指向的原理。

10.添加myled 模块(stm32F411平台 1个参数)

编写myled.c

#include "stdint.h"
#include "stdio.h"
#include "py/obj.h"
#include "py/runtime.h"
#include "py/mphal.h"
#include "py/runtime.h"

STATIC mp_obj_t mp_myled_function(mp_obj_t myms)
 {
    uint32_t Myms = mp_obj_get_int(myms);
	mp_hal_pin_low(pin_C13); //点亮灯
    mp_hal_delay_ms(Myms);
	mp_hal_pin_high(pin_C13);//熄灭灯
 	return mp_const_none;
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(My_mp_myled_function, mp_myled_function);

STATIC const mp_rom_map_elem_t modnormal_globals_table[] = {//映射表包含多个函数功能
    {MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_myled_module)},   
    {MP_OBJ_NEW_QSTR(MP_QSTR_myledfunction), MP_ROM_PTR(&My_mp_myled_function)}, 
};

STATIC MP_DEFINE_CONST_DICT(mp_module_modnormal_globals, modnormal_globals_table);   
const mp_obj_module_t mp_module_myled_mod = {
    .base = {&mp_type_module},    
    .globals = (mp_obj_dict_t *)&mp_module_modnormal_globals,
    //globals成员是mp_obj_dict_t类型的,里面存储的是功能映射关系。
};

MP_REGISTER_MODULE(MP_QSTR_myled_module, mp_module_myled_mod, 1);

文件mymodule.c 内创建了一个mp_module_myled_mod的模块,它包含了一个funtion。

注册module

我们要把我们定义的module注册到micropython中去,这个是在mpconfigport.h文件中修改,找到MICROPY_PORT_BUILTIN_MODULES 定义的地方按照格式添加我们定义的module:

// extra built in modules to add to the list of known ones
extern const struct _mp_obj_module_t esp_module;
......
extern const struct _mp_obj_module_t mp_module_onewire;
extern const struct _mp_obj_module_t mp_module_myled_mod;//声明模块
#define MICROPY_PORT_BUILTIN_MODULES \
    { MP_OBJ_NEW_QSTR(MP_QSTR_esp), (mp_obj_t)&esp_module }, \
  ......
    { MP_OBJ_NEW_QSTR(MP_QSTR_uhashlib), (mp_obj_t)&mp_module_uhashlib }, \
    { MP_OBJ_NEW_QSTR(MP_QSTR_myled_module), (mp_obj_t)&mp_module_myled_mod }, \

这里就添加两行代码,第一行是应用外部定义的结构体时要先有声明。第二个需要解释一下,MP_QSTR_myled_module这个名字要改成自定义的,MP_QSTR_这个一定要保留,后面的myled_module就是在python中显示的module名字。

在Makefile文件中更新SRC_C和SRC_QSTR

在SRC_C中添加myled.c文件, 将新增代码编译firmware中.

在SRC_QSTR中添加myled.c文件, 让build过程扫描myled.c文件, 从中提取字符串关键字增加到micropython的qstr列表当中.

第二步操作是根据官方文档的说明进行的操作, 正式的添加qstr. ??然后再修改一下Makefile文件,将myled.c编译进去就可以了。

在这里插入图片描述

在这里插入图片描述

按照官方开发文档的说明, 到这里就可以重新 make , 然后可以在 micropython 的交互命令行中引用led模块的.

编译并烧录,在MicroPython中使用import myled测试是否成功,不报错就是成功了。

通过编译之后, 下载firmware到电路板上, 复位后通过终端命令行交互, 能够成功识别led模块, 并且在板子上确实看到led灯受控亮灭了.

查看micropython已安装的库

help(‘modules’),已经能够查看到安装的模块。

在这里插入图片描述

在MicroPython中使用import myled_module测试是否成功,不报错就是成功了。

在这里插入图片描述

模块中有闪灯的代码,运行模块中的函数 , 灯亮延时3S,后熄灭。

import myled_module as myled
myled.myledfunction(3000)

查看模块中的函数

在这里插入图片描述

在这里插入图片描述

11. 总结

Micropython入手半年,从最开始的 OpenMV开始, 根据开源资料自行制作了MV4, 成功后,对里面的软件代码产生兴趣,目前已经成功将开源 git 上的代码移植到ST的 F407 F411H743 三个硬件平台。其中OpenMV还移植了 miniH743开发板、和自做的 H743平台。具体的移植过程记录在 micropython下载及安装编译过程 micropython下载及安装编译过程 和 openmv下载及安装编译过程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可能对于熟练使用C语言的编程方式,MP是一种新的方式,但是对于新手,MicroPython是极其友好的方式;同时ST对于 MP的支持是不遗余力的,同时未来对MP的使用,应该是越来越多的,2022年的智能汽车竞赛,已经允许使用MP作为开发语言。相信不久的将来,MP应该与python一样,使用的人会越来越多。

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 20:45:38  更:2022-03-21 20:47:18 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 20:07:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码