fuzidage
专注嵌入式、linux驱动 、arm裸机研究

导航

 

我们回顾下中断产生前后的处理流程:详见异常、中断的原理与流程

中断前:

中断产生后:

问题案例:
我们想实现一个按键点灯程序,我们知道有以下两种方案:

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配置成中断模式。

代码如下:

  1. 配置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被配置为中断引脚
    
  2. 设置中断触发方式:

当电平从高变低时,此时表示按键按下,当电平由低变高,表示松开按键。不妨设置中断方式为双边沿触发,按下按键,触发下降沿中断,中断服务程序就可以去点亮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);	
}

测试结果如下:

posted on 2020-01-16 17:14  fuzidage  阅读(917)  评论(0编辑  收藏  举报