如何用 C 添加一个 MaixPy 模块

    预备知识

    python 中万物皆对象

    需要先知道 module,type, function, class 分别是什么,有什么关系和区别

    • module(模块)

    MaixPy中,把每个类别的功能放到一个 模块 中,
    比如内置的 uos,usys,machine
    另外我们自己新建的文件, 比如test.py 也可以是一个模块,
    我们使用模块都这样使用:

    import uos
    import machine
    import test
    

    在 C 源码中就是 mp_type_module

    • type(类型)

    用来表示一个基本的类型, 它可以包含一些方法或者变量

    在 C 源码中就是 mp_type_type

    • class(类)

    一个 class 其实就是一个 type,比如

    class A:pass
    print(type(A))
    

    会输出

    <class 'type'>
    

    当对A进行了实例化

    class A:pass
    a = A()
    print(type(a))
    

    会输出

    <class 'A'>
    

    表示aA的一个实例(对象)

    在 C 中定义一个类其实就是定义一个 mp_type_type

    在 C 中添加模块

    我们的目标是实现在MaixPy层面可以使用以下代码:

    import my_lib
    print(my_lib.__name__)
    my_lib.hello()
    

    components/port/src目录下新建一个文件夹比如取名my_lib

    然后在my_lib文件夹下新建my_lib.c文件

    编辑my_lib.c添加代码

    定义一个模块:

    #include "obj.h"
    
    const mp_obj_module_t my_lib_module = {
        .base = { &mp_type_module },
        .globals = (mp_obj_dict_t*)&mp_module_my_lib_globals_dict,
    };
    

    这里my_lib_module是定义的my_lib模块对象,
    mp_type_module表明是一个模块,
    mp_module_my_lib_globals_dict是模块的全局变量和函数,是一个dict对象,有我们自己定义, 现在还没定义

    定义模块的全局变量

    STATIC mp_obj_t hello()
    {
        mp_printf(&mp_plat_print, "hello from my_lib");
        return mp_const_none;
    }
    
    MP_DEFINE_CONST_FUN_OBJ_0(my_lib_func_hello_obj, my_lib_func_hello);
    
    STATIC const mp_map_elem_t my_lib_globals_table[] = {
        { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_my_lib) },
        { MP_OBJ_NEW_QSTR(MP_QSTR_hello), (mp_obj_t)&my_lib_func_hello_obj },
    
    };
    
    STATIC MP_DEFINE_CONST_DICT (
        mp_module_my_lib_globals_dict,
        my_lib_globals_table
    );
    

    这里定义了一组键值对数组,键值对数值, mp_map_elem_t的定义如下:

    typedef struct _mp_map_elem_t {
        mp_obj_t key;
        mp_obj_t value;
    } mp_map_elem_t;
    
    • 第一个值是key,类型是str对象, 即在MaixPy层面使用my_lib.key来调用。这里用了MP_OBJ_NEW_QSTR(MP_QSTR___name__)生成了一个值为__name__str对象,你可能有疑问__name__这个c变量定义在哪里,这是在编译阶段使用工具自动生成c变量,总之记住这样可以写可以生成一个常量str对象保存在固件里就好了
    • 第二个值是数值,类型是一个对象,可以是str/function/int/float/tuple/list/dict等, 方式如下:

      • str: 这里同样是定义了一个str类型的值为my_lib,即在MaixPy层面使用my_lib.__name__得到结果my_lib

      • 其它常量对象: 可以使用mp_obj_new_xxx,比如int变量mp_obj_new_int(10), 函数在obj.h中搜索

      • 函数: 这里的key``hello对应的值为为(mp_obj_t)&my_lib_func_hello_obj,是一个函数对象,注意不是C函数,前面说了python中一切皆对象, 这里也是使用了一个函数对象,然后去地址强制转换成 mp_obj_t。这个函数对象使用了MP_DEFINE_CONST_FUN_OBJ_0宏定义将my_lib_func_hello这个C函数定义为my_lib_func_hello_obj这个对象,注意hello函数需要返回一个值mp_const_none,注意不能返回NULL, 因为NULL不是一个(MaixPy)对象, 这个返回值也就是MaixPy层面调用hello()函数时的返回值

        > 除了MP_DEFINE_CONST_FUN_OBJ_0即没有参数之外,还有1/2/3/n个参数,以及带关键字参数,这些请翻阅源码举一反三学习

    然后使用MP_DEFINE_CONST_DICT宏定义将my_lib_globals_table这个键值对变成MaixPy层面能理解的dict对象(mp_map_elem_t只是C层面能理解)mp_module_my_lib_globals_dict, 这个对象也被上一步中定义模块的时候使用

    到此一个模块就定义完成了, 在 MaixPy层面,理论上可以使用如下语句进行使用了

    import my_lib
    print(my_lib.__name__)
    my_lib.hello()
    

    但是我们还没编译

    将模块添加到固件, 并进行编译

    • my_lib.c文件末尾添加:
    MP_REGISTER_MODULE(MP_QSTR_my_lib, my_lib_module, MODULE_MY_LIB_ENABLED);
    
    

    这行代码注册这个模块,但是是否编译进固件取决与MODULE_MY_LIB_ENABLED这个宏定义在mpconfigport.h中是否定义为1

    • 所以我们打开mpconfigport.h文件,在里面添加
    #define MODULE_MY_LIB_ENABLED (1)
    
    • 打开components/micropython/CMakeLists.txt编辑

    找到文件中有############## Add source files ############### 的地方,
    在后面添加

    append_srcs_dir(MPY_PORT_SRCS "port/src/my_lib")
    

    到此,项目才会将my_lib这个文件夹编译到固件

    然后python project.py rebuild编译固件即可,因为新增了文件,一定要用rebuild命令而不是build,注意编译提示,如果有报错,注意修改

    在模块中添加一个 type

    前面定义了一个my_lib模块,现在我们希望在my_lib中定义一个类,叫A,如下

    import my_lib
    
    a = my_lib.A()
    print(a.add(1, 2))
    

    这里只讲大致上的思路,然后提供样例,聪明的你一下就能理解了

    • 定义一个mp_obj_type_t 对象,正如前面定义mp_obj_module_t一样
    • 同样的,给这个类对象一个dict对象,作为这个类的成员,成员可以是常量或者函数甚至是另一个type对象
    • 将这个类对象注册到前面的my_lib模块

    定义mp_obj_type_t对象和成员定义可以参考port/src/standard_lib/machine/machine_i2c.c中的实现

    定义mp_obj_type_t时有一个make_new成员,这个函数是用来新建对象时会被调用的函数,比如a = my_lib.A(); a.add(1,2)
    如果不新建对象,直接调用类方法或变量,这个函数不会被调用A.var_a

    比如我们定义了一个const mp_obj_type_t my_lib_A_type ...

    然后在my_lib/my_lib.cmy_lib_globals_table中添加这个对象,并将其映射到key A即可

    { MP_ROM_QSTR(MP_QSTR_A),  MP_ROM_PTR(&my_lib_A_type) },
    

    使用 C 语言编写固件时需要注意

    • mp_printf vs printk vs printf
      因为IDE使用了串口通信协议,所以在C层面不要直接使用printk或者printf函数打印消息,必须使用mp_printf函数来打印,不然会导致 IDE 运行时收到不理解的数据而断开连接!!

    当然平时调试可以使用printk,因为这个函数不会触发系统中断,可以在中断函数里面调用,但是仅限调试时使用, 实际提交代码时一定要删除掉!!