ARM处理器之中断与异常

目录

一、概念引入与处理流程

二、CPU模式(Mode)、状态(State)与寄存器

2.1 模式

2.2 状态(State)

2.3 程序状态寄存器

三、Thumb指令集示例

四、und异常模式程序示例

五、swi异常模式程序示例

六、按键中断程序示例

6.1 设置中断源

6.2 设置中断控制器

6.3 示例

6.4 函数指针数组注册中断


一、概念引入与处理流程

CPU被中断的方式有指令不对、数据访问有问题、Reset信号等这称为异常,还有中断源:按键、定时器、网路数据等称为中断,中断处于一种异常体系,对于异常,首先需要我们保存现场(也就是保存相关的寄存器),然后调用对应的处理函数,对于中断来说首先还需要分辨中断源再处理对应的处理函数,最后恢复现场

总体过程如下,其中标粗的过程是硬件决定的,后面的过程是软件决定的

  • 初始化:a.设置中断源,让其可以产生中断;b.设置中断控制器(可屏蔽,可设置优先级);c.设置CPU总开关(使能中断)
  • 执行程序
  • 产生中断:如按下按键,中断源经过中断控制器,中断控制器向CPU发出请求,CPU处理中断
  • CPU每执行完一条指令,都会检查有无异常/中断产生
  • 发生有异常/中断产生,开始处理,对于不同的异常,跳去不同的地址执行程序
  • 这些地址上,只是一条跳转指令,跳去某个函数,对于这些地址是处于连续的,在硬件上存在异常向量表
  • 这些函数,首先会保存现场(各类寄存器),处理异常(中断属于一种异常):分辨中断源,再调用不用的函数,然后恢复现场

对于S3C2440的中断向量表如下

对于u-boot来说,一开始都会指定向量表的跳转指令,其中reset是0地址,undefine_instruction就是地址4,依次增加可以知道irq的地址是24即0x18的地方,当我们发生中断时,CPU跳到这个地址执行该指令,于是CPU就会跳去执行irq的代码:保护现场、首先分辨中断源,调用不同的处理函数、恢复现场

 

二、CPU模式(Mode)、状态(State)与寄存器

 

2.1 模式

对于S3C2440来说有7种模式,对于其他ARM有可能还包含其他模式

usr用户模式
sys系统模式,用于运行特权级的操作系统任务
Undefined(und)未定义指令模式
Supervisor(svc)管理模式
Abort(abt)中止模式(1.指令余秋雨中止 2.数据访问中止)
IRQ(irq)中断模式
FIQ(fiq)快中断模式(快速处理)

其中除了usr/sys模式外的模式都是异常模式,而异常模式加上sys模式属于特权模式(Privileged Mode),可以编程操作CPSR(当前程序状态寄存器)直接进入其他模式,因此用户模式下即usr模式就不能通过编程操作来进入其他模式,因此在 有操作系统的情况下,应用程序的人处于用户模式,由于不能保证他们代码的稳定性,因此限制了应用程序的权限,防止破坏整个操作系统

对于每种异常之间的差异就是寄存器,灰色三角形代表的是其模式下的专属寄存器,例如MOV R0,R8,在sysy模式下是"MOV R0,R8",在FIQ模式下"MOV R0,R8_fiq",即两个寄存器不是同一个,这些专属的寄存器称为备份寄存器,假需在用户模式下发生异常的时候需要保存R0-R15,对于FIQ异常有自己的R8-R14,那么就可以不保存用户模式下的R8-R14

2.2 状态(State)

对于CPU来说有两种State如下,就比如MOV R0,R1 对于ARM4个字节,对于Thumb2个字节,机器码的字节数不同,对于单片来说ROM寸土寸金用Thumb指令集,对于S3C2440来说有Nand和Nor内存大,用的是ARM指令集,也可以用Thumb指令集

ARM state使用ARM指令集,每个指令4个byte
Thumb state使用Thumb指令集,每个指令2个byte

 

Thumb 指令可以看做是ARM指令压缩形式的子集,是针对代码密度的问题而提出的,它具有16为的代码密度。Thumb不是一个完整的体系结构,不能指望处理程序只执行Thumb指令而不支持ARM指令集。因此,Thumb 指令只需要支持通用功能,必要时,可借助完善的ARM指令集,例如:所有异常自动进入ARM状态。

在编写Thumb 指令时,先要使用伪指令CODE16声明,而且在ARM指令中要使用BX指令跳转到Thumb指令,以切换处理器状态。编写ARM指令时,可使用伪指令CODE32声明。

 

