定时器中断
- 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 参考资料:开发版原理图,S3C2440A datasheet
- 源码仓库:https://gitee.com/d_1254436976/Embedded-Linux-Phase-1
目录
一、如何使用定时器中断
定时器工作原理——原理图分析
观察PWM定时器框图,可以看到:
1、S3C2440A有5个16位定时器————Timer0 ~ 4,其中Timer0 ~ 3都分别有输出管脚TOUT0~3,Timer4则没有。
2、时钟源为PCLK,Timer0 ~ 1和Timer2 ~ 4分别共用一个8位的预分频器,时钟信号通过8位预分频器后进入第二级分频器,输出5种频率(1/2,1/4,1/8,1/16,TCLK0)的时钟信号。
3、通过5:1多路复用器选择哪个频率的时钟信号进入控制逻辑单元(Control Logic0~4)进行其内部的工作。
4、通过内部控制逻辑单元的工作,Timer0 ~ 3输出的时钟信号经过后续的多路选择器的处理,通过TOUT0~3进行输出。
观察定时器内部控制逻辑图可知其大致工作流程:
1、分别设置TCMPBn与TCNTBn寄存器中的比较值和初始计数值。
2、设置TCONn寄存器启动定时器n,这时TCMPBn与TCNTBn寄存器中的值将会被加载到内部寄存器TCMPn和TCNTn中,其后TCNTn开始减1计数。
3、当TCMPn == TCNTn时,对应定时器Timern的输出管脚TOUTn反转,TCNTn继续减1计数。
4、当TCNTn == 0时,可以触发中断(如果中断使能的话),输出管脚TOUTn反转。
二、如何设置定时器中断
编程思路:通过控制Timer0定时器,使其在TCNTn == 0时触发中断,led灯进行计数
1、初始化时钟
1.1 查手册可知
Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
当 PCLK = 50000000, prescaler value = 99, divider value = 16
则 Timer input clock Frequency =50000000/(99+1)/16 = 31250
即 对于时钟而已,从0计数到31250为一秒
PCLK的值在star.S中已确定,prescaler value需通过设置TCFG0寄存器, divider value需通过设置TCFG1寄存器
1.2 设置TCFG0寄存器
由于使用到Timer0,
TCFG0 = 0x63;
1.3 设置TCFG1寄存器
TCFG1 &=~ 0xf; //先请零
TCFG1 |= 0x0003; //后赋值
2、设置初值
设置TCNTO0寄存器
由于只有在TCNTn == 0时,可以触发中断,所以不需要用到当TCMP0寄存器,只需设置TCNTO0寄存器
TCNTB0 = 15625; //0.5s中断一次
3、加载初值,启动Timer
设置TCON寄存器
TCON = (1<<1); //手动更新,Update TCNTB0 & TCMPB0
4、设置为自动加载
设置TCON寄存器
TCON &=~(1<<1); //关闭手动更新
TCON |= (1<<0)|(1<<3); //Timer0开始计数,启动自动加载
5、开启Timer0中断使能
设置INTMSK 寄存器
INTMSK &= ~(1<<10); /* enable timer0 int */
三、如何在代码中实现
1、新建一个timer.c文件
用来设置操作定时器相关的寄存器和编写中断函数
#include "s3c2440_soc.h"
void timer0_init(void)
{
/*
* 初始化时钟:Timer 0
* =PCLK / {prescaler value+1} / {divider value}
* 50000000/(99+1)/16=31250
*/
TCFG0 = 0x63; //设置prescaler value
TCFG1 &=~ 0xf;
TCFG1 |= 0x0003; //设置divider value
/* 设置初值*/
TCNTB0 = 15625; //0.5s中断一次
/* 加载初值 */
TCON = (1<<1); //手动更新,Update TCNTB0 & TCMPB0
/* 启动时钟,设置为自动加载模式 */
TCON &=~(1<<1);
TCON |= (1<<0)|(1<<3);
}
/* 中断相关函数 */
void timer_irq(void)
{
/* 点灯计数 */
static int cnt = 0;
int tmp;
cnt++;
tmp = ~cnt;
tmp &= 7;
GPFDAT &= ~(7<<4);
GPFDAT |= (tmp<<4);
}
2、在main函数中初始化timer0_init()函数
#include "s3c2440_soc.h"
#include "uart.h"
#include "sdram_init.h"
char g_Char = 'A';
char g_Char2 = 'a';
char i ='0';
int main(void)
{
unsigned char c;
int flag;
interrupt_init();
key_eint_int();
timer0_init();
uart0_init();
sdram_init();
puts("uart0 init success!\n\r''");
putchar(i);
while(1)
{
putchar(g_Char);
g_Char++;
delay(1000000);
putchar(g_Char2);
g_Char2++;
delay(1000000);
}
return 0;
}
3、在eint.c文件中添加Timer0中断使能和中断号执行函数
#include "s3c2440_soc.h"
/* EINTPEND,可通过读此寄存器知道EINT4-EINT23哪个发生中断,请中断时可往里面写1 */
/* SRCPND 用来显示哪个中断产生了, 需要清除对应位
* EINT0-BIT[0]
* EINT2-BIT[2]
* EINR8_23-BIT[5]
*/
/* INTMOD 用来设置发生中断模式(IRQ和FIQ)
* 0-IRQ,1-FIQ,默认为0
*/
/* INTMSK 用来屏蔽中断, 1-masked
* EINT0-BIT[0]
* EINT2-BIT[2]
* EINR8_23-BIT[5]
*/
/* 初始化中断控制器 */
void interrupt_init(void)
{
INTMSK &=~ ((1<<0)|(1<<2)|(1<<5));
INTMSK &= ~(1<<10); /* enable timer0 int */
}
/* INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位
* EINT0-BIT[0]
* EINT2-BIT[2]
* EINR8_23-BIT[5]
*/
/* INTOFFSET 用来显示INTPND中哪一位被设置为1
* 这个位可以通过清除SRCPND和INTPND来自动清除。
*/
/*
*初始化按键并将其设置为中断源
*S2:EINT0:GPF0 S3:EINT2:GPF2 S4:EINT11:GPG3 S5:EINT19:GPG11
*设置中断方式:双边触发
*/
void key_eint_int(void)
{
/*
*配置按键
*设置寄存器,先请零、后设置
*/
GPFCON &=~((3<<0)|(3<<4));
GPFCON |= ((2<<0)|(2<<4)); //设置S2、S3
GPGCON &=~((3<<6)|(3<<22));
GPGCON |= ((2<<22)|(2<<6)); //设置S4、S5
EXTINT0 |= ((7<<0)|(7<<8)); //设置 EINT0、EINT2
EXTINT1 |= (7<<12); //设置EINT11
EXTINT2 |= (7<<12); //设置EINT19
EINTMASK &=~ ((1<<11)|(1<<19)); //使能EINT11,19
/*
*设置LED灯
*配置GPF4,5,6为输出口
*先对寄存器CPFCON清0
*后再配置寄存器,是GPF4,5,6为输出口
*/
GPFCON &=~ ((3<<8)|(3<<10)|(3<<12));
GPFCON |= ((1<<8)|(1<<10)|(1<<12));
}
/* 中断处理函数
* 先分辨中断源,对不同的中断源进行处理
* 后清中断
*/
void key_irq_handle(int flag)
{
unsigned int val = EINTPEND;
unsigned int valf = GPFDAT;
unsigned int valg = GPGDAT;
if (flag == 0)
{
if (valf & (1<<0)) /* s2 --> gpf6 */
{
/* 松开 */
GPFDAT |= (1<<6);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<6);
}
}
else if (flag == 2)
{
if (valf & (1<<2)) /* s3 --> gpf5 */
{
/* 松开 */
GPFDAT |= (1<<5);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<5);
}
}
else if (flag == 5)
{
if (val & (1<<11)) /* eint11 */
{
if (valg & (1<<3)) /* s4 --> gpf4 */
{
/* 松开 */
GPFDAT |= (1<<4);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<4);
}
}
else if (val & (1<<19)) /* eint19 */
{
if (valg & (1<<11))
{
/* 松开 */
/* 熄灭所有LED */
GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
}
else
{
/* 按下: 点亮所有LED */
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
}
}
EINTPEND = val;
}
void handle_irq_c(void)
{
int bit = INTOFFSET;
if (bit==0||bit==2||bit==5)
{
key_irq_handle(bit);
}
else if(bit == 10)
{
timer_irq();
}
SRCPND = (1<<bit);
INTPND = (1<<bit);
}
4、在Makefile文件中加上timer.o的编译指令
all: start.o led.o uart.o sdram_init.o main.o exception.o eint.o timer.o
arm-linux-ld -T sdram.lds $^ -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis
%.o : %.c
arm-linux-gcc -c -o $@ $<
%.o : %.S
arm-linux-gcc -c -o $@ $<
四、代码优化
4.1 为何优化
原因如下:
① 、普遍性
每一种中断都有对应的位,都需要设置INTMSK寄存器来使能对应的中断,如下图。
②、维护性
对于添加新的中断,都需要在eint.c文件中的handle_irq_c去判断INTOFFSET寄存器中的位来执行对应的中断函数,如果中断过多则if判断语句过多,对日后的代码阅读以及维护带来一定困扰。
4.2 如何优化
1、定义一个函数指针数组,用来存储相关信息
typedef void(*irq_fp)(int); //声明irq_fp为指向函数的指针类型,该函数没有返回整型值,有(int)参数//
irq_fp irq_handle[32];
2、编写一个注册函数,用来储存中断号和中断调用函数的入口地址以及实现中断使能。
void register_irq(int irq, irq_fp fp)
{
irq_handle[irq] = fp;
INTMSK &=~ (1<<irq);
}
3、每次添加新的中断时,只需要把INTMSK寄存器中对应的中断号以及编写的中断函数的入口地址传入注册函数中,执行中断函数只需要把函数指针数组中的地址读出即可。
4.3 实例
①、注册本篇中定时器中断以及012篇中的按键中断
/*
* 0为EINT0在INTMSK寄存器中的中断号, key_irq_handle为编写的中断函数
* 2为EINT2在INTMSK寄存器中的中断号, key_irq_handle为编写的中断函数
* 5为EINT11、19在INTMSK寄存器中的中断号, key_irq_handle为编写的中断函数
* 10为timer0在INTMSK寄存器中的中断号,timer_irq为编写的中断函数
*/
register_irq(10, timer_irq);
register_irq(0, key_irq_handle);
register_irq(2, key_irq_handle);
register_irq(5, key_irq_handle);
②、执行timer_irq函数,只需在eint.c文件中的handle_irq_c函数中添加如下代码,不需中断号
irq_handle[bit](bit);
4.4 完整代码
修改后timer.c文件:
#include "s3c2440_soc.h"
/* 中断相关函数 */
void timer_irq(void)
{
/* 点灯计数 */
static int cnt = 0;
int tmp;
cnt++;
tmp = ~cnt;
tmp &= 7;
GPFDAT &= ~(7<<4);
GPFDAT |= (tmp<<4);
}
void timer0_init(void)
{
/*
* 初始化时钟:Timer 0
* =PCLK / {prescaler value+1} / {divider value}
* 50000000/(99+1)/16=31250
*/
TCFG0 = 0x63; //设置prescaler value
TCFG1 &=~ 0xf;
TCFG1 |= 0x0003; //设置divider value
/* 设置初值*/
TCNTB0 = 15625; //0.5s中断一次
/* 加载初值,启动时钟 */
TCON = (1<<1); //手动更新,Update TCNTB0 & TCMPB0
/* 设置为自动加载模式 */
TCON &=~(1<<1);
TCON |= (1<<0)|(1<<3);
register_irq(10, timer_irq);
}
修改后eint.c文件:
#include "s3c2440_soc.h"
typedef void(*irq_fp)(int); //声明irq_fp为指向函数的指针类型,该函数没有返回整型值,有(int)参数//
irq_fp irq_handle[32];
/* EINTPEND,可通过读此寄存器知道EINT4-EINT23哪个发生中断,请中断时可往里面写1 */
/* SRCPND 用来显示哪个中断产生了, 需要清除对应位
* EINT0-BIT[0]
* EINT2-BIT[2]
* EINR8_23-BIT[5]
*/
/* INTMOD 用来设置发生中断模式(IRQ和FIQ)
* 0-IRQ,1-FIQ,默认为0
*/
/* INTMSK 用来屏蔽中断, 1-masked
* EINT0-BIT[0]
* EINT2-BIT[2]
* EINR8_23-BIT[5]
*/
/* 初始化中断控制器 */
// void interrupt_init(void)
// {
// INTMSK &=~ ((1<<0)|(1<<2)|(1<<5));
// INTMSK &= ~(1<<10); /* enable timer0 int */
// }
/* INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位
* EINT0-BIT[0]
* EINT2-BIT[2]
* EINR8_23-BIT[5]
*/
/* INTOFFSET 用来显示INTPND中哪一位被设置为1
* 这个位可以通过清除SRCPND和INTPND来自动清除。
*/
/* 创立一个注册函数,用来储存中断号和中断调用函数 */
void register_irq(int irq, irq_fp fp)
{
irq_handle[irq] = fp;
INTMSK &=~ (1<<irq);
}
/* 中断处理函数
* 先分辨中断源,对不同的中断源进行处理
* 后清中断
*/
void key_irq_handle(int flag)
{
unsigned int val = EINTPEND;
unsigned int valf = GPFDAT;
unsigned int valg = GPGDAT;
if (flag == 0)
{
if (valf & (1<<0)) /* s2 --> gpf6 */
{
/* 松开 */
GPFDAT |= (1<<6);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<6);
}
}
else if (flag == 2)
{
if (valf & (1<<2)) /* s3 --> gpf5 */
{
/* 松开 */
GPFDAT |= (1<<5);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<5);
}
}
else if (flag == 5)
{
if (val & (1<<11)) /* eint11 */
{
if (valg & (1<<3)) /* s4 --> gpf4 */
{
/* 松开 */
GPFDAT |= (1<<4);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<4);
}
}
else if (val & (1<<19)) /* eint19 */
{
if (valg & (1<<11))
{
/* 松开 */
/* 熄灭所有LED */
GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
}
else
{
/* 按下: 点亮所有LED */
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
}
}
EINTPEND = val;
}
/*
*初始化按键并将其设置为中断源
*S2:EINT0:GPF0 S3:EINT2:GPF2 S4:EINT11:GPG3 S5:EINT19:GPG11
*设置中断方式:双边触发
*/
void key_eint_int(void)
{
/*
*配置按键
*设置寄存器,先请零、后设置
*/
GPFCON &=~((3<<0)|(3<<4));
GPFCON |= ((2<<0)|(2<<4)); //设置S2、S3
GPGCON &=~((3<<6)|(3<<22));
GPGCON |= ((2<<22)|(2<<6)); //设置S4、S5
EXTINT0 |= ((7<<0)|(7<<8)); //设置 EINT0、EINT2
EXTINT1 |= (7<<12); //设置EINT11
EXTINT2 |= (7<<12); //设置EINT19
EINTMASK &=~ ((1<<11)|(1<<19)); //使能EINT11,19
/*
*设置LED灯
*配置GPF4,5,6为输出口
*先对寄存器CPFCON清0
*后再配置寄存器,是GPF4,5,6为输出口
*/
GPFCON &=~ ((3<<8)|(3<<10)|(3<<12));
GPFCON |= ((1<<8)|(1<<10)|(1<<12));
register_irq(0, key_irq_handle);
register_irq(2, key_irq_handle);
register_irq(5, key_irq_handle);
}
void handle_irq_c(void)
{
int bit = INTOFFSET;
irq_handle[bit](bit);
SRCPND = (1<<bit);
INTPND = (1<<bit);
}