在mm32f3270上为micropython创建ADC模块(2)
苏勇,2021年10月
文章目录
- 在mm32f3270上为micropython创建ADC模块(2)
- 前情回顾
- 实现启动ADC转换器的思路
- 实现ADC API的回调函数
- 实现ADC的实例化函数
- adc_find()
- make_new()
- 其它常规实现函数
- 一些收尾工作
- 实际执行
前情回顾
在上文中,我根据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(<ClassName>). */
.call = machine_adc_obj_call, /* __call__(), which can be called as <ClassName>(). */
.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[i]->pin_obj->gpio_port)
&& (pin_obj->gpio_pin == machine_adc_objs[i]->pin_obj->gpio_pin) )
{
return machine_adc_objs[i];
}
}
}
/* 如果传入参数是一个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) )
这是因为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().\r\n");
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的电压回来,
Figure ADC Test Samples
板子运行后,在终端中运行脚本如下:
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)
>>>
- END