2.3 程序状态寄存器

程序状态寄存器, CPSR(当前程序状态寄存器)/SPSR(保存的程序状态寄存器) ,例如"cmp R0,R1"会影响Z位,当R0与R1相等后Z位就会处于1,"beq XXX"指令会使用Z位,如果Z等于1,则跳转,对于SPSR用来保存 "被中断模式的CPSR"

可以读取bit4-bit来知道CPU当前处于哪种模式,也可以修改这些为来处理哪种模式,如果是用户模式就权限来修改,其中bit7显然是IRQ的总开关

进入异常的动作,异常处理流程(硬件)

  • LR_异常等于下一跳指令的地址,即被中断的下一条指令的地址,有可能是PC+4或者PC+8,取决于不同的情况
  • 然后SPSR_异常等于CPSR
  • 修改CPSR的M4~M0进入异常模式

退出异常的处理流程

  • PC等于LR_异常减去offset(后面有一张对应表格)
  • CPSR等于SPSR_异常
  • 清中断(如果是中断的话,对于其他异常就不需要管)

退出异常之后各异常减去的offset值

 

三、Thumb指令集示例

在gcc选项中加上"-mthumb",这样C文件都以thumb指令集执行,对于汇编文件需要我们自己来指定

all: led.o uart.o init.o main.o start.o
	#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -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 -mthumb -c -o $@ $<

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

Thumb不重点,例如在跳转到C函数的时候使用Thumb指令集,其中".code 32"表示用的是ARM指令集,由于CPU本身处于arm state,因此设置为".code 16"表示的是Thumb指令集的时候需要先切换到Thumb state模式,怎么切换如下代码所示,将跳转的thumb_func其地址的0位设置为1,CPU就会切换到thumb state模式,对于thumb state模式不能用"ldr pc, =main",需要借助R0来跳转

.text
.global _start
.code 32
_start:

	/* 关闭看门狗 */
	ldr r0, =0x53000000
	ldr r1, =0
	str r1, [r0]

	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
	ldr r0, =0x4C000000
	ldr r1, =0xFFFFFFFF
	str r1, [r0]

...
	/* 怎么从ARM State切换到Thumb State? */
	adr r0, thumb_func
	add r0, r0, #1  /* bit0=1时, bx就会切换CPU State到thumb state */
	bx r0
	
.code 16	
thumb_func:	
	bl sdram_init

	//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 */

	/* 重定位text, rodata, data段整个程序 */
	bl copy2sdram

	/* 清除BSS段 */
	bl clean_bss

	//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
	ldr r0, =main  /* 绝对跳转, 跳到SDRAM */
	mov pc, r0
...

GCC的ARM编译选项可参考:

https://www.cnblogs.com/QuLory/archive/2012/10/23/2735226.html

https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html

Getting GCC to compile without inserting call to memcpy :https://stackoverflow.com/questions/6410595/getting-gcc-to-compile-without-inserting-call-to-memcpy

 

四、und异常模式程序示例

参考异常向量表,可知道发生und会跳转到0x4的地址,故意引入一条未定义的指令(即CPU识别不了的指令),这样就会触发中断,最终会到do_und,此时发生的动作:lr_und保存有被中断模式下一条即将执行的指令的地址,SPSR_und保存有被中断模式的CPSR,CPSR中的M4-M0被设置为11011, 进入到und模式,跳到0x4的地方执行程序,sp_und未设置, 先设置它,在und异常处理函数中有可能会修改r0-r12, 所以先保存,lr(此lr是异常模式自己的lr即lr_und)是异常处理完后的返回地址, 也要保存,"mrs r0, cpsr"将cpsr寄存器的值保存在r0中向c函数传递参数,在汇编中字符串的定义可在网上找到官方的文档:http://web.mit.edu/gnu/doc/html/as_7.html ,里面介绍了两种方式,用.ascii来定义字符串的时候不会自动加上结束符,使用.string的时候会自动加上结束符

当发生异常的时候会打印出cpsr的值和字符串"undefined instruction exception",打印出来的值Bit4 - Bit0就表示的是UND模式

.text
.global _start

_start:
	b reset  /* vector 0 : reset */
	ldr pc, und_addr /* vector 4 : und */

und_addr:
	.word do_und

