上一节,我们讲解了如何使用Imx6uL上面的定时器EPTI,这一节我们将使用EPTI进行实战,也就是使用定时器的效果来使用延时
在之前的实验中我们都使用到了按键,用到按键就要处理因为机械结构带来的按键抖动问题,也就是按键消抖。前面的实验中都是直接使用了延时函数来实现消抖,因为简单,但是直接用延时函数来实现消抖会浪费 CPU 性能,因为在延时函数里面 CPU 什么都做不了。如果按键使用中断的话更不能在中断里面使用延时函数,因为中断服务函数要快进快出
使用定时器可以实现按键消抖,使用定时器既可以实现按键消抖,而且也不会浪费CPU性能。
按键消抖的原理之前我们已经讲解过了,这里简单讲解一下,在按键按下以后延时 一段时间再去读取按键值,如果此时按键值还有效那就表明这一次有效的按键,中间的延时就是消抖的。但是延时函数会浪费CPU的性能。如果按键是用中断方式实现的,那就更不能在中断函数中使用延时函数,因为中断都要求快进快出
所以在我们了解到定时器的基础上,定时器设置好时间,CPU就可以去做其他的事情了,定时时间到了过后就会触发中断然后在中断中做相应的处理即可。因此,我们可以借助定时器来实现消抖,按键采用中断驱动方式,当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为 10ms,当定时时间到了以后就会触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。
如上图, t1~t3 这一段时间就是按键抖动,是需要消除的。设置按键为下降沿触发,因此会在 t1、t2 和 t3 这三个时刻会触发按键中断,每次进入中断处理函数都会重新开器定时器中
断,所以会在 t1、t2 和 t3 这三个时刻开器定时器中断。但是 t1~t2 和 t2~t3 这两个时间段是小于
我们设置的定时器中断周期(也就是消抖时间,比如 10ms),所以虽然 t1 开启了定时器,但是定时器定时时间还没到呢 t2 时刻就重置了定时器,最终只有 t3 时刻开启的定时器能完整的完成整个定时周期并触发中断,我们就可以在中断处理函数里面做按键处理了,这就是定时器实现按键防抖的原理
下面梳理一下我们具体需要完成的思路,这个章节没有学习新的外设和内设,所以只需要对之前的知识点进行一个复习与总结
1.配置按键所使用的IO,因为要使用到中断驱动按键,所以需要配置IO的中断模式
2.初始化消抖用的定时器
消抖需要使用定时器来完成,所以需要初始化一个定时器,这里使用我们之前使用过的EPTI1定时器的一次巩固。定时器的定时周期为10ms,大概预测的按键可能会抖动的时间
3.编写中断处理函数
需要编写两个中断处理函数,按键对应的GPIO中断处理函数和EPIT1定时器的中断处理函数。在按键的中断处理函数中主要用于开启EPIT1定时器,EPIT1的中断处理函数才是重点,按键要做的具体任务都是在定时器EPIT1的中断处理服务函数中完成的,例如控制蜂鸣器或者关闭
这一讲的效果就是按下按键,蜂鸣器会打开,再次按下则关闭,灯会一直闪烁,实验相比于之前都更简单,这里我们就直接开始编写代码
首先是创建一个文件夹,名字叫做Keyfilter,然后在文件夹的下方创建一个.c和.h文件,名字都是bsp_keyfilter,首先就是按键的初始化
#include "bsp_keyfilter.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_epittimer.h"
void filterkey_init(void)
{
gpio_pin_config_t key_config;
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18,&key_config);
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,(system_irq_handler_t)gpio1_16_31_irqhandler,NULL);
gpio_enableint(GPIO1,18); /*使能GPIO1_IO18的中断功能*/
filtertimer_init(66000000/100); /*初始化定时器,10ms*/
}
在之间定义了结构体gpio_pin_config_t,定义了一个结构体变量,它的作用就是存储和表示一个GPIO引脚的配置信息。
因为这一节是对前面知识的复习,所以这里我们再用通俗的语言,来实际理解一下结构体变量(实例)与结构体成员的关系
首先,假如我们有一个工具箱,这个工具箱中有很多不同种类的工具,例如螺丝刀、扳手、锤子等。每种工具都有它自己的用途和特点。现在我们要将这些工具的信息(例如工具的名称、用途、存放位置等)整理起来,方便之后的查找和使用
这个时候,我们就可以去创建一个工具信息表,这个表就类似于我们的结构体,在其中它定义了每种工具需要记录的信息类型,加入我们的工具信息表有以下字段 ,工具名称(如“螺丝刀”),工具用途(如“拧紧螺丝”),存放位置(如“第一层,第二个格子”)
现在,我们有一个具体的螺丝刀,你想把它的信息记录在这个“工作信息表”里。于是,你创建一个“工作信息表的实例”,在C语言就是结构体变量,也就是我们上面代码的key_config
在这个例子中,工作信息表就是结构体(struct)的定义,它告诉你每种工具需要记录哪种信息。螺丝刀的信息卡片就是结构体变量(例子中是key_config),它包含了螺丝刀的具体信息,工具名称、工具用途和存放位置就是结构体中的成员(或字段),分别对应了螺丝刀的名称、用途和存放位置等信息。
那么回到我们的key_config变量里,它就类似一个专门为GPIO引脚填写的“信息卡片”。这个卡片上记录GPIO引脚的所有重要配置信息,比如它是输入还是输出、中断触发模式以及输出逻辑电平是高还是低等,这些信息都是通过修改key_config变量中的成员(或字段)来设置的
因此,结构体变量key_config
的作用就是存储和表示一个GPIO引脚的配置信息,而它与结构体中成员的关系就像是“信息卡片”与卡片上填写的具体内容之间的关系。
上面我们对于结构体进行了一个通俗的讲解,所以继续讲解我们剩下的代码,首先IOMUXC_SetPinMux是在复用GPIO1_IO18引脚,然后 IOMUXC_SetPinConfig是在配置引脚具体的电气属性这两步,都是在初始化IO口
#include "bsp_keyfilter.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_epittimer.h"
void filterkey_init(void)
{
gpio_pin_config_t key_config;
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18,&key_config);
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,(system_irq_handler_t)gpio1_16_31_irqhandler,NULL);
gpio_enableint(GPIO1,18); /*使能GPIO1_IO18的中断功能*/
filtertimer_init(66000000/100); /*初始化定时器,10ms*/
}
下一步就是将我们的IO初始化为中断,上面也讲解了结构体,我们的结构体如下,所以这里将GPIO输出方向设置为输入
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
gpio_interrupt_mode_t interruptMode; /* 中断方式 */
} gpio_pin_config_t;
typedef enum _gpio_pin_direction
{
kGPIO_DigitalInput = 0U, /* 输入 */
kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;
要将GPIO设置为中断,按键按下则触发中断,所以将中断触发类型设置为下降沿触发
typedef enum _gpio_interrupt_mode
{
kGPIO_NoIntmode = 0U, /* 无中断功能 */
kGPIO_IntLowLevel = 1U, /* 低电平触发 */
kGPIO_IntHighLevel = 2U, /* 高电平触发 */
kGPIO_IntRisingEdge = 3U, /* 上升沿触发 */
kGPIO_IntFallingEdge = 4U, /* 下降沿触发 */
kGPIO_IntRisingOrFallingEdge = 5U, /* 上升沿和下降沿都触发 */
} gpio_interrupt_mode_t;
这里我们将引脚设置为了输入,所以outputLogic
的值通常不会影响到引脚的行为,毕竟输入模式下的引脚是用来读取外部信号的,而不是输出信号
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,(system_irq_handler_t)gpio1_16_31_irqhandler,NULL);
gpio_enableint(GPIO1,18); /*使能GPIO1_IO18的中断功能*/
filtertimer_init(66000000/100); /*初始化定时器,10ms*/
接下来这部分就是使能GPIO中断,以及注册中断处理函数,按键引脚是GPIO1_IO18,对应的是如下中断号,所以使能此中断号,
GPIO1_Combined_16_31_IRQn = 99, /**< Combined interrupt indication for GPIO1 signals 16 - 31. */
GIC_EnableIRQ();也是编写好的初始化使能函数
FORCEDINLINE __STATIC_INLINE void GIC_EnableIRQ(IRQn_Type IRQn)
{
GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
gic->D_ISENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
}
下一步给指定的中断号注册中断服务函数
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,(system_irq_handler_t)gpio1_16_31_irqhandler,NULL);
编写的函数如下,这些前面都讲解过,而且了理解起来也不复杂,就不多赘述,主要目的就是注册GPIO1)_Combined_16_31_IRQn这个中断信号,然后待会儿编写它的中断处理函数的时候名字叫做,gpio1_16_31_irqhandler
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irqHandler = handler;
irqTable[irq].userParam = userParam;
}
使能对应的按键引脚GPIO1_18的中断功能,最后再初始化定时器,设置定时器的周期, filtertimer_init将在下面的代码中编写
/*初始化消抖定时器*/
void filtertimer_init(unsigned int value)
{
EPIT1->CR = 0; /*还是先将寄存器清零再配置*/
EPIT1->CR = (1 << 24 | 1 <<3 | 1<<2 | 1<<1);
EPIT1->LR = value; /*计数值*/
EPIT1->CMPR = 0; /*比较寄存器*/
GIC_EnableIRQ(EPIT1_IRQn);
system_register_irqhandler(EPIT1_IRQn,(system_irq_handler_t)filtertimer_irqhandler,NULL);
}
编写的定时器消抖函数和之前EPIT的初始化函数很相似,这里也可以直接使用之前EPIT初始化函数,中间的CR寄存器配置,因为设置为1分频,所以可以直接默认,所以这里就没有设置分频值
/*关闭定时器函数*/
void filtertimer_stop(void)
{
EPIT1-> &= ~(1<<0); /*关闭定时器*/
}
void filtertimer_start(unsigned int value)
{
EPIT1->CR &= ~(1<<0); /*先关闭定时器*/
EPIT1->LR = value; /*计数值*/
EPIT1->CR |= (1<<0); /*打开定时器*/
}
编写如下两个关闭和重启定时器的函数,重启定时器的时候第一段代码是关闭定时器也是为了严谨起见,其次就是需要重新加载计数值,然后再打开寄存器,接下来就是编写两个中断服务函数
首先第一个是定时器中断处理函数,定时一个静态的无符号字符变量state,并初始化为OFF。由于是静态,所以值在函数调用之间保持不变
下面的判断语句,检查EPIT1的状态寄存器SR是否被设置,也就是比较事件是否发生,若发生则进行下一步代码,也就是关闭定时器,因为事件已经发生自然就不需要定时器再运行,直到下一次的启动
然后下一个if判断,就是判断引脚GPIO1的18引脚的KEY0按键的状态。如果按键被按下就执行下面的蜂鸣器的状态反转,也就是使蜂鸣器工作,最后如果比较事件的判断没有成功,或者蜂鸣器反转完成,最后都需要清除中断标志位,准备下一次的中断事件
/*定时器中断处理服务函数*/
void filtertimer_irqhandler(void)
{
static unsigned char state = OFF;
if(EPIT1->SR & (1<<0)) /*判断比较事件是否发生*/
{
filtertimer_stop(); /*关闭定时器*/
if(gpio_pinread(GPIO1,18) == 0)
{
state =!state;
beep_switch(state);
}
}
EPIT1->SR |= 1<<0; /*清除中断标志位*/
}
也就是如果在按键按下的时候产生相关中断,中断产生后执行这个中断处理函数,这个函数的作用就是初始化定时器,然后设置周期过后开启定时器,也就是上面我们编写的那个函数,最后还是在完成这个中断后需要清除标志位,以用来准备下一次的中断产生
/*GPIO处理函数*/
void gpio1_16_31_irqhandler(void)
{
filtertimer_start(66000000/100); /*开启定时器*/
gpio_clearintflags(GPIO1,18);
}
到这里这完成了所有的有关蜂鸣器的操作,因为我们在实验目的当中还有一个就是需要LED灯一直以500ms的时间持续闪烁,所有最后我们还需要编写一下灯的相关代码,这个代码我们直接在main.c文件当中进行编写就可以了
灯的逻辑很简单,主函数中只需要引用相关的头文件,且将编写的好的外设进行初始化就可以了
#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_exit.h"
#include "bsp_epittimer.h"
#include "bsp_keyfilter.h"
int main(void)
{
unsigned char state =OFF;
int_init();
imx6u_clkinit();
clk_enable();
led_init();
beep_init();
filterkey_init();
while(1)
{
state = !state;
led_switch(LED0,state);
delay(500);
}
return 0;
}
最后修改一下Makefile的文件,然后将代码烧写到开发板中,就可以看到预期的实验现象了,这一讲主要就是对之前的定时器,以及代码的运用,这一节实验就讲解到这里,欢迎大家的指正与讨论