嵌入式技术论坛
直播中

杨海清

7年用户 1759经验值
私信 关注
[经验]

用RTT+MicroPython做一款硬件编程游戏机

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

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

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

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

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

/ 以下是正文 /

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

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

1.jpg

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。具体流程可参考如下图。

1.jpg

这段我也是抄过来的,你不需要看懂,主要明白里面有一个叫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。

不过我做的时候没用这个算法,下面自己就算出来了,具体为啥和上面不一样就不知道了,估计是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语言没怎么用过,只是辅导孩子的时候才偶尔看了一下,感觉天下的语言基本都是想通的,理解上应该是不成问题,只是对里面的一些机制不太了解,有时间再慢慢补吧。

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

原作者:Mars.CN

更多回帖

发帖
×
20
完善资料,
赚取积分