MicroPython中C函数与Python的绑定

这几天想给孩子做个编程玩具玩,找来找去,相中了MicroPython,这几年Python异常的火爆,原来只有程序员碰的东西,现在恨不得公司前台都得学会。

某天孩子从学校回来,带回来张表让填,我一看,竟然是编程培训,二年级开始学Python??

好吧,我已经落伍了……

 

RT-Thread已经玩了一段时间了,但始终没有拿他做点东西,正好趁这个机会,用RTT+MicroPython给孩子做一款硬件编程游戏机。

设计是这样的:*&%¥@#!#¥%&)*&%#¥@#%¥*(*&……

 

所以,里面很多函数需要C直接调用硬件,这就需要将C函数映射到MicroPython中,官方给出的资料太少了,按照那点少的可怜的资料,虽然搞出来了,但是还是不明白怎么实现的,于是有看了大量其他的资料,包括Python的源码都翻了,最终找到了合理的答案。

这里,贡献给大家,帮大家踩踩坑。

(声明一下,我对Python不是很熟,之前一直用的是JAVA,所以里面有些术语表达上可能有些词不达意的,还请多包含)

 

/**************************************以下是正文**************************************/

 

具体实现自己的Python接口有另种方法,一种是用现有的Python函数基础上,使用Python的语法直接封装,实现自己的功能,这种实现比较方便,就不讲了;我们主要将第二种方法,了利用C语言实现对底层硬件的操作,再把调用的方法以Python的语法开放给其他人用,就是俗称的C到Python的映射。

在此之前,我们先看一下Python的接口分类:

Python中我们要实现的接口主要包含module、type和function三类,从上图的结构中能看出,module相当于JAVA中的包的概念,Python中叫啥?模块?type相当于类的概念,module和type中都可以包含function,函数。

接下来的章节中,我会在RTT的MicroPython中分别创建module、function、type进行详细讲解。

 

一、 MicroPython的工作原理

Micropython技术是依赖Byte Code的执行,在编译阶段就将py文件先转换成MicroPython文件,在通过MicroPython-tool.py生成Byte Code,Byte Code在执行时会依赖Virtual Machine入口表,找到对应的Module入口,最终找到对应的Funcion binary code执行。其中所有的Function都通过Dictionary的形式存储,而每一个Dictionary都有自己的QSTR,Micropython有buildin的QSTR和用户扩展的QSTR。具体流程可参考如下图。

这段我也是抄过来的,你不需要看懂,主要明白里面有一个叫QSTR的东西就行了,这玩意儿贯穿整个Python,开始的时候不明白什么意思,就没管,结果费老劲了!

他大概的意思就是说,我们在Python中使用任何一个名称的时候,都需要QSTR进行定义,包括module的名称、type的名字、function的名字等等。

QDEF(MP_QSTR___main__, (const byte*)"\x8e\x13\x08" "__main__")

就像是这样,其他都好理解,按格式来就行了,但是中间有个\x8e\x13\x08的东西,需要经过一些计算,而计算的方法,人家也已经给了:

def dbj2_hash(qstr, bytes_hash):

    hash = 5381

    for b in qstr:

        hash = (hash * 33) ^ b

    # Make sure that valid hash is never zero, zero means "hash not com    puted"

    return (hash & ((1 << (8 * bytes_hash)) - 1)) or 1

这个函数在prot/genhdr/gen_qstr.py中,可以直接用python运行这个文件来获得QSTR。

不过我做的时候没用这个算法,直接去

https://summergift.top/RT-MicroPython-Generator/

输入函数名字,下面自己就算出来了,具体为啥和上面不一样就不知道了,估计是RTT中有自己的规则吧,总之大家用上面的网址计算就行了,然后把算出的QSTR贴到port/genhdr/qstrdefs.generated.h即可。

这个问题后面遇到的时候再具体说吧。

 

二、 添加module