do_und:
	/* 执行到这里之前:
	 * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_und保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
	 * 4. 跳到0x4的地方执行程序 
	 */

	/* sp_und未设置, 先设置它 */
	ldr sp, =0x34000000

	/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
	/* lr是异常处理完后的返回地址, 也要保存 */
	stmdb sp!, {r0-r12, lr}  
	
	/* 保存现场 */
	/* 处理und异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
und_string:
	.string "undefined instruction exception"

.align 4


...

	/* 故意加入一条未定义指令 */
und_code:
	.word 0xdeadc0de  /* 未定义指令 */


----------------------------------------------------

void printException(unsigned int cpsr, char *str)
{
	puts("Exception! cpsr = ");
	printHex(cpsr);
	puts(" ");
	puts(str);
	puts("\n\r");
}

 

五、swi异常模式程序示例

程序执行首先从0地址开始即跳到reset的地方,此时CPU处于Supervisor(svc)管理模式,首先我们需要切换到usr模式,因为swi异常会进入管理模式,对于linux中应用程序的人需要访问硬件,会受限于管理模式,那么APP想访问硬件,必须切换Mode,那怎么切换,就需要发生异常来切换,可以中断、und或者"swi #val"使用软中断来切换,swi是最常用的,在linux中可以用来执行各种系统调用

下列程序可以读出swi指令的值0x123,实现的机制是在svc模式下的lr寄存器会保存PC的值减去offset,因此需要要减去4,参考上面offset的表格,其中"stmdb sp!, {r0-r12, lr}"会保存lr的值,但是调用了"printException"C函数可能会用到lr,有两种解决方式,先执行"printSWIVal"来传参得到swi的值,也可以通过R4-R11来保存lr,在函数中,R4-R11可能被使用,所以在会在入口保存它们,在出口恢复它们,可以参考:ARM常用汇编指令与C程序机制

.text
.global _start

_start:
	b reset          /* vector 0 : reset */
	ldr pc, und_addr /* vector 4 : und */
	ldr pc, swi_addr /* vector 8 : swi */

und_addr:
	.word do_und

swi_addr:
	.word do_swi


...
do_swi:
	/* 执行到这里之前:
	 * 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_svc保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
	 * 4. 跳到0x08的地方执行程序 
	 */

	/* sp_svc未设置, 先设置它 */
	ldr sp, =0x33e00000

	/* 保存现场 */
	/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
	/* lr是异常处理完后的返回地址, 也要保存 */
	stmdb sp!, {r0-r12, lr}  

	mov r4, lr
	
	/* 处理swi异常 */
	mrs r0, cpsr
	ldr r1, =swi_string
	bl printException

	sub r0, r4, #4
	bl printSWIVal
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
swi_string:
	.string "swi exception"

.align 4

...
	/* 复位之后, cpu处于svc模式
	 * 现在, 切换到usr模式
	 */
	mrs r0, cpsr      /* 读出cpsr */
	bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */
	msr cpsr, r0

	/* 设置 sp_usr */
	ldr sp, =0x33f00000

	ldr pc, =sdram
sdram:
	bl uart0_init

	bl print1
	/* 故意加入一条未定义指令 */
und_code:
	.word 0xdeadc0de  /* 未定义指令 */
	bl print2

	swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 */

	//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
	ldr pc, =main  /* 绝对跳转, 跳到SDRAM */

halt:
	b halt
	
------------------------------
void printSWIVal(unsigned int *pSWI)
{
	puts("SWI val = ");
	printHex(*pSWI & ~0xff000000);
	puts("\n\r");
}

 

六、按键中断程序示例

大致过程:

初始化:

  • 设置中断源,使其具备发出中断信号
  • 其次需要设置2440的中断控制器,使其能够发出中断给CPU
  • 设置CPU的CPSR中的I位,总开关使能中断

处理时,需要分辨中断源

处理完清中断

6.1 设置中断源

首先初始化按键引脚为中断引脚,还要设置EXTINT寄存器来设置触发方式

按键中断还要经过EINTMASK才有能力发到中断控制器,其中EINT3-0就不需要

其中GPF1和GPF2为EXTINT寄存器中的EINT0和EINT1,GPG3和GPG11对应着EXTINT寄存器中的EINT11和EINT19,同时设置EINTMASK寄存器使能EINT11和EINT19

