前言
首先我们在使用开发板进行开发时,自然而然会使用到定时器这个外设,因为我们需要它来完成精准的定时功能,但是说到精准,我会在下一篇文章中使用其他的定时器来完成这个功能即GPT定时器。在本文章中我们会利用定时器中断来解决按键消抖功能,并且解决上一讲GPIO中断中的问题。
整体文件结构

源码分析(保姆级讲解)
带有消抖功能的按键初始化部分
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);
filtertimer_init(66000000/100);
}
好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!
gpio_pin_config_t key_config;
声明了一个gpio_pin_config_t 类型,并且名称为key_config。那我们可以看下这个结构体中声明了什么?
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
gpio_interrupt_mode_t interruptMode; /* 中断方式 */
} gpio_pin_config_t;
其中声明了三个变量,分别是direction,outputLogic和interruptMode。
其中gpio_pin_direction_t 结构体如下所示:
typedef enum _gpio_pin_direction
{
kGPIO_DigitalInput = 0U, /* 输入 */
kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;
其中gpio_interrupt_mode_t 结构体如下所示:
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;
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);
初始化IO复用功能为用为GPIO1_IO18。
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
配置GPIO1_IO18的IO属性 ,主要可设置功能如下,具体配置根据具体使用情况而定:
*bit 16:0 HYS关闭
*bit [15:14]: 11 默认22K上拉
*bit [13]: 1 pull功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 000 关闭输出
*bit [0]: 0 低转换率
key_config.direction = kGPIO_DigitalInput;
设置GPIO1_IO18方向为输入。
key_config.interruptMode = kGPIO_IntFallingEdge;
设置GPIO1_IO18为下降沿触发。
key_config.outputLogic = 1;
设置GPIO1_IO18初始电平为1,即高电平。
gpio_init(GPIO1, 18, &key_config);
因为要产生GPIO中断,所以需要配置中断号等其他设置。
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
使能GIC中对应的中断
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,
(system_irq_handler_t)gpio1_16_31_irqhandler,
NULL);
注册中断服务函数,并且名称为gpio1_16_31_irqhandler,即产生GPIO中断后,会自动进入该中断服务函数。
其
gpio_enableint(GPIO1, 18);
使能GPIO1_IO18的中断功能
filtertimer_init(66000000/100);
初始化EPIT定时器,10ms
带有消抖功能的EPIT定时器初始化部分
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);
}
好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!
EPIT1->CR = 0;
//先清零
EPIT1->CR = (1<<24 | 1<<3 | 1<<2 | 1<<1);
该寄存器具体配置如下:

在讲为什么会这样配置该寄存器之前,我们先了解下EPIT定时器的工作原理。