RTT的MicroPython中,为我们提供了一个自己添加函数的模板,下载好MicroPython的包后,在工程目录下有个packages,里面有micropython-v1.10.4(具体可能版本不同),进入就是MicroPython的源码。

今天我们要修改的是port/modules/user/moduserfunc.c这个文件,把我们自己写的函数添加到这个文件中。

本来我是想在这个目录下在建一个自己的文件的,但是添加后不起作用,肯定还需要改其他地方,这个放在以后慢慢研究吧。

这个章节中我们主要是创建一个module,要做的只有三件事:

1.定义module的全局字典

2.把定义的字典注册到.globals里面

3.定义module的原型

 

以下是源码:


/*

* 定义mars这个module的全局字典

* 之后我们要将所有的type和function都放到这里

* 这里需要注意几点:

* 1. MP_QSTR_的前缀不能改变

* 2. __name__前后有两个下划线,加上MP_QSTR_最后的下划线,MP_QSTR___name__中间的下划线是三个

* 3. MP_QSTR_mars一类的标签必须在qstrdefs.generated.h中定义过

* 4. 别少了末尾的分号

*/

STATIC const mp_rom_map_elem_t mars_globals_table[] = {

    { MP_OBJ_NEW_QSTR(MP_QSTR___name__) , MP_ROM_QSTR(MP_QSTR_mars)} ,      // 定义module的名称,在python中可以用import mars直接导入了

};



// 将mars_globals_table注册到mars_globals_table.globals中去,定义mp_module_mars_globals

STATIC MP_DEFINE_CONST_DICT(mp_module_mars_globals , mars_globals_table);



//定义module类型

const mp_obj_module_t mp_module_mars = {

    .base       =   {&mp_type_module},

    .globals    =   (mp_obj_dict_t*)&mp_module_mars_globals,

};

完成这一步后,还需要在mpconfigport.h留下点痕迹,否则import会失败的

extern const struct _mp_obj_module_t mp_module_mars;        //将自己的module类型导入

找到#define MICROPY_PORT_BUILTIN_MODULES,在其中添加宏定义

有两种方式,一种是可以参考USERFUNC的定义

#define MARS_PORT_BUILTIN_MODULES   { MP_ROM_QSTR(MP_QSTR_mars), MP_ROM_PTR(&mp_module_mars) },            //别少了最后的逗号

然后在#define MICROPY_PORT_BUILTIN_MODULES后面加上自己的标签

另一种是直接写

{ MP_ROM_QSTR(MP_QSTR_mars), MP_ROM_PTR(&mp_module_mars) },

其实结构都一样。

 

到此为止,module添加完毕。

编译运行:

>>> import mars

>>> type(mars)

<class 'module'>

>>> dir(mars)

['__class__', '__name__']

>>>

三、 添加function

上一章节中我们仅仅是创建了一个module,但是这个module中没有函数可用,本章节中,我会演示如何添加无参、带参、有返回值、无返回值的函数。

首先我们添加一个无参无返回值的参数,代码如下:

//定义函数原型

STATIC mp_obj_t mars_sayhello()

{

    printf("Hello ,This is a function without parameters and return values.\n");

    return mp_const_none;

}

//注册这个函数

STATIC const MP_DEFINE_CONST_FUN_OBJ_0(mars_obj_sayhello,mars_sayhello);

函数原型中,永远返回mp_obj_t类型的值,如果这个函数在Python中没有任何返回值,就直接return mp_const_none。

注册函数时,MicroPython给我提供了很多个方法,因为我们没有参数,所以用了MP_DEFINE_CONST_FUN_OBJ_0,另外还可以用MP_DEFINE_CONST_FUN_OBJ_1、MP_DEFINE_CONST_FUN_OBJ_2、MP_DEFINE_CONST_FUN_OBJ_3,那对于多余3个参数的函数咋办??查了查资料,大多用的都是MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN,其他的几个宏没研究是什么意思,暂时也用不到。

将函数原型和注册完成之后,还需要在刚才定义的mars_globals_table对函数进行声明

