12.ARM按键中断实例

目录

                  1.思路

2.例子

3.中断服务函数的改进

源代码





 1.思路

 步骤:

1.设置中断源,发出中断信号

2.设置中断控制器,能向CPU发出中断

3.设置CPU,CPSR的I位,打开IRQ中断总开关。

4.硬件进入中断处理函数之前会自动做一系列的工作

1).把下一条指令的地址 保存在Link Rigsiter(LR)寄存器(R14)里,注意此处的LR寄存器是异常模式下专属的LR寄存器,简称为LR_异常,其保存被中断模式下一条指令的地址(可能为PC+4,也有可能为PC+8,取决于不同的情况)。

2).把被中断模式下CPSR寄存器保存至当前模式下的SPSR_异常寄存器。

3).修改CPSR的模式位,进入异常模式(M4-M0),其位状态如下说明:

4).跳到中断向量表。

5.处理这个中断--->自己想做的工作

注意:在处理中断时,需要分辨中断源是哪一个,因为2440有很多中断源,看数据手册。

6.离开中断,CPU返回之前的模式需要做的工作

1).LR寄存器减去某个值,复制给PC(R15)=> PC = LR_异常  - offset,如下表

假如发生了swi异常,反返回的时候,可以把LR寄存器(R14_svc)的值赋值给PC,如果发生了IRQ中断,返回时可以把LR寄存器(R14_irq)的值减去4再赋值给PC、

2).恢复CPSR的值,从SPSR寄存器。CPSR = SPSR_异常

3).手动清除中断标识位


2.例子

例子:

1).设置cpsr寄存器的  I 位为(第七位)为0,打开IRQ总中断。

2).编写1个c程序,两个函数,一个是初始化中断控制器,另一个是外部按键的初始化

流程:

1.初始化中断:①中断源初始化:按键设置为中断引脚 ②初始化中断控制器 ③设置CPU的CPSR寄存器使能总中断

2.在没有中断发生时,主函数打印字符串AaBbCc.....,中断按钮按下时,产生中断,执行中断处理函数,其处理任务就是点亮led灯,执行完中断之后,返回主函数继续执行打印字符串任务。

编写第一个函数,初始化外部中断:

分几步:

①设置寄存器GPFCON,GPGCON,配置GPF0/2,GPG3/11为外部中断引脚,对应原理图的:S2,S3,S4,S5

②设置寄存器GPFCON,配置GPF4/5/6引脚为输出,用于控制LED灯,对应原理图的,D10,D11,D12

③设置EXTIN0,EXTIN1,EXTIN2,寄存器,设置中断引脚的触发方式为双边沿触发。

④设置EINTMASK寄存器,配置外部中断11和外部中断19中断使能,而外部中断0和2是默认使能的。

⑤可以通过查询EINTPEND寄存器查询某个中断是否发生,写相应位为1清除中断(可查询的位为:EINT4-EINT23)

 代码如下:

/*初始化按键中断*/
void key_eint_init(void)
{
	/*配置GPF0/2,GPG3/11,为中断引脚*/
	GPFCON &= ~((3<<0)| (3<<4));  //对位先清零
	GPFCON |=  ((2<<0)| (2<<4));	//设置[1:0]=0b10,[5:4]=0b10
	GPGCON &= ~((3<<6) | (3<<22));
	GPGCON |=  ((2<<6) | (2<<22));	//设置[7:6]=0b10,[23:22]=0b10
	/*配置GPF4/5/6 为输出引脚--->LED,对应D10,D11,D12*/
	GPFCON &= ~((3<<8) | (3<<10) 
| (3<<12));
	GPFCON |=  ((1<<8) | (1<<10) 
| (1<<12));
	
	/*设置中断的触发方式:双边沿触发*/

	EXTINT0 &= ~((7<<0) | (7<<8));
	EXTINT0 |=  ((7<<0) | (7<<8)); //设置[2:0]=0b111,[10:8]=0b111

	EXTINT1 &= ~(7<<12);
	EXTINT1 |=  (7<<12); //设置[14:12]=0b111

	EXTINT2 &= ~(7<<12);
	EXTINT2 |=  (7<<12);  //设置[14:12]=0b111

	/*设置EINTMASK使能外部中断11和19,外部中断0和2不用设置*/
	EINTMASK &= ~((1<<11) | (1<<19));
	

}

具体配置如下: 

  • 首先初始化按键设置为中断引脚:

查看原理图,配置外部中断(EINT0/2/11/19)为中断引脚,设置三个LED引脚为输出模式

其中中断引脚和LED的外部连接如下图所示:

外部中断0和2连接在:GPF0/2 ,外部中断11和19连接在:GPG3/11

LED的D10/11/12连接在:GPG4/5/6

  • 首先找到GPFCON(GPF配置寄存器)设置GPF0/2为外部中断引脚:

  • 设置GPGCON,配置GPG3/11为中断引脚:

  • 配置EXTINT0寄存器,设置中断的触发方式为:双边沿触发 

  • 配置EXTINT1寄存器,设置中断的触发方式为:双边沿触发

  • 配置EXTINT2寄存器,设置中断的触发方式为:双边沿触发

  • 配置EINTMASK寄存器,使能外部中断11和19