void key_eint_init(void)
{
	/* 配置GPIO为中断引脚 */
	GPFCON &= ~((3<<0) | (3<<4));
	GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 */

	GPGCON &= ~((3<<6) | (3<<22));
	GPGCON |= ((2<<6) | (2<<22));   /* S4,S5被配置为中断引脚 */
	

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);             /* S5 */

	/* 设置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1<<11) | (1<<19));
}

读取这个寄存器来分辨哪个中断产生了,然后还要清除他,EINT4-23要读这个寄存器才能知道哪个中断产生了,清中断时只需要写这个寄存器的相应位

 

6.2 设置中断控制器

其次设置2440的中断控制器,对于按键、定时器等是"without sub"类的,因此走的是红框的方向,其中MODE可以设置为FIQ快速中断模式

其中EINT4_7共用一条中断线,EINT8_23共用一条中断线

外部中断不需要经过SUBSRCPND和SUBMASK寄存器,只需要设置SRCPND、MASK、INTPND就可以了

对于SRCPND用来显示哪个中断产生了,执行完之后需要清除SRCPND

对于MASK如果设置为1,即使中断源过来了也不会触发中断

可以读INTPND寄存器得到当前正在处理的中断时哪一个,经过中断优先级的中断,需要清除对应位

次寄存器用来显示INTPND中哪一位被设置为1,因此读该寄存器的值更方便去读INTPND,这个位不需要我们清除,是因为清除了SRCPND和INTPND寄存器之后会自动清楚

 

6.3 示例

在start.S中使能总开关,同时当发生中断时,会执行handle_irq_c函数

.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 */
...

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里 */

...

	/* 复位之后, cpu处于svc模式
	 * 现在, 切换到usr模式
	 */
	mrs r0, cpsr         /* 读出cpsr */
	bic r0, r0, #0xf     /* 修改M4-M0为0b10000, 进入usr模式 */
	bic r0, r0, #(1<<7)  /* 清除I位, 使能中断 */
	msr cpsr, r0

 初始化按键, 设为中断源

void key_eint_init(void)
{
	/* 配置GPIO为中断引脚 */
	GPFCON &= ~((3<<0) | (3<<4));
	GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 */

	GPGCON &= ~((3<<6) | (3<<22));
	GPGCON |= ((2<<6) | (2<<22));   /* S4,S5被配置为中断引脚 */
	

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);             /* S5 */

	/* 设置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1<<11) | (1<<19));
}

 初始化中断控制器,对于其他寄存器在我们发生中断的时候用

void interrupt_init(void)
{
	INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}

当发生中断时,利用中断控制器的INTOFFSET来分辨中断源,清中断的时候需要从源头开始清除,需要先清除EINTPEND,再清除SRCPND和INTPND

void handle_irq_c(void)
{
	/* 分辨中断源 */
	int bit = INTOFFSET;

	/* 调用对应的处理函数 */
	if (bit == 0 || bit == 2 || bit == 5)  /* eint0,2,eint8_23 */
	{
		key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
	}

	/* 清中断 : 从源头开始清 */
	SRCPND = (1<<bit);
	INTPND = (1<<bit);	
}

 用EINTPEND分辨率哪个EINT产生,清除EINTPEND,只需要对其写入相应位


/* 读EINTPEND分辨率哪个EINT产生(eint4~23)
 * 清除中断时, 写EINTPEND的相应位
 */


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;
}

 

6.4 函数指针数组注册中断

实现的机制,在中断源初始化的过程中注册中断服务函数,注册的时候再设置其控制器的INTMSK寄存器来控制中断源发生,这样每使能一个中断源,只需要用register_irq来注册中断号和中断函数

typedef void(*irq_func)(int);
irq_func irq_array[32];


void interrupt_init(void)
{
	INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}

void key_eint_irq(int irq)
{
    ...
}


void handle_irq_c(void)
{
	/* 分辨中断源 */
	int bit = INTOFFSET;

	/* 调用对应的处理函数 */
	irq_array[bit](bit);
	
	/* 清中断 : 从源头开始清 */
	SRCPND = (1<<bit);
	INTPND = (1<<bit);	
}

void register_irq(int irq, irq_func fp)
{
	irq_array[irq] = fp;

	INTMSK &= ~(1<<irq);
}

void key_eint_init(void)
{
	/* 配置GPIO为中断引脚 */
	GPFCON &= ~((3<<0) | (3<<4));
	GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 */

	GPGCON &= ~((3<<6) | (3<<22));
	GPGCON |= ((2<<6) | (2<<22));   /* S4,S5被配置为中断引脚 */
	

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);             /* S5 */

	/* 设置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1<<11) | (1<<19));

	register_irq(0, key_eint_irq);
	register_irq(2, key_eint_irq);
	register_irq(5, key_eint_irq);
}

 

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值