STATIC const mp_rom_map_elem_t mars_globals_table[] = {

    { MP_OBJ_NEW_QSTR(MP_QSTR___name__) , MP_ROM_QSTR(MP_QSTR_mars)} ,      // 定义module的名称,在python中可以用import mars直接导入了

    { MP_ROM_QSTR(MP_QSTR_sayhello), MP_ROM_PTR(&mars_obj_sayhello) },      // 定义无参函数,首先确保MP_QSTR_sayhello在qstrdefs.generated.h中注册过

};

 

这里有几点需要注意的:

1.在编码过程中,用到的所有名称,必须要在qstrdefs.generated.h中定义

2.定义函数的时候用的是MP_ROM_PTR,而不是MP_ROM_QSTR

3.用printf输出(stdio的),rt_kprint不好使

4.输出的最后要加\n否则打印不出来。

 

运行结果:

>>> import mars

>>> mars.sayhello()

Hello ,This is a function without parameters and return values.

>>>

 

下面,我们再给这个module添加一个有两个参数和一个返回值的函数

代码基本相同:

//定义函数原型

STATIC mp_obj_t mars_add(mp_obj_t one , mp_obj_t two)

{

    mp_int_t a = mp_obj_get_int(one);

    mp_int_t b = mp_obj_get_int(two);

    mp_int_t ret_val;

    ret_val = a + b;

    printf("You are calling this function, passing in two parameters, %d and %d, and the result is %d!\n",a,b,ret_val);

    return mp_obj_new_float(ret_val);

}

//注册这个函数

STATIC const MP_DEFINE_CONST_FUN_OBJ_2(mars_obj_add,mars_add);



//添加定义
STATIC const mp_rom_map_elem_t mars_globals_table[] = {

    { MP_OBJ_NEW_QSTR(MP_QSTR___name__) , MP_ROM_QSTR(MP_QSTR_mars)} ,      // 定义module的名称,在python中可以用import mars直接导入了

    { MP_ROM_QSTR(MP_QSTR_sayhello), MP_ROM_PTR(&mars_obj_sayhello) },      // 定义无参函数,首先确保MP_QSTR_sayhello在qstrdefs.generated.h中注册过

    { MP_ROM_QSTR(MP_QSTR_add), MP_ROM_PTR(&mars_obj_add) },                // 定义有两个参数的函数,因为MP_QSTR_add已经被别人注册过了,我们不用重复注册

};



运行结果:

>>> import mars

>>> c = mars.add(100,200)

You are calling this function, passing in two parameters, 100 and 200, and the result is 300!

>>> print(c)

300.0

>>>

 

在映射中,所有的参数传递都用mp_obj_t类型,在C的原型函数中,根据需要通过mp_obj_get_XXX转换成具体类型,再参与运算。

这个函数返回一个float类型的值,所以使用mp_obj_new_float进行封装,其他类型的一律仿造mp_obj_new_XXX。

 

四、 添加type

终于到了重头戏了,做JAVA十几年了,对面向对象编程情有独钟,不太习惯面向过程的,所以在任何语言中都想找到对象这东西,Python的松散结构跟JS一样让人很不舒服(个人感觉),个人总想把一切都装箱,还好,Python提供Type这个概念。

 

首先我们创建一个叫做children的类,然后给这个类添加一个叫sayhello的函数。

和module不同,type得到创建稍微复杂点,大概分为4步:

1.定义type的结构体

2.定义locals_dict_type字典,并注册

3.创建type的类型结构

4.添加type的构造函数(这一步可以省略)

 

源码如下:

// 定义一个children的结构体

typedef struct _children_obj_t

{

    mp_obj_base_t   base;       // 定义的对象结构体要包含该成员

    char*       name;           // 成员函数

    uint8_t     age;

    uint8_t     sex;

    

}children_obj_t;

在创建这个类型的结构体时,我们给这个类定义了三个成员变量,分别是name,age,sex,最上面的base是每个对象必须包含的,而且必须放在开头,类型必须是mp_obj_base_t,这是Python的语法规定,咱也不好破坏人家的规矩,老实就范吧。


 