可以通过读取EINTPEND寄存器来读取某个中断是否发生,写相应的位为0

编写第二个函数,设置中断控制器

找到数据手册中断控制器的部分,有一张简图如下:

这里有几个重要寄存器和步骤:

①SRPEND用来提示哪个中断产生了,处理完中断之后,需要清除相应位。

②INTMOD默认设置,模式寄存器默认为IRQ中断。

设置INTMASK寄存器,不屏蔽外部中断0,外部中断2,以及外部中断8-23.

④暂不设置优先级.

⑤INTPEND用于显示当前优先级最高,正在发生的中断,需要清除相应位。

⑥INTOFFSET寄存器用于显示INTPEND中哪一位被设置为1,即通过判断其值,可以知道当前发生的中断。

代码如下:

/*设置中断控制器*/
void Interrupt_Init(void)
{
	/*设置中断屏蔽寄存器,不屏蔽EINT0/2/11/19*/
	INTMSK & =~((1<<0) | (1<<2) | (1<<5));


}

具体寄存器描述如下:

  • 读取SRPEND,用于显示某一个中断是否发生,读取完需要清除相应位。

需要关心的位为:0--->外部中断0 . 外部中断2 , 5 ,外部中断8-23

  • INTMOD寄存器,使用默认值 

  • 设置INTMASK寄存器,不屏蔽我们的中断源

  • INTPEND 用于显示优先级最高,且正在发生的中断

  • INTOFFSET,用来显示INTPEND中哪个中断正在等待处理,显示INPEND寄存器哪一个位设置为1

比如,读这个寄存器的值是0,代表中断0发生了,值为2,代表中断2发生了


3).在主函数中调用这两个函数,初始化中断

4).如果此时没有发生中断,就一直主函数执行打印函数,当发生中断时,硬件自动做一系列的动作(详情看上面),最终跳到IRQ异常向量地址的位置开始执行,即为:0x18的地方,所以需要在启动文件0x18的地方放一条跳转指令,接下来处理步骤就和前边的一样了,设置栈,保存现场,处理IRQ异常,恢复现场,回到中断之前的模式继续执行函数。

详情查看:点我查看

这里需要注意一点:

退出中断返回时,需要把R14_irq的值减去4,再赋值给PC

启动文件start.S中重要部分代码如下:

.text
.global _start

_start:
	b Reset					/*vector 0 : Reset*/
	ldr pc, und_handle_addr	/*vector 4 : undfined execption*/
	ldr pc, swi_handle_addr /*vector 8 : software interrupt execption*/
	b halt					/*vector 0x0c*/
	b halt					/*vector 0x10*/
	b halt					/*vector 0x14*/
	ldr pc,irq_handle_addr	/*vector 0x18:IRQ execption*/
	b halt					/*vector 0x1c*/
und_handle_addr:
	.word und_handle
swi_handle_addr:
	.word swi_handle
irq_handle_addr:
	.word irq_handle

irq_handle:
	/*执行到这之前硬件做的工作
	 *1.lr_irq保存有被中断模式中下一条即将执行指令的地址
	 *2.SPSR_irq保存有被中断模式的CPSR
	 *3.CPSR中的M4-M0被设置为10010,进入IRQ模式
	 *4.跳到0x18地方开始执行
	 */

	/*设置IRQ异常模式下的栈*/
	ldr sp, =0x30b00000

	/*保存现场*/
	/*1.保存r0-r12,因为这几个寄存器是和swi异常处于管理模式共用的
	 *在这个异常模式有可能会修改寄存器的值,先保存
	 *起来,等退出这个异常的时候再恢复回去
	 *2.在手册中可知,在swi异常返回时,直接把
	 *lr寄存器赋值减去4赋值给pc,有偏移,
	 */
	sub lr, l r, #4    /*lr 的值减去4*/
	stmdb sp!, {r0-r12, lr}
	
	/*处理IRQ异常*/
	mrs r0, cpsr
	ldr r1, =irq_string
	bl Execption_print

    bl IRQ_handle
	
	/*恢复现场*/
	/*1.恢复r0-r12的值,然后把lr寄存器赋值给pc,使程序
	 *返回到上一次异常的位置接着往下执行。
	 *2. ‘^’会把spsr的值恢复到cpsr寄存器
	 */
	ldmia sp!, {r0-r12,pc}^

irq_string:
	.string "IRQ  exception!"

/*此处注意:因为上面的字符串有可能不是四字节的倍数
 *所以此处需要加上一个四字节对齐,让Reset从四字节
 *对齐的地方开始运行。
 */
.align 4

备注:

为了方便查看结果,当发生中断时还会打印出当前的cpsr寄存器的值,以及打印出字符串IRQ  exception

5)在interrupt.c中编写中断服务函数:IRQ_handle

