在我的上一篇文章《 FireBLE裸奔之四欣赏Firefly精心设计之作-五向按键的驱动电路 》
中,已经比较详细的分析了五向按键的设计思路和设计流程(当然,不能用分析专业模拟电路的眼光来看待这篇分析,但是写程序没必要用这么专业的电路分析眼光,我们分析清楚逻辑就好),本文着重根据FireBLE的板子资源来进行讲解五向按键的驱动程序的设计思路。
根据《FireBLE裸奔之四欣赏Firefly精心设计之作-
五向按键的驱动电路》文中分析得知,FireBLE的五向按键使用到了2个资源,即分别为外部中断和ADC,那么如果来根据这两个资源来进行按键的驱动使用呢?先不提后面FireBLE跑协议栈是应用的架构。通常的使用方案如下:
(1)FireBLE主打低功耗,那么MCU进入睡眠模式必然是正常的事(这样才能很好的降低功耗),那么睡眠就需要唤醒的,通常RTC是一种方式,但是,通常情况下,MCU根本就不知道什么时候该被唤醒啊(实际上作为程序员的我们也不知道),那么最好的方法就是使用中断啦,当条件满足是,就产生中断,从而唤醒MCU起来干活。如下电路:
就是这条资源了!P1.2口,作为外部中断口。并且从电路得知此端口空闲时有R46即10K电阻上拉,那么空闲时的电平即为高电平,那么当按键按下时,MOS管Q6导通,相当于端口直接接地,即被拉低,所以就可以,判断,应该使用下降沿触发产生中断。
(2)通过分析电路,五向按键的键值是通过ADC进行采集,从而根据分析采集到的键值来区分所按下的按键的。如下电路:
P3.0口就是作为ADC采集端口了。(3)从以上两点的分析,基本上就知道了所用到的资源了。那么他们是如何使用的呢??我的处理方案是这样的:当有任意按键按下时,将会产生中断,在产生中断时,使用ADC对键值进行采集(采集50次,求平均值),根据键值进行判断是哪一个按键按下,那么这就存在一个问题了,只要是机械式按键,都存在抖动,那么该如何消抖呢?这个问题值得思考。
基本上思路就分析清楚了!下面进行分点实现:
1、实现按键中断。
呵呵!按键中断就是按下按键,触发MCU的外部中断,所以我们要干的事很简单,就是配置P1.2口,配置中断的触发方式为下降触发,使能中断,编写中断函数。
初始化程序就如上了,对于程序中的第一句gpio_init(key_Intrrupt);调用,暂时先不用管,在我前面的文章中我也从未调用过此函数,这将在下面进行详解。除此之外,其他调用语句就简单了,设置P12口外上拉、设置P12口为输入模式、设置P12口为下降沿触发中断口、使能P12口的外部中断功能。
思路很简单,但是呢,关于第一句的调用,可能有些童鞋是存在疑问的。 那么我们先看此函数的原型:
函数很简单,分为三个部分,将参数传输给结构变量、使能GPIO时钟、使能GPIO外部中断。那么问题就来了:
(1)此函数的参数是什么参数?为毛传输给一个结构体变量就没下文了?
关于这个问题,我们就得好好的跟踪代码了,首先确定参数的类型:
呵呵!是一个函数指针类型,再往下:
意思很明确了,上图中的语句就是将从形参传来的函数指针赋值给结构体变量gpio_env.callback,那么就是说,在程序员使用这个函数是,必须存在一个如上函数指针类型的回调函数。
其实此结构体变量也很简单,如下图:
没错,非常简单,结构体的成员只有一个,即函数指针。OK!那么此问题就先告一段落,只有这个调用的目的何在,后面自会分晓。
(2)既然使能了中断,那么中断函数在哪里?
对的!中断函数在何方??找到它最好最快的方法是,查看中断向量表,此表存在于MCU的启动文件中,即startup.s文件,如下图:
通过查表可以发现,和GPIO直接相关的中断函数只有一个,即为GPIO_IRQHandler,所以基本可以判断外部中断函数名为GPIO_IRQHandler,全局搜索即可找到中断函数。如下图:
从图中可以知道,此中断函数分为3个部分,首先判断是否启用低功耗模式,所以先唤醒,然后进行一些寄存器的配置,因为官方不提供参考手册,所以,此部分内容可以不管,直接绕过;最后就是获取GPIO的状态了,从而判断是哪一个GPIO口产生了中断。嘿!看见没,我用蓝色框框出来了,上面谈到的结构体变量使用到了,而且是一回调函数指针形式被调用的,所以基本上就很明确了,FireBLE使用函数指针和回调函数的方式将接口留给开发者进行中断内容代码的编写,这样就不会影响整个代码的架构设计,有额很好的防止了由于开发人员修改库文件内容而导致BUG的产生。所以针对我们基于平台编写软件的开发者,中断函数就是这个回调函数了。
在此解释一下,QN9021这款基于Cortex-M0的MCU的外部中断可能和其他通用系列的MCU的外部中断有点不太一样,就是外部中断的入口地址只有一个,即为GPIO_IRQHandler,但是人性化的是我们可以在中断通过读取寄存器值辨别是哪一个引脚产生了中断。
(3)在FireBLE的整个API代码库框架中,如果每次使用GPIO口,是不是都要使用这个函数进行初始化呢?如果是,那么当有多路外部中断时应该如何处理呢?如果没有用到外部中断,但是却使用这个函数进行GPIO的初始化,那么不就是打开中断了?那打开中断有何用?
关于这个问题,在前面我点灯的时候就说过了,所以,在此程序之前,我并没有调用此函数作为IO初始化,其实当IO口作为输出作用是,对程序有用的地方只有GPIO时钟的打开,所以还是养成一个好习惯,自觉的将时钟打开放在第一位。
OK!这些都搞清楚了!那么就可以回到我的调用了,gpio_init(key_Intrrupt);
我使用了这样的一句调用,已经表明,key_Intrrupt为回调函数,也可以叫它用户的外部中断函数。那么它的内容如下图:
很简单!先判断是不是P12口产生的中断,然以执行相应的操作,在这里我只是为了验证我的中断部分程序的可行性,所以,对一个LED的引脚电平进行翻转点灯。
主函数如下:
2、ADC采集键值
其实这才是重头戏,想都不用想!第一步肯定是调通MCU相应的ADC端口,然后再干事。按照正常步骤来!先初始化ADC,配置好ADC然后在进行采集AD值。如下:
(1)ADC初始化配置:
不多说!贴代码才是大事!
代码就如上了,我把它分成了4部分,先将P12口配置为模拟输入模式。然后使能ADC通道0、然后初始化ADC、再然后配置ADC的采集模式。值得分析的是后面两个部分了。那么且听我一一道来:
ADC初始化的原型:
内容还挺复杂,并且都是配置寄存器和调用库,没手册不好玩,所以就不看了,先看函数名和注释再说。基本上可以确定了,无非就是ADC的输入模式、工作时钟、ADC参考电压的选择和ADC精度的选择的配置。
这些都配置好了枚举,直接进行选择配置即可,我的配置如:
单端输入无缓冲ADC_SINGLE_WITH_BUF_DRV,在Datasheet中查的ADC的工作时钟可达1MHz,所以就让它跑的最快吧,ADC_CLK_1000000,关于参考电压的选择,通过查看原理图和Datasheet得:
此封装MCU不提供外部参考电压输入口,所以,就果断使用内部参考电压了。ADC_INT_REF,但是值得注意的是,内部参考电压为:VREF =
1.0V。这很重要。最后就是ADC精度的选择了,我不明白的是,在Datasheet中有这样的参考说明:
存在4个通道的10位精度的通用ADC。但是在库中却给出了12位精度的ADC进行选择,这。。。。。。。。。。。。。我真的不懂,但是作为库的提供,通常应该是没问题的,所以,既然精度高,那么我也要玩高精度的,果断的选择了12位精度。ADC_12BIT。
关于ADC的采集模式,也没啥,直接看需要,而且这是一个结构体,并且在进行采集ADC值时需要用到,所以将其定义为全局的即可,如:
adc_read_configuration
read_cfg;那么直接配置就好,分别为:ADC采集模式为突发模式、有软件触发采集、因为只用到一个通道的ADC,所以起始通道和结束通道均为AIN0。
初始化配置就这么完事了。
(2)按键初始化配置:
不管那么多!一眼即可看懂的代码。
(3)编写ADC采集函数:
其实,ADC的采集函数,官方也提供好了!只需要了解其函数即可,且看函数原型。
内容也不看了!只看注释和函数名原型即可,四个参数分别为: ADC采集的配置(就是前面定义的adc_read_configuration
read_cfg;变量啦)、缓存Buffer、采集的次数、和回调函数
。呵呵!。。。。。。又是回调函数,这是这个代码架构的武器之一哇!不管他啦!它需要啥,满足他就好。于是乎就有了如下:
呵呵!回调函数看见了,啥都没用,只是给一个全局变量赋值。嘿嘿!你看的没错,这是中断函数哇!我在中断函数中调用ADC进行采集,50次。啥意思呢?
其实就是,当按键按下,P12就好产生中断,那么我就在中断的这里进行键值采集,本身键值采集是需要时间的,再加上在这之前我使用for循环延时了一下,这就达到了按键消抖的效果,从而读取到的键值是正确的,稳定的。就是这样!
那么!ADC值采集了!但是还是需要进行一些处理的,所以,还应该存在一些函数,如下:
关于前两个函数,我只是对公用全局变量进行一下封装而已,以使得以后大量的工程代码使用修改的方便。最下面的函数当然是获取50次键值的平均值啦。
不多说!就是计算一下采集电压。当然,库中也存在一个函数进行调用,单位为mV。就是它了:
int16_t ADC_RESULT_mV(int16_t adc_data)
不过目前我的计算值是不准确的,因为我的ADC的取值没有经过移位的运算,所以不准确。这是ADC值采集的对齐关系,不过我并不想纠结过多,有兴趣的朋友可以自己玩玩,因为这并不影响我判断键值,而我的目的不是测电压,而是判断键值。
(4)主函数调用
且看我的调用!当按键按下,P12口产生中断,则进行ADC键值的采集,采集50次,完成之后,回调函数将全局变量adc_done置1,为真,即Get_Key_Mark()返回真(Get_Key_Mark()为全局变量adc_done的一个封装),则进入,先将adc_done清空,计算AD平均键值,然后打印键值和电压。就是这么简单。当五向按键往不同的方向按下时,就会打印不同的键值。就是这么简单!
Rebuild编译!无错误来无警告!烧录运行!我次奥!理想很丰满,现实很骨感。这尼玛程序跑不下来!脸打印语句打印的一串WWWW的地方都没跑到,就不知道死哪去了!呵呵!好玩了!折腾来了!
那么!先分析一下,首先,这while循环之前,关于ADC的调用的全是库函数,我只做了参数的配置,那么久意味着死机死在了库函数里面!呵呵!为了验证这种假设,多次运行代码跟踪(注意,我这里所说的代码跟踪不是用J-
Link或者其他Link,这种方法除非很必要才用),终于找到了死机的地方,如下:
adc_init(ADC_SINGLE_WITHOUT_BUF_DRV,ADC_CLK_1000000,ADC_INT_REF,
ADC_12BIT);函数的最后一行代码:
从函数名可知,此函数是作为ADC校准使用!那么继续跟踪:
跟踪到此!死在了蓝色框的函数里面!于是乎不到黄河不死心的再跟踪:
呵呵!到此!啥都没有了!这就是一个函数声明!啊啊啊啊啊!几个意思!死在了库里面!那还玩个啥呢????
最后,在Firefly的FireBLE的Wiki上的ADC驱动教程上了解到,哎呀!不想说了!有图有真相:
好!平台初始化是吧!OK!我们先来看看以前的初始化!如下图:
呵呵!内容挺多!至于是啥意思也不仔细分析了!干事要紧,将以上内容干掉,在将其改为如下:
改好了!Rebuild编译!烧录。。。。。。运行!我次奥!通过了,打印了一串WWWW字符串!按下各个方向的按键!很好!消抖情况很好!基本上很少出现抖动!嘿嘿!就这么愉快的完事了(其实是折腾)。效果如下:
以上就是各个方向的键值打印了!
总结: 本文详细的讲述了五向按键或者说是AD按键的使用过程,对于五向按键来说,中断的存在会减轻很大的代码的复杂度!
当然了,其他的好处你我都懂。那么关于按键的具体识别,我将其放到了下一篇文章中进行讲述!也顺便描述或者说是搭建一个简易的用户接口的MCU裸机软件程序框架。