// 定义type的locals_dict_type

STATIC const mp_rom_map_elem_t children_locals_dict_table[] = {

};

//定义字典的宏

STATIC MP_DEFINE_CONST_DICT(children_locals_dict,children_locals_dict_table);

创建字典并定义,这里我们还没有成员函数,所以字典中是空的,这里要注意一下,和module有点不同,type的名称不用放在字典中,直接写到结构体定义中即可,如下:

const mp_obj_type_t mars_children_type = {

    .base           =   { &mp_type_type },

    .name           =   MP_QSTR_children,           //名字要在这里定义,不是写在DICT中,同样要经过注册才行,但是这个单词已经被注册过了,所以就不用重复注册了

    .make_new       =   mars_children_make_new,     //构造函数

    .locals_dict    =  (mp_obj_dict_t*)&children_locals_dict,  //注册math_locals_dict

};

MP_QSTR_children的标签一样要在qstrdefs.generated.h中定义

 

至此,这type就定义完成了。

当然,如果有必要的话,可以定义一个构造函数,就是make_new所指向的那个函数。


// 添加构造函数

STATIC mp_obj_t mars_children_make_new(const mp_obj_type_t *type,

    size_t n_args , size_t n_kw,const mp_obj_t *args)

{

    mp_arg_check_num(n_args ,n_kw,1,3,true);            // 检查参数个数,最少1个参数,最多3个参数

    children_obj_t *self = m_new_obj(children_obj_t);   // 创建对象,分配空间

    self->base.type = &mars_children_type;             // 定义对象类型

    if(n_args >=1 )

    { self->name = mp_obj_str_get_str(args[0]); }

    if(n_args >=2 )

    { self->age = mp_obj_get_int(args[1]); }

    if(n_args ==3 )

    { self->sex = mp_obj_get_int(args[2]); }

    printf("Create a new children , name:%s , age:%d , sex:%s\n"

    ,self->name,self->age,self->sex==0?"girl":"boy");

    return MP_OBJ_FROM_PTR(self);                       //返回对象

}

这里我写了一个比较全的成员函数,包含了传入参数与参数的使用方法,下面一点点分析

第一句用mp_arg_check_num检查构造函数的参数是否符合规定,这里我规定的是最少1个,最多3个,超出这个范围一一律报错,如果没有入参,这两个都写0就好了,框架还提供了一个MP_OBJ_FUN_ARGS_MAX,应该是最大数量的参数。

下一句是用来创建这个对象,并未对象分配内存空间。

第三句,设定了这个类的具体类型,这里就写我们定义过的mars_children_type类型

在后面几个判断,是用来根据参数的个数设置成员变量的。

mp_obj_str_get_str需要注意一下,这东西让我找的好苦,其他类型的数据都是通过mp_obj_get_XXX就能获得,唯独这个string类型的比较特殊。

最后一句,返回这个对象。

 

运行结果:

>>> import mars

>>> type(mars.children)

<class 'type'>

>>> dir(mars.children)

['__class__', '__name__']

>>> d = mars.children("Aday")

Create a new children , name:Aday , age:0 , sex:girl

>>> d = mars.children("Claire",8)

Create a new children , name:Claire , age:8 , sex:girl

>>> d = mars.children("Komy",3,1)

Create a new children , name:Komy , age:3 , sex:boy

>>> type(d)

<class 'children'>

>>> dir(d)

['__class__']

>>>

 

最后一步,我们给这个类添加一个sayhello的成员函数,和module类似,但有些不一样的地方。

//children的成员函数

STATIC mp_obj_t mars_children_sayhello(mp_obj_t self_in , mp_obj_t name)

{

    children_obj_t *self = MP_OBJ_TO_PTR(self_in);  //从第一个参数中提取对象指针

    printf("Hi %s:\n",mp_obj_str_get_str(name));

    printf("  I'm %s. \n  I'm %d years old. \n  I'm a very lovely %s!\n",

        self->name,self->age,self->sex==0?"girl":"boy");

    return mp_const_none;

}

