我们回顾下中断产生前后的处理流程:详见异常、中断的原理与流程
中断前:
中断产生后:
问题案例:
我们想实现一个按键点灯程序,我们知道有以下两种方案:
1.轮询方案:轮询检测按键的电平状态,当检测到被按下后,对应的gpio会拉低,点亮对应的led;(略)
2.中断方案:将按键配置成外部中断源,当有按键按下,触发中断,在中断服务程序(isr)中去完成点灯。
下面开始写代码:
一.中断初始化
1)中断源设置
我们用按键作为外部中断源,我们把按键对应的gpio配置成中断引脚,当按键按下,相应的gpio产生了电平跳变,就会触发外部中断。
我们想达到按下按键灯亮,松开按键灯灭这种效果(配成双边沿触发,按下的时候产生下降沿中断,进行点亮,松开产生上升沿中断,进行熄灭)。当然也可做成按一下点亮,再按一下熄灭的效果(设成单边沿触发,每来一次中断,对led电平进行一次取反)。
查看原理图如下:
我们从按键的原理图中得知,当按键没有按下时,接上拉电阻,那么按键为高电平状态。当按键按下时,电位被拉低,按键处于低电平状态。s2-s5分别对应GPF0,GPF2,GPG3,GPG11; D10-D12这3盏led所对应的gpio分别是GPF4,GPF5,GPF6.
那么我们让s2,s3,s4分别控制D10,D11,D12;s5对D10-D12同时控制(按下s5,同时点亮3个led)。
我们需要配置D10-D12的gpio为输出模式,s2-s4的gpio为外部中断模式。
打开芯片手册找到第九章 IO ports,找到对应的gpio控制寄存器,将对应的gpio配置成中断模式。
代码如下:
-
配置GPIO为中断引脚:
GPG的一样,我就不放图片了。GPFCON &= ~((3<<0) | (3<<4)); //先把eint0和eint2这两个引脚清零 GPFCON |= ((2<<0) | (2<<4)); //S2,S3被配置为中断引脚 GPGCON &= ~((3<<6) | (3<<22)); GPGCON |= ((2<<6) | (2<<22)); //S4,S5被配置为中断引脚
-
设置中断触发方式:
当电平从高变低时,此时表示按键按下,当电平由低变高,表示松开按键。不妨设置中断方式为双边沿触发,按下按键,触发下降沿中断,中断服务程序就可以去点亮led,反之,松开触发上升沿中断,就可以去熄灭led。
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
3.设置外部中断屏蔽寄存器EINTMASK:
从上图我们知道外部中断0-3是直接连接到中断控制器,而外部中断4-7、外部中断8-23还要经过EINTMASK,那么我们需要配置EINTMASK来打开中断的通道:
EINTMASK &= ~((1<<11) | (1<<19)); //打开外部中断通道
4.外部中断挂起寄存器EINTPEND:
当一个外部中断(EINT4-EINT23)发生后,那么相应的位会被置1, 所以中断结束后需要清除对应位。这个寄存器可以用来区分外部中断4-23的哪一个中断源。
2)中断控制器设置
我们先来看下中断控制器的总框图:
1.首先是SRCPND:用来表示哪个中断源发出了中断请求。
我们先看下中断源:
从上图我们发现外部中断有24个外部中断,除了外部中断EINT,还有定时器中断,ADC中断,UART中断等…。
我们来认识下SRCPND寄存器:(用来表示哪个(哪些)中断源已产生中断请求,中断结束后要清中断)
从上图中我们发现EINT4-7共用1bit,EINT8-23共用1bit,那么肯定有其他寄存器来区分它们,那就是EINTPEND寄存器(后面5会讲)。
2.然后到达INTMSK:(中断屏蔽寄存器)
我们需要把INTMSK寄存器配置成非屏蔽状态,默认是中断源时屏蔽的,见下图:
3.INTMOD(中断模式,是fiq还是irq)
4.Priroty:
5.INTPND:
INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位。
中断发生后,SRCPND中会有bit置1,可能好几个(因为同时可能发生几个中断),这些中断会由优先级仲裁器选出一个最紧迫的,然后把INTPND中相应位置1。所以只有INTPND置1,CPU才会处理。
我们知道有可能同时出现多个中断请求,那么INTPND就挑选出当前优先级最高的、正在发生的中断。
当产生irq后,要去分辨是哪个中断源,根据不同的中断源去中断服务程序isr中做不同的事情,那么如何得知当前产生的中断是哪一个外部中断源产生的呢?那么就可以访问这个INTPND寄存器。
可是我们要去手工去解析INTPND里面的位,才能知道是哪个中断源产生了中断请求。那么有没有什么比较快捷的方式自动帮我们解析INTPND呢,直接返回中断号给我们?
当然有啦,有一个INTOFFSET寄存器的值就是代表哪个中断请求产生了,如果INTOFFSET=0表示EINT0产生了中断请求,INTOFFSET=2表示EINT2产生了中断请求。具体见下图:
我们从上图看到ENIT4-7共用一个offset, EINT8-23也共用一个offset,那么要通过访问EINTPEND寄存器来区分它们。
中断控制器设置代码入下:
/* 初始化中断控制器 */
void interrupt_init(void)
{
//1是屏蔽我们需要清零,外部中断0 外部中断2 外部中8_23里面还有外部中断11到19
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
//INTMOD默认是irq,可以不设置
}
3)中断总开关
CPSR有I位,是irq的总开关,我们需要把CPSR寄存器 bit7给清零,这是中断的总开关,如果bit7设置为1,CPU无法响应任何中断。
/* 把bit7这一位清零 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0
二. 中断服务程序设计
到这里中断前的初始化工作知识点就已经讲完了,当然要提前准备好led初始化工作(就是将led对应的gpio配置成输出模式,这个不讲解)。
那么中断产生后,我们之前讲过,会跳转到0x18异常向量,执行跳转指令ldr pc, =_irq
,和之前的swi异常,und异常框架一样。
代码框架如下:
展开代码
.text .global _start _start: b reset /* vector 0 : reset */ ldr pc, und_addr /* vector 4 : und(绝对跳转) */ ldr pc, swi_addr /* vector 8 : swi */ b halt /* vector 0x0c : prefetch aboot */ b halt /* vector 0x10 : data abort */ b halt /* vector 0x14 : reserved */ ldr pc, irq_addr /* vector 0x18 : irq */ b halt /* vector 0x1c : fiq */ und_addr: .word do_und swi_addr: .word do_swi irq_addr: .word do_irq reset: /* 关闭看门狗 */ /*初始化时钟*/ /*初始化sdram,设置栈*/ /*代码重定位,清bss*/ /* 把bit7这一位清零(打开中断总开关) */ bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */ msr cpsr, r0 ldr pc, =main /* 绝对跳转, 跳到SDRAM */ halt: b halt
1.我们在start.s中用汇编代码设置cpsr的I位,开启中断开关;
2.在main函数中初始化中断源key_eint_init,初始化中断控制器interrupt_init;
3.然后继续执行main主函数。
4.当中断产生,触发irq异常,进入0x18异常向量,执行do_irq。
do_irq实现如下(和之前的do_und, do_swi类似):
展开代码
do_irq: /* 执行到这里之前: */ /* 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址 */ /* 2. SPSR_irq保存有被中断模式的CPSR */ /* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式 */ /* 4. 跳到0x18的地方执行程序 */ /* sp_irq未设置, 先设置它 */ ldr sp, =0x33d00000 /* 保存现场 */ /* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */ /* lr-4是异常处理完后的返回地址, 也要保存 */ sub lr, lr, #4 stmdb sp!, {r0-r12, lr} /* 处理irq异常 */ bl handle_irq_c /* 恢复现场 */ ldmia sp!, {r0-r12, pc}^ /* ^会把spsr_irq的值恢复到cpsr里 */
handle_irq_c函数实现如下:
void key_eint_irq(int irq)
{
unsigned int val = EINTPEND;
unsigned int val1 = GPFDAT;
unsigned int val2 = GPGDAT;
if (irq == 0) /* eint0 : s2 控制 D12 */
{
if (val1 & (1<<0)) /* s2 --> gpf6 */
{
/* 松开 */
GPFDAT |= (1<<6);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<6);
}
}
else if (irq == 2) /* eint2 : s3 控制 D11 */
{
if (val1 & (1<<2)) /* s3 --> gpf5 */
{
/* 松开 */
GPFDAT |= (1<<5);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<5);
}
}
else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
{
if (val & (1<<11)) /* eint11 */
{
if (val2 & (1<<3)) /* s4 --> gpf4 */
{
/* 松开 */
GPFDAT |= (1<<4);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<4);
}
}
else if (val & (1<<19)) /* eint19 */
{
if (val2 & (1<<11))
{
/* 松开 */
/* 熄灭所有LED */
GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
}
else
{
/* 按下: 点亮所有LED */
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
}
}
EINTPEND = val; /* 清中断 : 源头*/
}
/*INTOFFSET中哪一位被设置成1,就表示哪一个 中断源*/
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;
/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);/*清EINT0,EINT2,EINT5*/
INTPND = (1<<bit);
}
测试结果如下: