如何用 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,因为这个函数不会触发系统中断,可以在中断函数里面调用,但是仅限调试时使用, 实际提交代码时一定要删除掉!!