STATIC MP_DEFINE_CONST_FUN_OBJ_2(mars_children_sayhello_obj,mars_children_sayhello);

首先定义这个函数的原型,函数本身有一个入参,但是所有type的成员函数必须将mp_obj_t self_in放在第一个,所有这时候我们会得到两个入参,学习Python的时候应该也注意到了吧,不过多解释。

在注册函数原型的时候用的是STATIC MP_DEFINE_CONST_FUN_OBJ_2,不是STATIC MP_DEFINE_CONST_FUN_OBJ_1,这点是不一样的地方

然后在字典总加入这个函数

STATIC const mp_rom_map_elem_t children_locals_dict_table[] = {

    { MP_ROM_QSTR(MP_QSTR_sayhello),MP_ROM_PTR(&mars_children_sayhello_obj) },

};

因为sayhello这个字符串在之前已经定义过了,所以不用重复定义。

运行结果:

>>> import mars

>>> type(mars.children)

<class 'type'>

>>> dir(mars.children)

['__class__', '__name__', 'sayhello']

>>> d = mars.children("Claire",8,0)

Create a new children , name:Claire , age:8 , sex:girl

>>> d.sayhello("mars")

Hi mars:

  I'm Claire.

  I'm 8 years old.

  I'm a very lovely girl!

>>>

 

所有注册的QSTR

QDEF(MP_QSTR_mars, (const byte*)"\x68\x04" "mars")

QDEF(MP_QSTR_sayhello, (const byte*)"\xec\x08" "sayhello")

QDEF(MP_QSTR_children, (const byte*)"\xf6\x08" "children")

有些例如add一类的,自己已经有了,就不再重复注册了

 

五、结束语

总结完了,希望对各位有帮助。

Python语言没怎么用过,只是辅导孩子的时候才偶尔看了一下,感觉天下的语言基本都是想通的,理解上应该是不成问题,只是对里面的一些机制不太了解,有时间再慢慢补吧。

里面还有很多不完善的地方,希望各位大牛补充。

 

 

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2022 / 01/ 30: 新版esptool 刷micropython固件指令不是 esptool.py cmd... 而是 esptool cmd... 即可;另外rshell 在 >= python 3.10 的时候出错解决方法可以查看:  已于2022年发布的: 第二章:修复rshell在python3.10出错 免费内容: https://edu.csdn.net/course/detail/29666 micropython语法和python3一样,编写起来非常方便。如果你快速入门单片机玩物联网而且像轻松实现各种功能,那绝力推荐使用micropython。方便易懂易学。 同时如果你懂C语音,也可以用C写好函数并编译进micropython固件里然后进入micropython调用(非必须)。 能通过WIFI联网(2.1章),也能通过sim卡使用2G/3G/4G/5G联网(4.5章)。 为实现语音控制,本教程会教大家使用tensorflow利用神经网络训练自己的语音模型并应用。为实现通过网页控制,本教程会教大家linux(debian10 nginx->uwsgi->python3->postgresql)网站前后台入门。为记录单片机传输过来的数据, 本教程会教大家入门数据库。  本教程会通过通俗易懂的比喻来讲解各种原理与思路,并手把手编写程序来实现各项功能。 本教程micropython版本是 2019年6月发布的1.11; 更多内容请看视频列表。  学习这门课程之前你需要至少掌握: 1: python3基础(变量, 循环, 函数, 常用库, 常用方法)。 本视频使用到的零件与淘宝上大致价格:     1: 超声波传感器(3)     2: MAX9814麦克风放大模块(8)     3: DHT22(15)     4: LED(0.1)     5: 8路5V低电平触发继电器(12)     6: HX1838红外接收模块(2)     7:红外发射管(0.1),HX1838红外接收板(1)     other: 电表, 排线, 面包板(2)*2,ESP32(28)  

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值