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。
不过我做的时候没用这个算法,下面自己就算出来了,具体为啥和上面不一样就不知道了,估计是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