前言
首先我们在使用开发板进行开发时,自然而然会使用到定时器这个外设,因为我们需要它来完成精准的定时功能,但是说到精准,我会在下一篇文章中使用其他的定时器来完成这个功能即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
不断的闪烁