由上图所示,我们分别说以下6点:
①、这是个多路选择器,用来选择 EPIT 定时器的时钟源,EPIT 共有 3 个时钟源可选择,ipg_clk、ipg_clk_32k 和 ipg_clk_highfreq。
②、这是一个 12 位的分频器,负责对时钟源进行分频,12 位对应的值是 0~ 4095,对应着1~4096 分频。
③、经过分频的时钟进入到 EPIT 内部,在 EPIT 内部有三个重要的寄存器:计数寄存器(EPIT_CNR)、加载寄存器(EPIT_LR)和比较寄存器(EPIT_CMPR),这三个寄存器都是 32 位的。
EPIT 是一个向下计数器,也就是说给它一个初值,它就会从这个给定的初值开始递减,直到减为 0,计数寄存器里面保存的就是当前的计数值。如果 EPIT 工作在 set-and-forget 模式下,当计数寄存器里面的值减少到 0,EPIT 就会重新从加载寄存器读取数值到计数寄存器里面,重新开始向下计数。比较寄存器里面保存的数值用于和计数寄存器里面的计数值比较,如果相等的话就会产生一个比较事件。
④、比较器。
⑤、EPIT 可以设置引脚输出,如果设置了的话就会通过指定的引脚输出信号。
⑥、产生比较中断,也就是定时中断。
EPIT 定时器有两种工作模式:set-and-forget 和 free-running,这两个工作模式的区别如下:
set-and-forget 模式:EPITx_CR(x=1,2)寄存器的 RLD 位置 1 的时候 EPIT 工作在此模式下,在此模式下 EPIT 的计数器从加载寄存器 EPITx_LR 中获取初始值,不能直接向计数器寄存器写入数据。不管什么时候,只要计数器计数到 0,那么就会从加载寄存器 EPITx_LR 中重新加载数据到计数器中,周而复始。
free-running 模式:EPITx_CR 寄存器的 RLD 位清零的时候 EPIT 工作在此模式下,当计数器计数到0以后会重新从0XFFFFFFFF开始计数,并不是从加载寄存器EPITx_LR中获取数据。
所以通过了解了上述功能后,我们来看下这行代码都做了些什么
EPIT1->CR = (1<<24 | 1<<3 | 1<<2 | 1<<1);
1<<1:当计数器在每次变为0之后,会从加载寄存器读取下一轮计数的初始值。1<<2:当为1时,代表使能比较中断,即当计数器的值现在和我们设定的比较值相等时,会触发定时器中断。1<<3:为 1 的时候工作在 set-and-forget 模式,即会从会从加载寄存器读取下一轮计数的初始值。1<<24:选择定时器的时钟源为Peripheral 时钟(ipg_clk),即66MHz。
EPIT1->LR = value;
EPIT1->LR 时加载寄存器。
EPIT1->CMPR = 0;
EPIT1->CMPR是比较值,意味着当加载寄存器从value减少到0之后,会触发定时器中断。
GIC_EnableIRQ(EPIT1_IRQn);
使能GIC中对应的中断
system_register_irqhandler(EPIT1_IRQn, (system_irq_handler_t)filtertimer_irqhandler, NULL);
注册中断服务函数filtertimer_irqhandler
gpio中断服务函数部分
void gpio1_16_31_irqhandler(void)
{
/* 开启定时器 */
filtertimer_restart(66000000/100);
/* 清除中断标志位 */
gpio_clearintflags(GPIO1, 18);
}
void filtertimer_restart(unsigned int value)
{
EPIT1->CR &= ~(1<<0); /* 先关闭定时器 */
EPIT1->LR = value; /* 计数值 */
EPIT1->CR |= (1<<0); /* 打开定时器 */
}
按键消抖其实就是在按键按下以后延时一段时间再去读取按键值,如果此时按键值还有效那就表示这是一次有效的按键,中间的延时就是消抖的。因为中断服务函数最基本的要求就是快进快出!
当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为 10ms,当定时时间到了以后就会触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。
那我们如果想让其10ms触发一次定时器中断,我们应该设置多大的value。
计算公式如下
Tout = ((frac +1 )* value) / Tclk;
其中:
Tclk:EPIT1 的输入时钟频率(单位 Hz)
Tout:EPIT1 的溢出时间(单位 S)。
frac:分频值,默认是0,代表是1分频
那么1ms = ((0+1)* 66000000/100)/66000000 = 1/100s=10ms
gpio_clearintflags(GPIO1, 18);
每次完成一次gpio中断响应后,我们需要手动清除中断标志位,方便下一次进入中断函数。
定时器中断服务函数部分
void filtertimer_irqhandler(void)
{
static unsigned char state = OFF;
if(EPIT1->SR & (1<<0)) /* 判断比较事件是否发生 */
{
filtertimer_stop(); /* 关闭定时器 */
if(gpio_pinread(GPIO1, 18) == 0) /* KEY0 */
{
state = !state;
beep_switch(state); /* 反转蜂鸣器 */
}
}
EPIT1->SR |= 1<<0; /* 清除中断标志位 */
}
if(EPIT1->SR & (1<<0)) /* 判断比较事件是否发生 */
{
filtertimer_stop(); /* 关闭定时器 */
if(gpio_pinread(GPIO1, 18) == 0) /* KEY0 */
{
state = !state;
beep_switch(state); /* 反转蜂鸣器 */
}
}
此函数先读取 EPIT1_SR 寄存器,判断当前的中断是否为比较事件,如果是的话,并且此时gpio状态还是低电平状态,则代表此时按键按下,即我们将蜂鸣器翻转即可。
EPIT1->SR |= 1<<0; /* 清除中断标志位 */
每次完成一次定时器中断响应后,我们需要手动清除中断标志位,方便下一次进入中断函数。
while循环部分
while(1)
{
state = !state;
led_switch(LED0, state);
delay(500);
}
每隔500ms,led灯亮灭。
最终编译验证
按下 KEY 就会控制蜂鸣器的开关,并且 LED0 不断的闪烁



















