完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
在上文中,我根据micropython开发文档中的约定,设计了machine.ADC模块的接口,并分析了ADC模块的传参方式,并确定了ADC模块先启动转换器,再向转换序列中添加转换通道的实现思路。
实现启动ADC转换器的思路 虽然在 import ADC 时启动ADC转换器的时机是最理想的(只要确定要在程序中使用ADC,就先把ADC转换器预热起来),但我暂时没有找到micropython开放给底层移植的相关接口(也可能就真的没有,以后需要动内核才能实现)。我曾经想过在启动micropython的时候,直接把底层所有的外设(包括ADC)全部启动起来,即使完全不用ADC,也要让ADC转换器空跑着,但这样会让整个系统很浪费电。还有一种考虑,在实例化第一个ADC通道时启动ADC转换器,这确实是一个比较“智能”的做法。但最终,我选择了最笨的办法:在ADC类的实例化函数中,添加了一个关键字参数“init”,用来表示是否需要重新初始化ADC转换器,这样做的好处在于,用户在应用程序中可以在启用了多个通道之后,清空转换队列并重新构造新的转换队列。之前的实现,包括micropython开发文档中提供的API,都没有体现出关闭ADC转换器或者ADC转换通道的操作,在我使用的笨办法中,未增加API的前提下,使用一个关键字参数,在某种程序上实现了关闭部分ADC转换通道得需求(清空转换队列,再重建新队列)。其实,在理想情况下,我更希望能在micropython的gc回收adc的实例时,能触发某个开发者创建ADC类时编写的回调函数,将相应的ADC通道关闭。 我使用了全局变量 adc_working_conv_seq 记录当前已经开启的转换通道,当init=True,adc_working_conv_seq = new_channel_mask,否则, adc_working_conv_seq |= new_channel_mask。 特别希望能找到import ADC的回调函数,用于实现启动ADC转换器的操作。 特别希望能找到del adc的回调函数,用于实现停用ADC转换通道的操作。 实现ADC API的回调函数 “machine_adc_type”就是ADC模块的回调入口: /* class locals_dict_table. */ STATIC const mp_rom_map_elem_t machine_adc_locals_dict_table[] = { /* Class instance methods. */ { MP_ROM_QSTR(MP_QSTR_read_u16), MP_ROM_PTR(&machine_adc_read_u16_obj) }, { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&machine_adc_init_obj) }, }; STATIC MP_DEFINE_CONST_DICT(machine_adc_locals_dict, machine_adc_locals_dict_table); const mp_obj_type_t machine_adc_type = { { &mp_type_type }, .name = MP_QSTR_ADC, .print = machine_adc_obj_print, /* __repr__(), which would be called by print( .call = machine_adc_obj_call, /* __call__(), which can be called as .make_new = machine_adc_obj_make_new, /* create new class instance. */ .locals_dict = (mp_obj_dict_t *)&machine_adc_locals_dict, }; “.print”, “.call"和”.make_new"是内核级别的回调函数,其中make_new对应类对象的实例化函数,init函数将放于此。machine_adc_locals_dict里放的就是类对象内部可以用字符串索引的属性(方法)了,根据micropython开发手册中对ADC的描述,这里仅实现了init()函数和read_u16()函数。 实现ADC的实例化函数 machine_adc_obj_make_new() -> machine_adc_obj_init_helper() machine_adc_init() -> machine_adc_obj_init_helper() make_new()和init() 都调用了machine_adc_obj_init_helper() , helper()函数就是对底层操作的可复用代码的打包。 调用init()函数时,helper()函数的第一个参数已经指定了ADC转换通道的设备实例。但在调用make_new()函数时,通过参数传进来的只是一些提示片段,ADC通道号或者引脚编号等,此时就遇到了前文中提到的“找引脚”的问题。“找引脚”不仅仅要找到对应的ADC转换通道号以加入转换队列,还要找到对应的GPIO引脚以修改引脚的复用功能为模拟输入。 adc_find() 我希望实现多种方式定位到ADC的转换通道所对应的引脚: 通过ADC转换通道号找到对应引脚。这需要维护一个以通道号作为索引的引脚表格,使用通道号找引脚。 如果本身已经是一个ADC对象,则直接使用这个ADC对象实例化一个新的ADC对象(副本)。 通过GPIO引脚对象,找到对应ADC的通道。这需要在“以通道号作为索引的引脚表格”进行倒排查询,使用引脚找通道。 为此,类似于machine_pin_obj_t,我设计了 machine_adc_obj_t 表示adc转换通道对象,位于“machine_adc.h”文件中: /* ADC class instance configuration structure. */ typedef struct { mp_obj_base_t base; // object base class. const machine_pin_obj_t * pin_obj; ADC_Type * adc_port; uint32_t adc_channel; } machine_adc_obj_t; 如代码中所示,adc_obj 中必须包含base, 这使得这个结构体类型的实例可以适用于micropython中的通用类处理方法。另外,ADC通道对象会绑定一个具体的引脚(配置复用功能,标记ADC通道等等),所以内部包含了一个 pin_obj。另外,adc_port和adc_channel就是adc_obj专属的ADC属性了。 另外,我还定义了一个 adc_obj 的列表,machine_adc_objs,位于 machine_pin_board_pins.c中,这个表就是 adc_find() 需要的索引表。 ... const machine_adc_obj_t adc_CH15 = { .base = { &machine_adc_type }, .pin_obj = &pin_NUL, .adc_port = ADC1, .adc_channel = 15 }; ... /* for ADC pin. */ const machine_pin_obj_t pin_NUL = { .base = { &machine_pin_type } }; const uint32_t machine_adc_num = 16u; const machine_adc_obj_t adc_CH0 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA0, .adc_port = ADC1, .adc_channel = 0u }; const machine_adc_obj_t adc_CH1 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA1, .adc_port = ADC1, .adc_channel = 1u }; const machine_adc_obj_t adc_CH2 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA2, .adc_port = ADC1, .adc_channel = 2u }; const machine_adc_obj_t adc_CH3 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA3, .adc_port = ADC1, .adc_channel = 3u }; const machine_adc_obj_t adc_CH4 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA4, .adc_port = ADC1, .adc_channel = 4u }; const machine_adc_obj_t adc_CH5 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA5, .adc_port = ADC1, .adc_channel = 5u }; const machine_adc_obj_t adc_CH6 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA6, .adc_port = ADC1, .adc_channel = 6u }; const machine_adc_obj_t adc_CH7 = { .base = { &machine_adc_type }, .pin_obj = &pin_PA7, .adc_port = ADC1, .adc_channel = 7u }; const machine_adc_obj_t adc_CH8 = { .base = { &machine_adc_type }, .pin_obj = &pin_PB0, .adc_port = ADC1, .adc_channel = 8u }; const machine_adc_obj_t adc_CH9 = { .base = { &machine_adc_type }, .pin_obj = &pin_PB1, .adc_port = ADC1, .adc_channel = 9u }; const machine_adc_obj_t adc_CH10 = { .base = { &machine_adc_type }, .pin_obj = &pin_PC0, .adc_port = ADC1, .adc_channel = 10u }; const machine_adc_obj_t adc_CH11 = { .base = { &machine_adc_type }, .pin_obj = &pin_PC1, .adc_port = ADC1, .adc_channel = 11u }; const machine_adc_obj_t adc_CH12 = { .base = { &machine_adc_type }, .pin_obj = &pin_PC2, .adc_port = ADC1, .adc_channel = 12u }; const machine_adc_obj_t adc_CH13 = { .base = { &machine_adc_type }, .pin_obj = &pin_PC3, .adc_port = ADC1, .adc_channel = 13u }; const machine_adc_obj_t adc_CH14 = { .base = { &machine_adc_type }, .pin_obj = &pin_NUL, .adc_port = ADC1, .adc_channel = 14u }; const machine_adc_obj_t adc_CH15 = { .base = { &machine_adc_type }, .pin_obj = &pin_NUL, .adc_port = ADC1, .adc_channel = 15u }; const machine_adc_obj_t * machine_adc_objs[] = { &adc_CH0 , &adc_CH1 , &adc_CH2 , &adc_CH3 , &adc_CH4 , &adc_CH5 , &adc_CH6 , &adc_CH7 , &adc_CH8 , &adc_CH9 , &adc_CH10, &adc_CH11, &adc_CH12, &adc_CH13, &adc_CH14, &adc_CH15, }; /* CH14 & CH15 are for internal sensors. */ 同pin_obj类似,这里使用了静态数组的方式创建adc_obj,避免了再make_new() 时动态分配内存创建,无论是在代码执行效率还是系统安全性上,对于MCU平台都是十分友好的。 这里还有一个设计要点,创建了 “pin_NUL” 表示没有具体指定 GPIO 端口号的引脚。如此,对于ADC_CH14和ADC_CH15,就只能通过ADC通道号索引到对应的adc_obj了。 好了,让我们再看一下最终的 adc_find() 函数的实现: const machine_adc_obj_t *adc_find(mp_obj_t user_obj) { //const machine_pin_obj_t *pin_obj; /* 如果传入参数本身就是一个ADC的实例,则直接送出这个ADC。 */ if ( mp_obj_is_type(user_obj, &machine_adc_type) ) { return user_obj; } /* 如果传入参数本身就是一个Pin的实例,则通过倒排查询找到包含这个Pin对象的ADC通道。 */ if ( mp_obj_is_type(user_obj, &machine_pin_type) ) { for (uint32_t i = 0u; i < machine_adc_num; i++) { machine_pin_obj_t * pin_obj = (machine_pin_obj_t *)(user_obj); if ( (pin_obj->gpio_port == machine_adc_objs->pin_obj->gpio_port) && (pin_obj->gpio_pin == machine_adc_objs->pin_obj->gpio_pin) ) { return machine_adc_objs; } } } /* 如果传入参数是一个ADC通道号,则通过索引在ADC清单中找到这个通道,然后送出这个通道。 */ if ( mp_obj_is_small_int(user_obj) ) { uint8_t adc_idx = MP_OBJ_SMALL_INT_VALUE(user_obj); if ( adc_idx < machine_adc_num ) { return machine_adc_objs[adc_idx]; } } mp_raise_ValueError(MP_ERROR_TEXT("ADC doesn't exist")); } 在函数中,使用系统函数 mp_obj_is_type() 判断传入对象的类型,例如 adc_type、pin_type,从而相应各自不同的处理方式。 特别注意,在其中传入pin_obj时,是在machine_adc_objs 中,逐个比较表中的ADC通道内的GPIO编号,从而选定他们的父结构作为目标对象。我一时没有想到更巧妙的索引方法,暂时用笨办法实现,所幸这些操作只是在应用的初始化过程中使用,对整个应用的执行效率影响不大。 make_new() make_new()函数是另一个设计要点。make_new() 函数要把adc_obj参数从一开始无差别的参数清单中解析出来,并且要为解析关键字参数 init 做准备。 /* return an instance of machine_pin_obj_t. */ mp_obj_t machine_adc_obj_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_adc_obj_t *adc = adc_find(args[0]); if ( (n_args >= 1) || (n_kw >= 0) ) { mp_map_t kw_args; mp_map_init_fixed_table(&kw_args, n_kw, args + n_args); /* 将关键字参数从总的参数列表中提取出来,单独封装成kw_args。 */ machine_adc_obj_init_helper(adc, n_args - 1, args + 1, &kw_args); } return (mp_obj_t)adc; } 当micropython内核调用make_new()函数时,在第一个参数传入的,是micropython认为是该类实例对象的类型,而不是包含初始化信息的实例化新对象。如果这里打算用动态内存分配,倒是可以用 mp_obj_is_type() 判断 type 参数的类型,然后对应分配一块内存用于保存该类型的对象。真正的传参信息位于args数组中。 args中保存的即是实例化参数列表,其中第一个参数,就是用以索引ADC通道的关键信息,args[0] 被传入adc_find()函数,返回adc_type 类型的 对象。这个转换仅发生一次,之后的字符串方法的第一个参数,都已经是这里已经查找好的adc_obj了。 n_args记录了字符串参数的总个数,n_kw记录了字符串参数列表中关键字参数的个数。这里是我在具体调试环节中遇到的一个坎。在我原本的设计中,用于表示是否重置转换队列的 init 关键字总是无法正确识别,我甚至尝试在 helper() 函数中调整了 init 参数的属性,仍是不起作用。一顿各种试之后,才改到这里。在Pin类的make_new函数实现中,对n_args和n_kw的判断条件是: if ( (n_args > 1) || (n_kw > 0) ) 1 这是因为Pin的实例化函数的实际有效参数是两个以上,但对于ADC,只有第一个有效参数是必要的,因此判断条件应该加上等号 “=”,在n_kw = 0 时,也要能够执行到 helper() 函数。在helper() 函数中,n_kw值的不同将会对应不用的执行过程。 STATIC mp_obj_t machine_adc_obj_init_helper ( const machine_adc_obj_t *self, /* machine_adc_obj_t类型的变量,包含硬件信息 */ size_t n_args, /* 位置参数数量 */ const mp_obj_t *pos_args, /* 位置参数清单 */ mp_map_t *kw_args ) /* 关键字参数清单结构体 */ { printf("%machine_adc_obj_init_helper().rn"); static const mp_arg_t allowed_args[] = { //[ADC_INIT_ARG_MODE] { MP_QSTR_init , MP_ARG_REQUIRED | MP_ARG_BOOL, {.u_bool = false} }, [ADC_INIT_ARG_MODE] { MP_QSTR_init , MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, }; ... } 在helper() 函数中的参数解析清单中,指定 init 参数的默认值为False。 PS:这里我也挺奇怪的,MP_QSTR_init被用了两次,init()函数和init参数,竟然没打架? 其它常规实现函数 剩下的就是常规操作: 在helper() 函数中执行对底层API的调用 完成print() 函数和 call() 函数 完成init() 函数和 read_u16() 函数,init() 传入的是参数清单,read_u16()的第一个参数是adc_obj。这个传参模型还是有点迷惑??? 一些收尾工作 在clock_init.c中为ADC模块打开时钟: void BOARD_InitBootClocks(void) { CLOCK_ResetToDefault(); CLOCK_BootToHSI96MHz(); ... /* ADC1. */ RCC_EnableAPB2Periphs(RCC_APB2Periph_ADC1, true); RCC_ResetAPB2Periphs(RCC_APB2Periph_ADC1); } 在 modmachine.c 中添加ADC类的代码: extern const mp_obj_type_t machine_pin_type; extern const mp_obj_type_t machine_adc_type; ... STATIC const mp_rom_map_elem_t machine_module_globals_table[] = { ... { MP_ROM_QSTR(MP_QSTR_Pin), MP_ROM_PTR(&machine_pin_type) }, { MP_ROM_QSTR(MP_QSTR_ADC), MP_ROM_PTR(&machine_adc_type) }, ... }; 在makefile中添加新增的文件,并且添加到QSTR检索路径中: SRC_HAL_MM32_C += $(MCU_DIR)/devices/$(CMSIS_MCU)/system_$(CMSIS_MCU).c ... $(MCU_DIR)/drivers/hal_adc.c ... SRC_C += main.c modmachine.c machine_pin.c machine_sdcard.c machine_adc.c ... $(SRC_TINYUSB_C) ... # List of sources for qstr extraction SRC_QSTR += modmachine.c machine_pin.c machine_adc.c modutime.c $(BOARD_DIR)/machine_pin_board_pins.c 然后启动编译,清理一下杂七杂八的编译包含问题等等,就可以执行代码了。 实际执行 目前用来调程序的板子上没有直接用来测量ADC的设备(全都是各种插针),好不容易找到两各连了上拉电阻的引脚,可以采一个接近VCC的电压回来, 板子运行后,在终端中运行脚本如下: MicroPython v1.16 on 2021-10-26; MB_F3270 with MM32F3277G7P >>> from machine import Pin >>> from machine import ADC >>> pin0 = Pin('PB0') >>> adc0 = ADC(pin0, init=True) >>> adc0.read_u16() 65280 >>> print(adc0) ADC(8) >>> adc1 = adcADC(9) >>> adc1.read_u16() 65280 >>> print(adc1) ADC(9) >>> |
|
|
|
只有小组成员才能发言,加入小组>>
3278 浏览 9 评论
2955 浏览 16 评论
3455 浏览 1 评论
8987 浏览 16 评论
4050 浏览 18 评论
1102浏览 3评论
570浏览 2评论
const uint16_t Tab[10]={0}; const uint16_t *p; p = Tab;//报错是怎么回事?
568浏览 2评论
用NUC131单片机UART3作为打印口,但printf没有输出东西是什么原因?
2301浏览 2评论
NUC980DK61YC启动随机性出现Err-DDR是为什么?
1857浏览 2评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-22 04:11 , Processed in 1.178714 second(s), Total 81, Slave 61 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号