为micropython添加模块(2)-类模块
作者:互联网
这篇是我早年学习micropython的学习笔记.
当时关于micropython的开发文档资料相当匮乏, 我自己很多开发的思路都是通过研读代码, 连蒙带猜一点一点摸索出来的. 这篇<移植mpy:向模块中添加类>的文档成文之后, 一直存放在我的一个私有代码仓库里, 作为我近几年学习micropython的知识基础重要组成部分, 为我研究和应用micropython提供重要的依据.
最近看到大家对micropython的关注度又有所提高, 因此我把陈年的私藏开放出来, 与同行们共勉, 以此也希望更多优秀的工程师对micropython的开发过程进行丰富的和充分的实践, 少走弯路, 并贡献出更多有价值的开发文档, 推动对micropython的规范化开发.
早年的开发经验和和心态同现在也是不同了, 这三年中发生了太多的事情…(这会是另一个系列的故事中的一部分). micropython在这几年的代码也在不断演进, 我最近把micropython又捡起来了, 根据最新的代码和开发环境, 重新把这些基础的过程走一遍, 并开始撰写新的笔记. 但从知识上, 还是以之前的经验总结作为基础. 看着之前的笔记, 自己时不时会得意一下, 自己那时候还是有点小聪明的嘛 ^v^
移植mpy:向模块中添加类
文章目录
本文以向pyb模块中添加Pin类为例,说明在mpy中添加类的操作步骤
实现底层功能函数并定义类结构
在pyb目录下创建pin.c/.h文件,包含Pin类定义的主体内容
其中pin.h文件中的内容为:
#ifndef __PYB_PIN_H__
#define __PYB_PIN_H__
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "py/nlr.h"
#include "py/obj.h"
#include "py/runtime.h"
#include "py/binary.h"
extern const mp_obj_type_t pyb_pin_type;
#endif /*__PYB_PIN_H__*/
暂时不用管这里包含的头文件的含义,只要照抄就行。唯一有用的一句话是最后一句,定义了“pyb_pin_type”的一个类型实例,这个类型实例将在pyb.c文件中被整合到pyb模块下面。
pin.c文件中实现mpy通过C语言操作硬件的功能函数。这个文件里可以直接包含C源代码的驱动程序文件,可以像平时写C语言的单片机程序一样自由发挥。
例如:
#include "pin.h"
#include "hal_gpio.h" /* dac hardware hal driver. */
#define HAL_GPIO_PORT_COUNT 5
#define HAL_GPIO_PIN_COUNT 32
#define HAL_GPIO_DIR_OUTPUT 1
#define HAL_GPIO_DIR_INPUT 0
#define HAL_PORT_PULL_NONE 0x0
#define HAL_PORT_PULL_DOWN 0x2
#define HAL_PORT_PULL_UP 0x3
PORT_Type * const cPortBasePtrArr[] = PORT_BASE_PTRS;
GPIO_Type * const cGpioBasePtrArr[] = GPIO_BASE_PTRS;
这些代码同用C语言操作底层寄存器的代码没有任何区别。
但是,需要通过必要的结构入口同mpy关联起来。
定义类属性结构体类型
/* Define a type of pin handler. */
typedef struct _pyb_pin_obj_t
{
mp_obj_base_t base;
GPIO_Type *dev_base;
uint8_t port_id; /* 端口号 */
uint8_t pin_id; /* 引脚号 */
uint8_t pin_val; /* 用户最后操作引脚的值 */
} pyb_pin_obj_t;
这个结构体类型的变量将被mpy类方法的初始化函数使用,用于保存在mpy脚本中创建类实例的内部数据。这个结构体类型定义了类实例中的所有底层驱动开发者可用的全局内部变量。“内部”指的是仅在类内可见,而用户在应用层的脚本上不可见。“全局”指的是在类内部的多个方法的实现过程中均可访问,相当于是类的属性。因此,也可以称这个结构体为“类属性”结构体。
定义初始化外设的函数
STATIC mp_obj_t pyb_pin_init_helper( pyb_pin_obj_t *self,
size_t n_args,
const mp_obj_t *pos_args,
mp_map_t *kw_args);
STATIC mp_obj_t pyb_pin_init(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args)
{
/* 调用此函数时,参数列表中的第一个参数就是self */
return pyb_pin_init_helper(args[0], n_args - 1, args + 1, kw_args);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(pyb_pin_init_obj, 1, pyb_pin_init);
这里的重点在于pyb_pin_init函数,因此其内部调用的pyb_pin_init_helper函数仅仅放了一个声明在pyb_pin_init函数之前,pyb_pin_init_helper函数本身实现也是比较讲究的,放到后面再详细说明。类初始化函数在这里仅仅是一个格式化的封装,将作为一个回调函数“注册”到mpy的类管理器中,在实例化类对象时调用构造函数的过程中调用此处的初始化函数。因此,此函数的接口要严格符合mpy类管理器对类实例初始化函数的要求。简单来说,pyb_pin_init仅仅是个格式化的壳子,它的助理“helper”函数才是真正干活的家伙。
这里剧透一下,后面将要介绍的“pyb_pin_make_new”就是本类实例的构造函数,构造函数将会解析部分定长参数填充类属性结构体
对函数接口进行说明:
- 返回值:pyb_pin_init通过pyb_pin_init_helper,最终返回一个状态对象,实际上就是一个状态值常量,但是在mpy中,一切皆是对象,哪怕仅仅是一个常量。
- 参数1:“size_t n_args”是从构造函数的回调中传入的参数总个数。在最终的应用脚本中,实例化一个Pin对象时,构造函数是可以接收变长参数列表的。例如, pin1 = Pin(0, 1, dir=DIR_INPUT),但是对另一个引脚对象的实例化就变成了pin2 = Pin(0, 2, dir=DIR_OUTPUT, val=1)。处理变长参数列表,最终还是要当成有确定长度的数组来处理,因此记录变长参数列表,两个必要的描述属性之一就是参数数量(数组长度),另一个是参数队列(数组元素)。
- 参数2:“const mp_obj_t *args”描述的就是构造函数传入变长参数列表的数组元素清单,等价于数组的首指针。特别注意,当类实例的构造过程回调初始化函数时,传入参数列表的第一个参数就是之前定义的pyb_pin_obj_t结构体指针,并且在回调初始化函数之前已经分配好内存,之后在pyb_pin_init_helper函数中从参数列表中解析出有效数据并填充到类属性结构体中,以便于后续方法使用。
- 参数3:“mp_map_t *kw_args”对于移植代码的开发者没有意义,这应该是mpy内核临时借给初始化过程的一块内存,在pyb_pin_init_helper函数中带有赋值语句的参数解析成字典,保存在kw_args指向的一块内存(数组)中。具体可参见pyb_pin_init_helper函数中调用mp_arg_parse_all的操作。
pyb_pin_init函数对pyb_pin_init_helper函数的调用,就是把args参数数组中的首个元素,也就是属性结构体,作为第一个参数传入;第二、三个参数是回调变长参数列表中剩余参数的数量的元素数组;最后一个参数传入构造函数借给初始化函数用于解析变长参数并保存为字典的内存块。
定义好pyb_pin_init函数之后,需要通过“MP_DEFINE_CONST_FUN_OBJ_KW”宏指令“开光”,将它转化成对象“pyb_pin_init_obj”。其中,操作指令的“KW”表示此处转化为变参数函数,后面还会见到对应位置有“1”,“2”,“3”等等的宏指令,表示的是不同定参数函数;第二个参数的“1”表示此变参数函数至少保证有前1个固定参数。“开光”之后变身为pyb_pin_init_obj对象的实体将通过mpy的标准注入流程被注册到mpy的类管理器中。
下面专门介绍pyb_pin_init_helper函数的实现内容。pyb_pin_init_helper函数:内部调用mp_arg_parse_all函数,对构造函数传入的参数列表进行解析,同预定于的参数字段进行匹配,并找到用户为对应参数赋予的值;将解析后的值保存到类对象的属性结构体中,便于后续方法使用;调用底层硬件的驱动函数,“接地气”,完成对硬件的初始化配置;最后返回“mp_const_none”常量对象,表示正常完成初始化过程。
/* 处理关键字参数列表 */
STATIC mp_obj_t pyb_pin_init_helper( pyb_pin_obj_t *self,
size_t n_args,
const mp_obj_t *pos_args,
mp_map_t *kw_args)
{
/* 定义可以接收的关键字参数并指定默认值 */
static const mp_arg_t allowed_args[] =
{
{ MP_QSTR_dir, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = HAL_GPIO_DIR_INPUT} }, /* GPIO数据方向,默认为输入 */
{ MP_QSTR_val, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0 } }, /* GPIO输出初值,默认为0 */
{ MP_QSTR_pull, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = HAL_PORT_PULL_UP}}, /* 上拉/下拉电阻,默认上拉电阻 */
};
/* 解析参数:
* 在传入参数序列中匹配出有效的关键字参数,并为其赋值(覆盖默认值)
* 匹配参数的顺序同上述定义顺序一致
*/
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
/* 根据最终的设定,配置硬件模块 */
PORT_Type * port_base = (PORT_Type * )cPortBasePtrArr[self->port_id];
GPIO_Type * gpio_base = (GPIO_Type * )cGpioBasePtrArr[self->port_id];
/* 配置上拉/下拉电阻 */
if (args[2].u_int == HAL_PORT_PULL_NONE)
{
port_base->PCR[self->pin_id] = 0U;
}
else if (args[2].u_int == HAL_PORT_PULL_DOWN)
{
port_base->PCR[self->pin_id] = PORT_PCR_PE_MASK;
}
else if (args[2].u_int == HAL_PORT_PULL_UP)
{
port_base->PCR[self->pin_id] = PORT_PCR_PE_MASK | PORT_PCR_PS_MASK;
}
else
{
mp_raise_ValueError("unsupported pull mode for Pin.");
}
/* 配置端口初值 */
if (args[1].u_int < 2)
{
GPIO_SetPinLogic(gpio_base, self->pin_id, (args[1].u_int == 1) );
}
else
{
mp_raise_ValueError("unsupported pin value for Pin.");
}
/* 配置数据方向 */
if (args[0].u_int < 2)
{
GPIO_SetPinDir(gpio_base, self->pin_id, (args[0].u_int == HAL_GPIO_DIR_OUTPUT) );
}
else
{
mp_raise_ValueError("unsupported dir setting for Pin.");
}
return mp_const_none;
}
在解析构造函数传入的参数时:
首先定义了一个关键字列表allowed_args,其中定义了关键字的字符串、类型及默认值。特别地,这里使用到了“MP_QSTR_”前缀命名的字符串,包括定义关键字参数的字符串,及对参数可配置值最终映射成的字符串,都被转化成了可以在脚本中被用户直接使用的字符串对象。
例如,在本源文件的代码中,在实现了所有类方法并对它们“开光”之后,定义了mp_rom_map_elem_t类型的映射表pyb_pin_locals_dict_table,其中将几个类方法对象(例如刚讲过开过光的pyb_pin_init_obj)和可用的配置属性字符串,都映射成脚本中用户可用的字符串对象。这些字符串对象还需要在“qstrdefsport.h”文件中声明才能被mpy识别,这个后续还会提到。
/* Define the "table" */
STATIC const mp_rom_map_elem_t pyb_pin_locals_dict_table[] =
{
/* 实例方法 */
{ MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&pyb_pin_init_obj) },
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&pyb_pin_deinit_obj) },
{ MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&pyb_pin_write_obj) },
{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&pyb_pin_read_obj) },
/* 类常量 */
{ MP_ROM_QSTR(MP_QSTR_PULL_NONE), MP_ROM_INT(HAL_PORT_PULL_NONE) },
{ MP_ROM_QSTR(MP_QSTR_PULL_DOWN), MP_ROM_INT(HAL_PORT_PULL_DOWN) },
{ MP_ROM_QSTR(MP_QSTR_PULL_UP), MP_ROM_INT(HAL_PORT_PULL_UP) },
{ MP_ROM_QSTR(MP_QSTR_DIR_INPUT), MP_ROM_INT(HAL_GPIO_DIR_INPUT) },
{ MP_ROM_QSTR(MP_QSTR_DIR_OUTPUT), MP_ROM_INT(HAL_GPIO_DIR_OUTPUT)},
};
定义其它类方法的函数
除了类实例的构造函数是需要注册到mpy的类管理系统中,我们人为地定义了一些“标准”的类方法,参考posix的驱动API设计标准,定义了open、close、read、write及ioctl及其变种函数。
这里先以pyb_pin_deinit方法函数的实现为例说明这些标准方法在类定义中的实现。pyb_pin_deinit几乎是最简单的标准方法了,它的传入参数最少,只有一个self_in。python的类方法(不仅仅是mpy)的实现要求第一个参数必须为一个指向类自己的指针,相当于是面向对象中的“this”,而在Pin类中,这个this指向的就是类属性结构体。上文曾提到,类属性结构体是类内部的全局变量,可以用于存放类实例的属性信息。其它类方法函数的实现也将包含这个“this”或者“self”的参数。
STATIC mp_obj_t pyb_pin_deinit(mp_obj_t self_in)
{
pyb_pin_obj_t *self = self_in;
printf("pyb_pin_deinit(), %d\n", self->pin_val);
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(pyb_pin_deinit_obj, pyb_pin_deinit);
在定义pyb_pin_deinit函数之后,需要通过MP_DEFINE_CONST_FUN_OBJ_1宏指令为其开光,进阶为pyb_pin_deinit_obj对象。如前文提到,MP_DEFINE_CONST_FUN_OBJ_1宏指令中的“_1”表示这是为固定有一个参数的函数进行开光的专用法器,而在前文中对pyb_pin_init函数开光时用了专门处理变长参数列表的“_KW”开光神器。
接下来再看实现write标准操作的pyb_pin_write函数定义。
STATIC mp_obj_t pyb_pin_write(mp_obj_t self_in, mp_obj_t val)
{
pyb_pin_obj_t *self = self_in;
uint8_t pin_val = (uint8_t)mp_obj_get_int(val);
GPIO_Type * gpio_base = (GPIO_Type *)cGpioBasePtrArr[self->port_id];
/* 配置端口初值 */
if (pin_val < 2)
{
GPIO_SetPinLogic(gpio_base, self->pin_id, (pin_val == 1) );
self->pin_val = pin_val;
}
else
{
mp_raise_ValueError("unsupported pin value for Pin.\n");
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(pyb_pin_write_obj, pyb_pin_write);
相对于pyb_pin_deinit,pyb_pin_write函数多了一个参数,将要写入的值val。这个值将是用户在脚本中传递给类方法的,但只要是从脚本中传过来的变量,就都是对象。然而,C语言层面上可不管对象,只要值!这里可以看到,函数中简单地用了一个转换:
uint8_t pin_val = (uint8_t)mp_obj_get_int(val);
这也是mpy移植过程中常用的手法,可以用mp_obj_get_int函数将对象转化成int类型的值,甚至还可以尝试mp_obj_get_char、mp_obj_get_string等等。
一旦拿到值,剩下用常规C语言操作底层硬件就没啥问题了。
这里还有两个要点:
- 可以使用self指针访问到类属性结构体中的值,从而提取Pin实例的端口号和引脚号。
- 可以使用mp_raise_ValueError函数,向mpy解析器上报错误,mpy最终会在终端界面打印出错误信息并执行mpy标准的错误处理流程。
最后,仍是通过MP_DEFINE_CONST_FUN_OBJ_2宏指令,将pyb_pin_write函数开光成pyb_pin_write_obj对象。
然后再看pyb_pin_read函数的实现。
STATIC mp_obj_t pyb_pin_read(mp_obj_t self_in)
{
pyb_pin_obj_t *self = self_in;
GPIO_Type * gpio_base = (GPIO_Type *)cGpioBasePtrArr[self->port_id];
self->pin_val = GPIO_GetPinLogic(gpio_base, self->pin_id) ? 1 : 0;
return mp_obj_new_int(self->pin_val);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(pyb_pin_read_obj, pyb_pin_read);
read函数的实现提供了一个新的要点,那就是返回值。之前函数的返回值都是没有实际意义的,但是read函数要把从底层硬件捕获到的数据返回给mpy解析器。在write函数的实现中,已经介绍了将mpy对象转换成数值的方法,那么这里看到的是将数值转换成mpy对象的方法。
return mp_obj_new_int(self->pin_val);
最后在对函数开光的时候,开光的宏指令并没有因为这个返回值有了意义而特别对待,仍然是用了同之前相同的MP_DEFINE_CONST_FUN_OBJ_1法器。
到目前为止,已经定义了足够给Pin类的方法及实现函数了。
定义从属于本类的对象字典
pyb_pin_locals_dict_table对象字典定义了在Pin类内部可以使用的从属对象。在python中的一切皆是对象,包括类,及类内部的方法(函数对象)及类内部可使用的常量(常数对象),并且这些对象都需要映射到字符串,因为python是通过解析脚本中的字符串检测到对象实体的。
这个字典的定义的内容有两个含义:
- 将之前定义的对象(函数经过开光之后变身成为函数对象,或者直接转化的整数型对象),同对应的字符串建立起映射关系。有了这个映射关系,python才能将脚本文本中引用函数或者数值的字符串看做是对应的函数或是数值。
- 指定从属关系。当在脚本中,将会使用“pin.write()”或者“Pin.PULL_NONE”的语句使用本类中定义的函数。这里可以看到,这些调用中的函数或者变量都是通过“.”从属于Pin类及其实例的。
/* Define the "table" */
STATIC const mp_rom_map_elem_t pyb_pin_locals_dict_table[] =
{
/* 实例方法 */
{ MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&pyb_pin_init_obj) },
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&pyb_pin_deinit_obj) },
{ MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&pyb_pin_write_obj) },
{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&pyb_pin_read_obj) },
/* 类常量 */
{ MP_ROM_QSTR(MP_QSTR_PULL_NONE), MP_ROM_INT(HAL_PORT_PULL_NONE) },
{ MP_ROM_QSTR(MP_QSTR_PULL_DOWN), MP_ROM_INT(HAL_PORT_PULL_DOWN) },
{ MP_ROM_QSTR(MP_QSTR_PULL_UP), MP_ROM_INT(HAL_PORT_PULL_UP) },
{ MP_ROM_QSTR(MP_QSTR_DIR_INPUT), MP_ROM_INT(HAL_GPIO_DIR_INPUT) },
{ MP_ROM_QSTR(MP_QSTR_DIR_OUTPUT), MP_ROM_INT(HAL_GPIO_DIR_OUTPUT)},
};
/* Create the "dict" from "table". */
STATIC MP_DEFINE_CONST_DICT(pyb_pin_locals_dict, pyb_pin_locals_dict_table);
定义映射字典,创建了一个对象映射表的数组。但是,数组仍然是C语言层面上的概念,最后还是需要经过MP_DEFINE_CONST_DICT宏命令开光成字典对象,生成pyb_pin_locals_dict。至此,Pin类方法和类属性都被打包到类字典中了,并且成功完成开光。后面更高层次的封装可以通过类字典找到本文件中用C语言实现的所有底层函数、变量及常量。
需要注意的是,在其它类中仍可将不同的函数映射到相同的python字符串上,例如当创建一个DAC的新类时,可能会定义这样的语句:
STATIC const mp_rom_map_elem_t pyb_dac_locals_dict_table[] =
{
/* 实例方法 */
...
{ MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&pyb_dac_write_obj) },
...
};
这里将pyb_dac_write_obj映射到了同一个“write”字符串上。不用担心,因此write对象有不同的从属关系,在mpy中仍能够分辨出pin.write()和dac.write(),从而执行不同的write函数。
定义本类的构造函数
构造函数是在脚本中创建一个对象实例时调用的。构造函数的调用方式同之前的类方法不同,它使用类名作为函数名,同时也允许使用固定参数和可变参数组成的参数列表。在构造函数中,需要实现为类实例分配内存及执行一定的初始化(软件&硬件),最后在用户脚本的层面上返回一个类实例(的引用),后面的脚本就可以通过构造函数返回的类实例调用类方法和类属性了。
Pin类的构造函数实现代码如下:
/* 构造函数:
* 两个固定参数,分别为端口号和引脚号
* 其余关键字参数,可指定:数据方向,初值,及上拉下拉等
*/
mp_obj_t pyb_pin_make_new( const mp_obj_type_t * type,
size_t n_args,
size_t n_kw,
const mp_obj_t *args)
{
// this checks the number of arguments (min 1, max 1);
// on error -> raise python exception
mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, true); /* 保证两个固定参数 */
// create a new object of our C-struct type
pyb_pin_obj_t *self = m_new_obj(pyb_pin_obj_t);
// give it a type.
self->base.type = &pyb_pin_type;
// fill in the number value with the first argument of the constructor.
/* 第一个参数是端口号 */
if (MP_OBJ_IS_INT(args[0]) )
{
if (mp_obj_get_int(args[0]) < HAL_GPIO_PORT_COUNT)
{
self->port_id = mp_obj_get_int(args[0]);
}
else
{
nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_ValueError, "Only %d ports are available on this device.\n", HAL_GPIO_PORT_COUNT));
}
}
else
{
mp_raise_TypeError("Pin port id type error. It should be a number\n");
}
/* 第二个参数是引脚号 */
if (MP_OBJ_IS_INT(args[1]) )
{
if (mp_obj_get_int(args[1]) < HAL_GPIO_PIN_COUNT)
{
self->pin_id = mp_obj_get_int(args[1]);
}
else
{
nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_ValueError, "Only %d pins are available on this device.\n", HAL_GPIO_PIN_COUNT));
}
}
else
{
mp_raise_TypeError("Pin pin id type error. It should be a number\n");
}
/* 处理后续的关键字参数列表,并最终配置硬件模块 */
mp_map_t kw_args;
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args); /* 创建关键字参数列表,此时关键字参数的数量已经确定 */
pyb_pin_init_helper(self, n_args - 2, args + 2, &kw_args); /* 从args参数列表中,跳过固定参数,将变参数指针传入helper函数进一步处理 */
printf("new Pin instance created.\n");
/* return the object. */
return MP_OBJ_FROM_PTR(self);
}
首先看一下类构造方法的传入参数:
mp_obj_t pyb_pin_make_new( const mp_obj_type_t * type,
size_t n_args,
size_t n_kw,
const mp_obj_t *args)
mp_obj_type_t类型的type参数在本例中没有用到,暂且不表,其余的三个参数n_args,n_kw及args一起定义了一个输入参数列表,n_args指定了固定参数的个数,n_kw指定了其中可变参数的个数,而args直接就是参数对象的数组。
后续还使用了参数检测命令
mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, true); /* 保证两个固定参数 */
对传入参数列表的有效性进行了验证。顺便说一下,这有这里验证有效才能进行后续的实例化操作,也就是说,此处验证命令中配置的参数限定了类构造函数参数的有效传入方式。
然后就是从内存系统的堆空间中分配一块内存给新创建的类实例,并且将Pin类的类型对象填入新创建类实例的type属性中。也就是说,构造函数首先通过m_new_obj()函数创建了一个通用的类实例,然后标记其为某一个特别类型的类,从而完成创建一个特别类型的类实例的过程。
// create a new object of our C-struct type
pyb_pin_obj_t *self = m_new_obj(pyb_pin_obj_t);
// give it a type.
self->base.type = &pyb_pin_type;
然后是使用参数列表中的固定参数对设备完成一定程度的初始化,包括验证传入参数的有效性,并且填充类属性结构体。
// fill in the number value with the first argument of the constructor.
/* 第一个参数是端口号 */
if (MP_OBJ_IS_INT(args[0]) )
{
if (mp_obj_get_int(args[0]) < HAL_GPIO_PORT_COUNT)
{
self->port_id = mp_obj_get_int(args[0]);
}
else
{
nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_ValueError, "Only %d ports are available on this device.\n", HAL_GPIO_PORT_COUNT));
}
}
else
{
mp_raise_TypeError("Pin port id type error. It should be a number\n");
}
/* 第二个参数是引脚号 */
if (MP_OBJ_IS_INT(args[1]) )
{
if (mp_obj_get_int(args[1]) < HAL_GPIO_PIN_COUNT)
{
self->pin_id = mp_obj_get_int(args[1]);
}
else
{
nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_ValueError, "Only %d pins are available on this device.\n", HAL_GPIO_PIN_COUNT));
}
}
else
{
mp_raise_TypeError("Pin pin id type error. It should be a number\n");
}
这里有三个要点:
- 此时使用args参数列表中的固定参数,固定参数在总的参数列表中的位置是固定的,这部分是没有像后面的可变参数列表可以通过解析执行解析出一系列参数映射对。
- args参数列表中的固定参数,是以对象方式传入的,需要使用mp_obj_get_int()等API将它们转换成C语言层面上的数值,才能被C语言的语句编程。
- 构造函数的初期初始化过程需要填充self类属性结构体。在后面调用pyb_pin_init_helper进一步初始化类实例的时候,需要将self传入其中,使其在内部可以从中读取有效的赋值。填充self类属性结构体,是在早期初始化过程中最有意义的一步操作,后续的初始化过程将传给helper函数继续搞定。
前面讲固定参数的价值榨取干净了,紧接着将处理可变参数列表。
/* 处理后续的关键字参数列表,并最终配置硬件模块 */
mp_map_t kw_args;
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args); /* 创建关键字参数列表,此时关键字参数的数量已经确定 */
pyb_pin_init_helper(self, n_args - 2, args + 2, &kw_args); /* 从args参数列表中,跳过固定参数,将变参数指针传入helper函数进一步处理 */
在代码中,首先创建了一个纯可变参数列表的对象,mp_map_init_fixed_table()函数总的args中偏移n_args个单元,提取出剩下的n_kw个可变参数装入kw_args中,此时可变参数列表实际变成了一个固定长度的参数(但是参数内容仍可以用赋值参数的定义方式,即可使用映射关系作为传入参数的手段)。之后,就修正后的参数信息(新的参数长度和新的参数数组)及self(经过开光的类属性结构体),传递给pyb_pin_init_helper()函数继续完成类实例的初始化操作。
构造函数实现内容的最后,将pyb_pin_obj_t类型的self对象的指针返回给脚本中的调用者,使用这个指针,或者应该称为句柄,将可以访问到该类的所有资源。
/* return the object. */
return MP_OBJ_FROM_PTR(self);
实例化本类的类型对象
python中的一切皆是对象,即使对象的类型也是对象。
在类实现过程的最后,终于将之前定义的所有类资源封装到一个类类型的对象中,这个pyb_pin_type实例,将会用来表示Pin类。它以固定的格式封装,包括指定本结构体是一个类型定义的实例,可以使用“Pin”字符串作为本类的名字,使用pyb_pin_make_new函数作为构造函数,使用pyb_pin_locals_dict中定义的方法和属性作为本类的从属资源。
/* Define the class. */
const mp_obj_type_t pyb_pin_type =
{
{ &mp_type_type },
.name = MP_QSTR_Pin,
.make_new = pyb_pin_make_new,
.locals_dict = (mp_obj_dict_t*)&pyb_pin_locals_dict,
};
pyb_pin_type的封装,也是将Pin类实现的所有资源整合在一起,可以作为一个独立的实现单元。之后,将在外部被更高层面上的对象类型(模块)包含。由于在这个层面上涉及到了对外交涉,因此这个类型变量的引用声明同时也被放在了pin.h文件中,当实现更高层面上的对象类型时,通过包含pin.h文件,可以引用到pyb_pin_type类型变量中封装的资源。
在qstrdefsport.h文件中声明本类中使用的字符串对象
在移植mpy的过程中,新增需要mpy识别的字符串,都必须在“\ports\ke18f”下的qstrdefsport.h文件中声明,相当于是mpy的预编译系统中“挂个号”。声明语法就是用一个“Q”,再带一对括号,在括号里直接写字符串,不用C语言的双引号引用的字符串。例如,在Pin类曾经定义的POSIX标准IO函数:init(), deinit(), write(), read()及后续可能用到的ioctl(),Pin类的名字本身也需要声明一个字符串,Pin方法中传入的命名参数dir,var和pull,以及在脚本中可用的常量PULL_NONE,PULL_UP,PULL_DOWN,DIR_INPUT, DIR_OUTPUT等。
// qstrs specific to this port
// common driver functions:
Q(init)
Q(deinit)
Q(write)
Q(read)
Q(ioctl)
// classes in pyb:
// class DAC:
Q(DAC)
Q(bits)
Q(outval)
// class Pin:
Q(Pin)
Q(dir)
Q(val)
Q(pull)
Q(PULL_NONE)
Q(PULL_UP)
Q(PULL_DOWN)
Q(DIR_INPUT)
Q(DIR_OUTPUT)
还是那句话,在python中一切皆是对象,在脚本中一切皆是字符串。执行脚本的过程就是解析字符串的过程。
这里有两件事要交代一下:
-
不同类使用的同一个字符串,例如在Pin类下的write和DAC类下的write,只要在本文件中声明一次write就好。可以理解为,mpy将pin.write和dac.write字符串解析成pin+write和dac+werite的编码,即使write的编码相同,但是pin和dac的编码不同,那么最终完整字符串的编码仍是不同的,从而可以区分不同的函数。
-
stm32芯片的移植代码中没有向这个文件中增加所有的字符串,那是因为在编译image的makefile中调用了脚本扫描代码并自动生成了部分Q(xxx)的字符串到最终的qstrdefsport.h文件中文件中。特别的,stm32的移植为引脚定义了字母排序的命名,如果人为列出所有的字符串确实是比较麻烦。但是在我的移植中,直接用数字编号表示引脚,所以就省掉了这个环节。实际上,用数字对引脚进行编号也是常用的,可以接受的方式。
在pyb.c中向pyb模块注册本类
pyb是目前mpy操作硬件的根模块,所有的外设类都是挂在pyb模块下。pyb模块定义在“\ports\ke18f”目录下的pyb.c源文件中。在注册外设类时,在pyb.c文件中引用pin.h文件
#include "pin.h"
其实pin.h文件中仅仅声明了一个对pin类类型的外部引用,实际在pyb.c中注册pin类也仅仅需要使用这个类型变量的引用,将它的指针注册到pyb的映射字典中,就像我们之前在定义Pin类下属的方法和常量字符串时一样。
STATIC const mp_rom_map_elem_t pyb_globals_table[] =
{
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_pyb) },
...
{ MP_ROM_QSTR(MP_QSTR_Pin), MP_ROM_PTR(&pyb_pin_type) },
...
};
向pyb模块中注册新类仅仅需要向pyb_globals_table数组中添加新的项目即可,不需要增加其它的代码,其它部分内容保持不变。
至此,就可以重新编译代码,下载的开发板中,然后在命令行终端中测试是否能使用新定义的类了。Bingo!
标签:micropython,obj,pin,args,添加,MP,模块,pyb,mp 来源: https://blog.csdn.net/suyong_yq/article/details/113828901