定时器是最常用的外设,常要用定时器完成精准的定时, I.MX6U 提供了多种硬件定时器。本篇记录了学习如何配置 EPIT 定时器,使其按照给定的时间,周期性的产生定时器中断,在定时器中断里面可以进行其他操作,比如:点灯,开关蜂鸣器等等。
EPIT 全称:Enhanced Periodic Interrupt Timer,增强型周期中断定时器,它主要用来完成周期性中断定时。在 STM32 里定时器还有其它功能,比如:输入捕获、 PWM 输出比较等。但 I.MX6U 的 EPIT 定时器只可用来完成周期性中断定时这一项功能!至于输入捕获、 PWM 输出等这些功能, I.MX6U 由GPT定时器外设来完成。I.MX6U对各个外设负责的功能划分的比较细致。
EPIT 是一个 32 位定时器,在CPU几乎不用介入的情况下提供精准的定时中断,软件使能后 EPIT 就会开始运行, EPIT 定时器有如下特点:
EPIT定时器框图
图中各部分的功能如下:
- ①处是个多路选择器,用来选择 EPIT 定时器的时钟源, EPIT 共有 3 个时钟源可选择:ipg_clk、 ipg_clk_32k 和 ipg_clk_highfreq。选择:ipg_clk=66Mhz
- ②处是一个 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定时器之后,计数寄存器会每个时钟减1,若和比较寄存器里面的值相等的话,就会触发中断。
EPIT 定时器两种工作模式: set-and-forget 和 free-running, EPITx_CR(x=1, 2)寄存器的 RLD 位来控制切换这两种工作模式,置 1 的时候EPIT 工作在set-and-forget 模式, EPITx_CR 寄存器的 RLD 位清0时 EPIT 工作在free-running模式,这两个工作模式区别如下:
set-and-forget 模式:此模式下 EPIT 的计数器从加载寄存器(EPITx_LR)中获取初始值,不能直接向计数寄存器(EPIT_CNR)写入数据。不管什么时候,只要计数器计数到 0,那么就会从加载寄存器 中重新加载数据到计数器中,周而复始。
free-running 模式:当计数器计数到0后会重新从0xFFFFFFFF开始计数,并不是从加载寄存器 EPITx_LR中获取数据。
EPIT重要寄存器结构:
6ULL有两个EPIT定时器,每个定时器有5个寄存器:EPITx_CR,EPITx_SR,EPITx_LR,EPITx_CMPR,EPITx_CNR。
1.EPIT 的配置寄存器 EPITx_CR
寄存器 EPITx_CR 我们用到的重要位如下:
2.寄存器 EPITx_SR
寄存器 EPITx_SR 只有bit0这一个位有效,OCIF(bit0),此位是比较中断标志位,为 0 时表示没有比较事件发生,为 1 的时候表示有比较事件发生。当比较中断发生后需手动清除此位,此位写 1 清零。
3.寄存器 EPITx_LR、 EPITx_CMPR 和 EPITx_CNR 分别为加载寄存器、比较寄存器和计数寄
存器,这三个寄存器都是用来存放数据的,没有什么特殊位操作。
本实验需要实现:使用 EPIT 产生定时中断, 然后在中断服务函数里面翻转 LED0。
配置好 EPIT 后,即可通过 EPIT 的比较中断来实现 LED0 的翻转。
实验要求:使用EPIT实现500ms周期定时中断,在EPIT中断服务函数里开关LED灯。首先在 bsp 文件夹下创建 “epittimer” 的文件夹,然后在 bsp/epittimer 中新建 bsp_epittimer.c 和 bsp_epittimer.h 两个文件。
#ifndef _BSP_EPITTIMER_H
#define _BSP_EPITTIMER_H
#include "imx6ul.h"
/* 函数声明 */
void epit1_init(unsigned int frac, unsigned int value);
void epit1_irqhandler(void);
#endif
bsp_epittimer.c 里有两个函数 epit1_init 和 epit1_irqhandler,分别是 EPIT1 初始化函数和
EPIT1 中断处理函数。
函数epit1_init 有两个参数 frac 和 value,frac是分频值, value是加载值。EPIT1->CMPR = 0,此句代码设置比较寄存器为 0,也就是当计数器倒计数到 0 会触发比较中断,因此分频值 frac 和加载值 value 就可以决定中断频率,计算公式如下:
Tout = ((frac +1 )* value) / Tclk;
Tclk: EPIT1 的输入时钟频率(单位 Hz)。 Tout: EPIT1 的溢出时间(单位 S)。
EPIT1->CR = (1<<24 | frac << 4 | 1<<3 | 1<<2 | 1<<1)此句代码设置了EPIT的时钟源为ipg_clk=66MHz,frac 分频值,工作模式为 set-and-forget,比较中断使能,设置计数器初始值。
假如要设置 EPIT1 中断周期为 500ms,可以设置分频值为 0,也就是 1 分频,这样进入 EPIT1的时钟就是 66MHz(66MHz的意思就是1s计数66000000次)。若要实现 500ms 的中断周期,EPIT1 的加载寄存器就应该为66000000/2=33000000。也就是value=33000000。
函数 epit1_irqhandler 是 EPIT1 的中断处理函数,此函数先读取 EPIT1_SR 寄存器,判断当前的中断是否为比较事件,如果是的话就翻转 LED 灯。最后在退出中断处理函数时需要清除中断标志位。
#include "bsp_epittimer.h"
#include "bsp_int.h"
#include "bsp_led.h"
/*
* @description : 初始化 EPIT 定时器.
* EPIT 定时器是 32 位向下计数器,时钟源使用 ipg=66Mhz
* @param – frac : 分频值,范围为 0~4095,分别对应 1~4096 分频。
* @param - value : 倒计数值。
*/
void epit1_init(unsigned int frac, unsigned int value)
{
if(frac > 0XFFF) //0xFFF=4095,将分频值限制在4095,不能设置超了
frac = 0XFFF;
EPIT1->CR = 0; /* 先清零 CR 寄存器 */
/*
* CR 寄存器:
* bit25:24 01 时钟源选择 Peripheral clock=66MHz
* bit15:4 frac 分频值
* bit3: 1 当计数器到 0 的话从 LR 重新加载数值
* bit2: 1 比较中断使能
* bit1: 1 初始计数值来源于 LR 寄存器值
* bit0: 0 先关闭 EPIT1
*/
EPIT1->CR = (1<<24 | frac << 4 | 1<<3 | 1<<2 | 1<<1);
EPIT1->LR = value; /* 加载寄存器值 */
EPIT1->CMPR = 0; /* 比较寄存器值设置为0,value值计数到0时引发中断 */
/* 使能 GIC 中对应的中断 */
GIC_EnableIRQ(EPIT1_IRQn);
/* 注册中断服务函数 */
system_register_irqhandler(EPIT1_IRQn,(system_irq_handler_t)epit1_irqhandler,NULL);
EPIT1->CR |= 1<<0; /* 使能 EPIT1 */
}
//EPIT中断处理函数
void epit1_irqhandler(void)
{
static unsigned char state = 0;
state = !state;
if(EPIT1->SR & (1<<0)) /* 判断比较事件发生 */
{
led_switch(LED0, state); /* 定时器周期到,反转 LED */
}
EPIT1->SR |= 1<<0; /* 清除中断标志位 */
}
调用函数 epit1_init 来初始化 EPIT1,分频值为 0,也就是 1 分频,加载寄存器值为 66000000/2=33000000, EPIT1 定时器中断周期为 500ms。
#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_epittimer.h"
int main(void)
{
int_init(); /* 初始化中断(一定要最先调用!) */
imx6u_clkinit(); /* 初始化系统时钟 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化 led */
beep_init(); /* 初始化 beep */
key_init(); /* 初始化 key */
epit1_init(0, 66000000/2); /* 初始化 EPIT1 定时器, 1 分频
//计数值为:66000000/2,也就是定时周期为 500ms。
while(1)
{
}
return 0;
}
用到按键就要处理因机械结构带来的按键抖动问题,也就是按键消抖,之前直接使用延时函数实现消抖效果,但直接用延时函数来实现消抖会浪费 CPU 性能,还有上面中断中使用延时,都是禁止使用的。而使用定时器既可以实现按键消抖,而且也不会浪费CPU 性能,这也是 Linux 驱动里面按键消抖的做法。
按键消抖的原理其实就是在按键按下后延时一段时间再去读取按键值,若此时按键值还有效那就表示这是一次有效的按键,中间的延时就是消抖的。但延时函数会浪费 CPU 性能,因为延时函数就是空跑。如果按键是用中断方式实现的,就更不能在中断服务函数里面使用延时函数,因为中断服务函数最基本的要求就是快进快出!
本部分利用定时器实现按键消抖:利用上文学习到的 EPIT 定时器,定时器设置好定时时间,CPU 就可以去做其他事,定时时间到了以后就会触发中断,然后在中断中做相应的处理即可。我们可借助定时器实现消抖,按键采用中断驱动方式,当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为 10ms,当定时时间到之后触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。定时器按键消抖如图
上图中,t1~t3 这一段时间就是按键抖动,是需要消除的。设置按键为下降沿触发,因此会在 t1、 t2 和 t3 这三个时刻会触发按键中断,每次进入按键中断处理函数中都会重新设置定时器中断,所以在 t1、 t2 和 t3 这三个时刻都会重新设置定时器中断。但 t1~t2 和 t2~t3 这两个时间段是小于我们设置的定时器中断周期(也就是消抖时间,比如设置为10ms),所以虽然 t1 开启了定时器,但定时器定时时间还没到10ms,t2 时刻就重置了定时器,只有 t3 时刻开启的定时器能完整的完成设置好的整个定时周期并触发定时中断,此时我们在定时器中断处理函数里面再做按键处理,这就是定时器实现按键消抖原理,Linux 里面的按键驱动用的就是此原理!接下来即为:使用 EPIT1 配合按键 KEY来实现消抖具体的配置步骤。
本实验现象:按下 KEY 会打开蜂鸣器,再按下 KEY 会关闭蜂鸣器。LED0 作为不断闪烁。
在 bsp 文件夹下创建名为“keyfilter”的文件夹,然后在 bsp/keyfilter 中新建 bsp_keyfilter.c 和 bsp_keyfilter.h 两个文件。
#ifndef _BSP_KEYFILTER_H
#define _BSP_KEYFILTER_H
/* 函数声明 */
void filterkey_init(void);
void filtertimer_init(unsigned int value);
void filtertimer_stop(void);
void filtertimer_restart(unsigned int value);
void filtertimer_irqhandler(void);
void gpio1_16_31_irqhandler(void);
#endif
bsp_keyfilter.c 一共有 6 个函数:
filterkey_init 是本试验的初始化函数,此函数首先初始化了 KEY 所使用的 UART1_CTS 这个 IO,设置这个 IO 的中断模式,并且注册中断处理函数,最后调用函数 filtertimer_init 初始化定时器 EPIT1 定时周期为10ms。
函数 filtertimer_init 是定时器 EPIT1 的初始化函数,内容基本和上文 EPIT1 初始化函数一样。
函数 filtertimer_stop 和 filtertimer_restart 分别是 EPIT1 定时器的关闭和重启函数。
filtertimer_irqhandler 是 EPTI1 的中断处理函数,此函数里面就是按键要做的工作,在本例程里
面就是开闭蜂鸣器。
函数 gpio1_16_31_irqhandler 是 GPIO1_IO18 的中断处理函数,此函数工作为重启定时器 EPIT1。
#include "bsp_key.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_beep.h"
#include "bsp_keyfilter.h"
void filterkey_init(void)
{
gpio_pin_config_t key_config;
/* 1、初始化 IO */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
/* 2、初始化 GPIO 为中断 */
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18, &key_config);
/* 3、 使能 GPIO 中断,并且注册中断处理函数 */
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, 1s=1000ms
}
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; /* 比较寄存器为 0 */
/* 使能 EPIT1 中断并注册中断处理函数*/
GIC_EnableIRQ(EPIT1_IRQn);
system_register_irqhandler(EPIT1_IRQn,
(system_irq_handler_t)filtertimer_irqhandler, NULL);
}
void filtertimer_stop(void)
{
EPIT1->CR &= ~(1<<0); /* 关闭定时器 */
}
void filtertimer_restart(unsigned int value)
{
EPIT1->CR &= ~(1<<0); /* 先关闭定时器 */
EPIT1->LR = value; /* 设置计数值 */
EPIT1->CR |= (1<<0); /* 打开定时器 */
}
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; /* 清除中断标志位 */
}
void gpio1_16_31_irqhandler(void)
{
filtertimer_restart(66000000/100); /* 开启,重设定时器 */
gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}
main.c 文件只有一个 main 函数,调用函数 filterkey_init 来初始化带有消抖的按键,在 while 循环里面翻转 LED0,周期大约为 500ms。
#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_keyfilter.h"
int main(void)
{
unsigned char state = OFF;
int_init(); /* 初始化中断(一定要最先调用! ) */
imx6u_clkinit(); /* 初始化系统时钟 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化 led */
beep_init(); /* 初始化 beep */
filterkey_init(); /* 带有消抖功能的按键 */
while(1)
{
state = !state;
led_switch(LED0, state);
delay(500);
}
return 0;
}
主函数循环翻转 LED0,周期约为 500ms,按键按下会触发中断,中断里开闭蜂鸣器,实验现象与EPIT定时器中断实验相同,只是更换了按键消抖方式。