阅读micropyton源码-添加C扩展类模块(3)
苏勇,2021年8月
排除万难,终于讲到函数实现了。
python中通过类实例化一个对象,在实例化过程中,首先“无中生有”搞出一块内存存放对象,然后实现传参,将实例化参数传入内部对象实体,最后调用相关初始化操作,为后续类方法正常工作创造运行环境。“无中生有”的内存来自于gc_pool,但在哪个环节搞出来的内存,这是一个需要追一下的思路。传参比较有趣,涉及到关键字的定义和解析,还可以直接接受一个引脚对象的传参,并且还能查表,在python中如何指定某个引脚,这都是本节的重点。调用初始化相关工作反而比较柴,直接引用SDK中的API即可。
从make_new()函数开始
mp_pin_make_new()函数是直接登记在machine_pin_type结构体的“.make_new”字段上的,前文提到,我觉得使用machine_pin_obj_make_new()命名更合适,顾名思义,就是要“make a new machine_pin object”。
mp_obj_t mp_pin_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, MP_OBJ_FUN_ARGS_MAX, true);
const machine_pin_obj_t *pin = pin_find(args[0]);
if (n_args > 1 || n_kw > 0) {
// pin mode given, so configure this GPIO
mp_map_t kw_args;
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);
machine_pin_obj_init_helper(pin, n_args - 1, args + 1, &kw_args);
}
return (mp_obj_t)pin;
}
这个函数的声明是python内核同外部模块类约定好的,不能随便改。
第一个参数type在实现代码中没有用到,但实际上是通过python内核传入的“machine_pin_type”对象,哈哈,典型的回调设计,第一个参数是指向包含自己的结构体,如果需要在函数内部使用同属的其它资源,例如获取运行环境相关的信息,就可以通过这个指针拿到,但通常也不会用到。
后面的n_args、n_kw和args实际描述了一个用户传入参数的清单,args表示一个参数(字符串)的列表,n_args表示其中总共有多少个参数,而n_kw表示这些参数中“关键字参数”的数量。
关于位置参数和关键词参数,在通用的python中也有这样的语法现象,具体用法可参见:python中*args和**kw
在代码实现的过程中,第一个参数“args[0]”给pin_find()函数用来找到指定的引脚对象,然后把pin代入handler传入machine_pin_obj_init_helper()中继续执行初始化操作。传入helper()函数时,参数列表已经移出了args参数列表中的第一个元素,传入的n_args数量相应已被减少1,args参数列表的首指针也跳过了原来的首元素
在这个helper()函数中,执行了一堆操作,但最后还是将处理好后的信息填入pin,最后向调用者返回pin对象。
mp_map_init_fixed_table()函数只是做了个打包而已,将参数列表用mp_map_t类型封装一下,以适应helper()函数的传参要求。
至于“args + n_args”,我猜想并不是使用了args后面的内存,而是在python内核里,读写这块内存是倒着读取的,有可能是以栈的结构处理数据的。我这聪明的小脑瓜,v,赞。
看到后面之后专门回来补充这个知识点,之前是想错了,n_args是位置参数的数量,n_kw是关键字参数的数量,args包含了位置参数和关键字参数,args+n_args是直接拿到关键字参数的参数清单。
看一眼“py/map.c”文件中对mp_map_init_fixed_table()函数的定义:
void mp_map_init_fixed_table(mp_map_t *map, size_t n, const mp_obj_t *table) {
map->alloc = n;
map->used = n;
map->all_keys_are_qstrs = 1;
map->is_fixed = 1;
map->is_ordered = 1;
map->table = (mp_map_elem_t *)table;
}
可以看到,它就是把mp_obj_t类型的数组,格式化成mp_map_elem_t类型的指针之后,穿了个mp_map_t的马甲再放出来而已。
这个pin_find()和helper()函数都是很有趣的东西,继续慢慢品。
pin_find()函数
pin_find()函数的定义在“ports/mimxrt/pin.c”中,这也是一个跟具体移植平台相关的定义。
const machine_pin_obj_t *pin_find(mp_obj_t user_obj) {
const machine_pin_obj_t *pin_obj;
// If a pin was provided, then use it
if (mp_obj_is_type(user_obj, &machine_pin_type)) {
pin_obj = user_obj;
return pin_obj;
}
// If pin is SMALL_INT
if (mp_obj_is_small_int(user_obj)) {
uint8_t value = MP_OBJ_SMALL_INT_VALUE(user_obj);
if (value < num_board_pins) {
return machine_pin_board_pins[value];
}
}
// See if the pin name matches a board pin
pin_obj = pin_find_named_pin(&machine_pin_board_pins_locals_dict, user_obj);
if (pin_obj) {
return pin_obj;
}
// See if the pin name matches a cpu pin
pin_obj = pin_find_named_pin(&machine_pin_cpu_pins_locals_dict, user_obj);
if (pin_obj) {
return pin_obj;
}
mp_raise_ValueError(MP_ERROR_TEXT("Pin doesn't exist"));
}
从代码上看,至少在这一层,还没有进行“无中生有”的操作。且看:函数返回的pin_obj,仅是函数内部声明的一个指向machine_pin_obj_t的指针;但必然在下属的函数中执行了“无中生有”否则这里返回的指针变量的值是一个没有意义的随机数。同时,也能获知,“无中生有”搞出来的就是machine_pin_obj_t类型的实例,而每个Pin模块的实例都会拥有各自的machine_pin_obj_t结构体。machine_pin_obj_t结构体在“ports/mimxrt/pin.h”中定义,描述了每个引脚各自不同的硬件属性
typedef struct {
mp_obj_base_t base;
qstr name; // pad name
GPIO_Type *gpio; // gpio instance for pin
uint32_t pin; // pin number
uint32_t muxRegister;
uint32_t configRegister;
uint8_t af_list_len; // length of available alternate functions list
uint8_t adc_list_len; // length of available ADC options list
const machine_pin_af_obj_t *af_list; // pointer to list with alternate functions
const machine_pin_adc_obj_t *adc_list; // pointer to list with ADC options
} machine_pin_obj_t;
再看函数内部的实现,并列用了几种不同的方式提供有效的pin_obj指针,逐个看下去。
// If a pin was provided, then use it
if (mp_obj_is_type(user_obj, &machine_pin_type)) {
pin_obj = user_obj;
return pin_obj;
}
啊哈,这里就是实现用引脚对象实例化的关键了。如果传入的user_obj和machine_pin_type是同一个类型,也就是说user_obj也是一个machine_pin_type,那么就不进行特别的封装了,直接返回给make_new()函数。但同时也能看到,这里的user_obj就是make_new参数列表里的第一个参数。实际执行对硬件的初始化操作应该在helper()函数里,但参数列表里却把这第一个参数拿掉了。NO,第一个参数费了点周折,搞出来个pin,作为一个专门的参数传入了helper(),这个描述了每个对象实例独有硬件信息的结构体的地位反而被提升了。
// If pin is SMALL_INT
if (mp_obj_is_small_int(user_obj)) {
uint8_t value = MP_OBJ_SMALL_INT_VALUE(user_obj);
if (value < num_board_pins) {
return machine_pin_board_pins[value];
}
}
如果用一个整数指代一个引脚,那么就要从machine_pin_board_pins[]这个数组里直接取出预先写好的引脚配置,用传入整数进行索引。这里有两个点:一是说python中一切皆对象,判断user_obj是否为整数,看的是它的对象类型,而不是它的值,如果要拿到它的值,必须用专门的宏函数MP_OBJ_SMALL_INT_VALUE()把值提取出来;二就是这个machine_pin_board_pins[]数组了。
machine_pin_board_pins[]和num_board_pins的声明出现在“ports/mimxrt/pin.h”中(后面的machine_pin_cpu_pins和machine_pin_boards_pins也一并看了):
extern const machine_pin_obj_t *machine_pin_board_pins[];
extern const uint32_t num_board_pins;
extern const mp_obj_type_t machine_pin_board_pins_obj_type;
extern const mp_obj_type_t machine_pin_cpu_pins_obj_type;
extern const mp_obj_dict_t machine_pin_cpu_pins_locals_dict;
extern const mp_obj_dict_t machine_pin_board_pins_locals_dict;
但定义竟然是通过脚本在make环节搞出来的,代码见“ports/mimxrt/boards/make-pins.py”文件:
def print(self):
# Print Pin Object declarations
for pin in self.cpu_pins:
pin.print()
print("")
print("const machine_pin_obj_t* machine_pin_board_pins [] = {")
for pin in self.board_pins:
print(" &pin_{},".format(pin.pad))
print("};")
print("const uint32_t num_board_pins = {:d};".format(len(self.board_pins)))
# Print Pin mapping dictionaries
self.print_named("cpu", self.cpu_pins)
self.print_named("board", self.board_pins)
print("")
如果要自己实现这部分代码,可以直接在c代码中写好,不要用make带python脚本,否则如果玩不转make就太麻烦。比如自己可以手写或者用手动调用脚本预先生成一个很长的引脚清单。如果用表格的话,这个设计就比较巧妙了,本来需要“无中生有”的保存引脚硬件信息的存储块,可以作为常量放在FLASH中,节约了SRAM的开销。
至此,猜想pin_find_named_pin()函数也是通过字符串作为关键字,在一个预先(用make-pins.py)创建好的引脚信息清单中进行检索。你看,代码里还用了“machine_pin_board_pins_locals_dict”,我只看到了“locals_dict”,说不定又是一堆“MP_QSTR_”前缀的定义出来。在make-pins.py中有脚本:
@staticmethod
def print_named(label, pins):
print("")
print(
"STATIC const mp_rom_map_elem_t pin_{:s}_pins_locals_dict_table[] = {{".format(label)
)
for pin in pins:
print(
" {{ MP_ROM_QSTR(MP_QSTR_{}), MP_ROM_PTR(&pin_{}) }},".format(pin.name, pin.pad)
)
print("};")
print(
"MP_DEFINE_CONST_DICT(machine_pin_{:s}_pins_locals_dict, pin_{:s}_pins_locals_dict_table);".format(
label, label
)
)
生成的代码在“ports/mimxrt/build-TEENSY40/pins_gen.c”中,真是很多啊,如果手写估计要烦死了:
...
#define PIN(_name, _gpio, _pin, _af_list, _adc_list_len, _adc_list) \
{ \
.base = { &machine_pin_type }, \
.name = MP_QSTR_##_name, \
.gpio = (_gpio), \
.pin = (uint32_t)(_pin), \
.muxRegister = (uint32_t)&(IOMUXC->SW_MUX_CTL_PAD[kIOMUXC_SW_MUX_CTL_PAD_##_name]), \
.configRegister = (uint32_t)&(IOMUXC->SW_PAD_CTL_PAD[kIOMUXC_SW_PAD_CTL_PAD_##_name]), \
.af_list_len = (uint8_t)(sizeof((_af_list)) / sizeof(machine_pin_af_obj_t)), \
.adc_list_len = (_adc_list_len), \
.af_list = (_af_list), \
.adc_list = (_adc_list), \
} \
static const machine_pin_af_obj_t pin_GPIO_AD_B0_00_af[1] = {
PIN_AF(GPIO1_IO00, PIN_AF_MODE_ALT5, GPIO1, 0x10B0U),
};
const machine_pin_obj_t pin_GPIO_AD_B0_00 = PIN(GPIO_AD_B0_00, GPIO1, 0, pin_GPIO_AD_B0_00_af, 0, NULL);
static const machine_pin_af_obj_t pin_GPIO_AD_B0_01_af[1] = {
PIN_AF(GPIO1_IO01, PIN_AF_MODE_ALT5, GPIO1, 0x10B0U),
};
const machine_pin_obj_t pin_GPIO_AD_B0_01 = PIN(GPIO_AD_B0_01, GPIO1, 1, pin_GPIO_AD_B0_01_af, 0, NULL);
...
const machine_pin_obj_t* machine_pin_board_pins [] = {
&pin_GPIO_AD_B0_03,
&pin_GPIO_AD_B0_02,
&pin_GPIO_EMC_04,
...
};
const uint32_t num_board_pins = 54;
STATIC const mp_rom_map_elem_t pin_cpu_pins_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_GPIO_AD_B0_00), MP_ROM_PTR(&pin_GPIO_AD_B0_00) },
{ MP_ROM_QSTR(MP_QSTR_GPIO_AD_B0_01), MP_ROM_PTR(&pin_GPIO_AD_B0_01) },
{ MP_ROM_QSTR(MP_QSTR_GPIO_AD_B0_02), MP_ROM_PTR(&pin_GPIO_AD_B0_02) },
...
};
MP_DEFINE_CONST_DICT(machine_pin_cpu_pins_locals_dict, pin_cpu_pins_locals_dict_table);
STATIC const mp_rom_map_elem_t pin_board_pins_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_D0), MP_ROM_PTR(&pin_GPIO_AD_B0_03) },
{ MP_ROM_QSTR(MP_QSTR_D1), MP_ROM_PTR(&pin_GPIO_AD_B0_02) },
{ MP_ROM_QSTR(MP_QSTR_D2), MP_ROM_PTR(&pin_GPIO_EMC_04) },
...
};
MP_DEFINE_CONST_DICT(machine_pin_board_pins_locals_dict, pin_board_pins_locals_dict_table);
helper()函数
find_pin()函数帮忙获取了一个有效的指向引脚硬件信息的结构体,mp_map_init_fixed_table()函数对参数清单进行了封装(其实没必要)。向helper()函数传参时,还帮忙整理了参数列表,把原参数列表中的第一个参数提取出来,再把新的参数列表传入helper()。n_args-1,args+1和&kw_args,其实将原参数列表拿掉第一个参数后形成的干干净净的参数列表,第一个参数已经专门通过pin传入。
helper()函数是新增模块要实现的最重要的函数。
STATIC mp_obj_t machine_pin_obj_init_helper(const machine_pin_obj_t *self, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_mode, ARG_pull, ARG_value };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_mode, MP_ARG_OBJ, {.u_obj = mp_const_none}},
{ MP_QSTR_pull, MP_ARG_OBJ, {.u_obj = MP_OBJ_NEW_SMALL_INT(-1)}},
{ MP_QSTR_value, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
};
// parse args
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);
...
这里的参数列表没毛病,make_new()中传给位置参数pos_args的就是args+1,传给关键字参数kw_args的是args+n_kw的一个变体。
一开始这部分是用来解析关键字参数的,allowed_args[]定义了关键字参数中可用的关键字列表,每个表项三个内容:关键字名字的qstr字符串、参数属性和默认值,见“py/runtime.h”文件中的mp_arg_t的定义。
typedef enum {
MP_ARG_BOOL = 0x001,
MP_ARG_INT = 0x002,
MP_ARG_OBJ = 0x003,
MP_ARG_KIND_MASK = 0x0ff,
MP_ARG_REQUIRED = 0x100,
MP_ARG_KW_ONLY = 0x200,
} mp_arg_flag_t;
typedef struct _mp_arg_t {
uint16_t qst;
uint16_t flags;
mp_arg_val_t defval;
} mp_arg_t;
算了,找不到对mp_arg_flag_t的使用说明,先放在这里,后面研究mp_arg_parse_all()函数的工作机制时,看各个参数是怎么用的吧。
mp_arg_parse_all()函数的定义位于“py/argcheck.c”文件中:
void mp_arg_parse_all(size_t n_pos, const mp_obj_t *pos, mp_map_t *kws, size_t n_allowed, const mp_arg_t *allowed, mp_arg_val_t *out_vals)
这里看一下函数的输入输出就好,不再看内核的实现细节了。
- n_pos传入的是位置参数的数量
- pos传入的是位置参数的参数列表
- kws传入的是穿了马甲的关键字参数列表,也包含参数数量
- n_allowed和allowed传入的是关键字参数匹配的数量和关键字列表
- out_val是输出列表,按匹配关键字的顺序排列
typedef union _mp_arg_val_t {
bool u_bool;
mp_int_t u_int;
mp_obj_t u_obj;
mp_rom_obj_t u_rom_obj;
} mp_arg_val_t;
后面能解析到参数,就可以直接使用args[0]、args[1]里面的内容了。
例如:
// Get io mode
uint mode = args[PIN_INIT_ARG_MODE].u_int;
if (!IS_GPIO_MODE(mode)) {
mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("invalid pin mode: %d"), mode);
}
到此为止,python就已经能访问到底层了,此处用SDK的驱动程序操作硬件均可。
END