/*中断服务函数*/
void IRQ_handle(void)
{ 
	int eint_bit = INTOFFSET;  //读取INTOFFSET寄存器的值
	/*分辨中断源:使用INTOFFSET寄存器判断发生哪个中断*/
	/*外部中断0,或者外部中断2,或者外部中断8-23发生*/
	if(eint_bit == 0 || eint_bit==2 || eint_bit==5)   
	{
		/*调用按键中断处理函数,清除中断*/
		key_eint_irq(eint_bit);
		
	}
	
	/*清除中断标志位,
	 *1.首先清除具体哪一个中断,在上面执行了
	 *2.清除SRCPEND
	 *3.清除INTPEND
	 */
	 SRCPND = (1<<eint_bit);  //写1清除
	 INTPND = (1<<eint_bit);  //写1清除

}

 解析:

在start.S的启动文件中编写,当发生IRQ异常的时候先保存现场,接着调用IRQ_handle()函数,也就是如上的函数,读取INTOFFSET寄存器,通过判断这个寄存器的值可以知道当前发生了哪个中断,如果有我们所关注的中断就调用按键中断处理函数key_eint_irq(eint_bit),这个函数在下面,处理完了当然是清除中断标志位了,这是一个硬性的规定。

这个函数的原型如下,总共有四个按钮,全部设置为外部中断,eint0控制D12,eint2控制D11,eint11控制D10,eint19控制全部的灯,当有按键按下时灯就亮,否则等灭。

void key_eint_irq(int irq)
{
	unsigned int val = EINTPEND;
	unsigned int val1 = GPFDAT;
	unsigned int val2 = GPGDAT;
	if(irq == 0) //外部中断0发生了,控制D12
	{
		if(val1 &(1<<0))  /*对应GPF0*/
		{
		 	/*松开*/
			GPFDAT |= (1<<6);
		}
		else
		{
			/*按下*/
			GPFDAT &= ~(1<<6);
		}
		
	}
	else if(irq == 2)//外部中断2发生了,控制D11
	{
		if(val1 &(1<<2))  /*对应GPF0*/
		{
		 	/*松开*/
			GPFDAT |= (1<<5);
		}
		else
		{
			/*按下*/
			GPFDAT &= ~(1<<5);
		}

	}
	else if(irq == 5)//外部中断8 -23其中一个发生,eint11-控制D10,eint19控制所有的灯
	{
		/*表明发生了外部中断8-23的其中一个,需要区分*/
		/*读取EINTPEND寄存器
		 *EINTPEND的位11为1,说明产生的是外部中断11
		 *EINTPEND的位19为1,说明产生的是外部中断19
		 */
		if(val & (1<<11))  /*外部中断11*/
		{
			if(val2 &(1<<3))  /*对应GPG3*/
			{
		 		/*松开*/
				GPFDAT |= (1<<4);
			}
			else
			{
				/*按下*/
				GPFDAT &= ~(1<<4);
			}

		}
		else if(val & (1<<19)) /*外部中断19*/
		{
			if(val2 & (1<<11))  /*对应GPG11*/
			{
		 		/*松开*/
				GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
			}
			else
			{
				/*按下*/
				GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
			}
			
		}
		

	}
	/*清除中断标志位*/
	EINTPEND = val; //写相应位清除
}

解析: 

通过上一个函数传进来的参数,也就是INTOFFSET寄存器的值就可以知道发生的是哪一个中断,根据不同的中断源做不同的处理,中断引脚连接的是按键,一个中断源控制一个灯,按下灯亮,松开熄灭,在最后还需要清除中断标志位。

  •  在主函数中调用中断初始化初始化函数:

6).修改Makefile,增加interrupt.c

all: start.o led.o uart.o sdram_init.o main.o execption.o interrupt.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

%.o : %.c
	arm-linux-gcc -c -o $@ $<

%.o : %.S
	arm-linux-gcc -c -o $@ $<
clean:
	rm *.bin *.o *.elf *.dis

6).编译,下载运行。

打开串口,刚开始程序正常运行,打印出AaBbCc,此时按下按键,s2,打印信息发生变化。

开发版的灯,也同样被开发版控制着。


3.中断服务函数的改进

步骤:

1.新建一个interrupt.h的文件:

根据INTMSK急寄存器的位做如下定义:

2.在interrupt.c中新建一个函数数组,用于保存注册的中断,他所保存的是已经注册的中断服务函数

3.提供一个中断服务注册函数:

备注:如果调用该函数去注册某一个中断的话,该中断就会保存在irq_array这个数组中。 

4.修改总中断处理函数:

4.在初始化按键中断中注册相应的中断类型

即:在irq_func irq_array[32];数组中,的第0、2、5的位置保存的是key_eint_irq函数的地址。

注意:此处无论是发生了0、2或5号中断都会执行key_eint_irq函数。


验证可行性:

在主函数中调用初始化按键中断函数:key_eint_init();

注意:现在可以不需要调用Interrupt_Init函数了

因为在注册中断时已经同时设置了。

下载之后,运行代码,结果和上面一样。 


源代码

IRQ外部中断:https://download.csdn.net/download/qq_36243942/10930909

IRQ代码改进:https://download.csdn.net/download/qq_36243942/11122869

发布了91 篇原创文章 · 获赞 247 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/qq_36243942/article/details/86470227