11.ARM架构的异常与中断

目录

1.异常与中断的引入

2.CPU模式(Mdde)状态(state)以及程序状态寄存器

3.Undefined 异常模式例子

4.SWI异常模式实例

源代码:


1.异常与中断的引入

1.1.中断的概念

所谓中断是指CPU对系统发生的某个事件作出的一种反应:CPU暂停正在执行的程序,保留现场后自动地转去执行相应的处理程序,处理完该事件后再返回断点继续执行被“打断”的程序。引起中断的事件称为中断源,中断源向CPU提出进行处理的请求称为中断请求。

1.2.中断类型

按中断事件来源进行分类,主要有两类:

(1)中断。由CPU以外的事件引起的中断,如I/O中断、时钟中断、控制台中断等。

(2)异常(exception)。来自CPU的内部事件或程序执行中的事件引起的过程。如

由于CPU本身故障、程序故障和请求系统服务的指令引起的中断等。

1.3.中断的一般处理过程

中断处理一般分为中断响应和中断处理两个步骤。中断响应由硬件实施,中断处理主要由软件实施。

1.4.生活例子引入

假如我现在再家里玩游戏,母亲出门之前交代我看管着在房间里熟睡的孩子,那我如何知道小孩子是否醒了呢?

答:

方式一:我时不时停下手中的游戏,然后打开房门看一下孩子醒了没有,查询方式。

在c语言中的逻辑就是,我现有有两个任务,打游戏和照顾孩子,孩子睡觉了,我就一直打游戏,而且时不时去看一下孩子,如果孩子醒了的话我就不打游戏,照顾孩子,如果孩子还没醒我就继续打游戏。

方式二:干脆我不管了,打开房间门,听到孩子哭了以后我再去照顾他,中断方式。

在这里也是有两个任务,但是我做的就是一直打游戏,除非孩子的哭声中断我打游戏,我就停下来照顾孩子,等孩子不吵闹了,我就继续回去打游戏(处理完中断事件)。

而上面方式二中,孩子的哭声打断了我的游戏时间,在ARM中是如何处理的是中断和异常的核心。

按照正常的逻辑思维,处理过程是怎样的?

1.我在打游戏

2.有各种声音,外面的猫叫(不理睬),狗叫(不理睬),门铃声(开门),小孩哭声(照顾小孩)

3.如果有我关心的声音,我需要怎么处理?

①暂停游戏(保存现场)

②处理事件(照顾小孩,开门),对于如何处理,是发生不同情况做不同处理。(处理事件)

③处理完,继续玩游戏。(恢复现场)

1.5.什么事情能打断我打游戏动作呢?

我现在在玩游戏,在外部我的耳朵能听到猫叫声,狗叫声,门铃声,小孩的哭声。其中门铃声和小孩的哭声会中断我的游戏。

内部:身体不舒服,电脑坏了等等。导致我不能玩游戏了。对于这些特殊的情况不像远处的猫狗叫声可以忽略,这个必须马上处理。

对比ARM架构处理中断

其中,在上面,狗叫猫叫,门铃声,小孩的哭声属于中断,而身体不舒服、电脑坏了,属于异常(没有办法屏蔽)

1.6.ARM对异常(中断)的使用过程

对比引入上面的例子。

1.初始化:a.设置中断源(允许产生中断 ),b.设置中断源(选择接受的中断,或屏蔽某个中断)c.设置中断总开关

【对比生活例子:需要使用耳朵(总开关),选择感兴趣的声音(哭声,门铃)。】

2.执行一般程序(打游戏)

3.产生中断:按键按下,中断信号发给中断控制器,然后发送给CPU,而CPU每执行完一条指令都会检查是否有中断(异常)产生。(孩子哭声)

4.发现有中断(异常)产生,处理,对于不同的异常(中断),采取不同的处理方式(跳到不同的地址执行程序,根据中断向量表【硬件机制】),注意:这个地址只是一条跳转指令,跳转到中断处理函数,在后面的程序中就能体现出来。

对于是门铃声音还是孩子哭声,都采取不同的处理方式。

5.跳转到中断处理函数需要做的事情:

其中有三个核心步骤:

①保存现场(保存处理之前寄存器的状态)

②处理异常(中断)-->根据不同的异常,执行不同的函数

③恢复现场(恢复中断之前的寄存器的状态)


2.CPU模式(Mdde)状态(state)以及程序状态寄存器

2.1.CPU的七种模式

以S3C2440为例,打开其数据手册

2.2.CPU的两种状态

1.ARM state :使用ARM指令集,每个指令占四个字节。

2.Thumb state:使用Thumb指令集,每个指令占2个字节。

例子:

指令:mov r0 r1 

如果使用ARM指令集编译,占四个字节,使用Thumb指令集占两个字节。即编译产生的机器码占用的空间。

目的:为了减少存储程序的控件,节省资源。

Thumb指令集 

Thumb 指令可以看作是 ARM 指令压缩形式的子集,是针对代码密度的问题而提出的,它具有 16 位的代码密度但是它不如ARM指令的效率高 .Thumb 不是一个完整的体系结构,不能指望处理只执行Thumb 指令而不支持 ARM 指令集.因此,Thumb 指令只需要支持通用功能,必要时可以借助于完善的 ARM 指令集,比如,所有异常自动进入 ARM 状态.在编写 Thumb 指令时,先要使用伪指令 CODE16 声明,而且在 ARM 指令中要使用 BX指令跳转到 Thumb 指令,以切换处理器状态.编写 ARM 指令时,则可使用伪指令 CODE32声明。
2.3.ARM state 下的寄存器资源的差别

2.4.程序状态寄存器

2.5.发生异常时,ARM state下的寄存器的变化或协同工作

1).进入异常时的动作(硬件自动完成)

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

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

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

4.跳到向量表。

2).退出异常时的动作

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

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

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

3.清除中断标识位。


3.Undefined 异常模式例子

以下是一个例子,在例子中写一条指令,让CPU进入未定义指令异常的模式,然后根据第3节的说明进行简单的编程设置来处理这个异常,处理完异常之后重新回到程序中正常运行主函数。

3.1.ARM920t(2240)的异常向量表地址

3.2.编写程序

在第10节的例子上修改:点我查看

1).修改启动文件start.S,增加异常向量表。

默认跳转到Reset,在0地址添加一个跳转指令:b Reset,然后定义一个入口Reset,系统一上电后复位,从0地址开始运行,0地址是一个跳转指令,跳转到Reset的地方。

如果发生了未定义指令异常,CPU直接跳转到0x0000004的地方(这是硬件决定的),在0x04的地方放一条跳转指令,当发生未定义指令异常时,我们自己需要去处理这个异常。

2).系统一上电从0地址开始运行,在Reset这里,添加一条未定义指令,让他能够进入未定义指令异常模式:

在执行到这条指令,CPU读取指令之后解析,发现没办法执行,就会发生未定义指令异常,硬件就会跳到:0x0000004的地方开始指令,而我们在0x4的地方放了一条跳转指令。接着就会开始执行:und_handle函数。

如何查看哪些是未定义指令呢?

查看2440的ARM指令集的部分,里面有对指定的定义

而0xdeadc0de,是没有定义的指令,CPU无法识别出来。

3).在进入异常之后,添加und_handle的处理流程

为了简单起见,当发生这个异常时,只打印出异常指令字符就行了。

使用C语言编写,新建一个文件,命名为:execption.c,内容如下:

#include "uart.h"


void Execption_print(unsigned int cpsr,char *str)
{
	putString("\r\nUndifined Execption! cpsr = ");
	//打印CPSR的值
	puthex(cpsr);
	putString("  ");
	putString(str);
	putString("\n\r");
		

}

当处理异常时,就来调用这个函数,即,打印出CPSR的值,看当前处于哪一个模式,然后再打印字符串。

  • 在启动文件处理流程中调用这个函数

这个函数需要两个参数,CPSR和str,先定义

注意事项:

1.因为und_string的字符串不一定是四字节的整数倍,所以在这个语句后面需要添加四字节指令对齐函数,让指令四字节对齐

2.因为如果遇到指令异常,会进入指令异常模式,在这个模式中需要使用串口的打印函数,所以在异常指令前面就需要去初始化串口函数,如下:

4).修改Makefile添加这个新的c文件,进行编译。

内容如下:

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

 Makefile文件解析:

  • 通配符符号:%

①: $@   表示目标文件

②: $<     表示第1个依赖文件

③: $^     表示所有依赖文件

5).上传,编译烧写:

烧写,查看结果:

6).流程和结果分析:

1.可以看到CPSR 寄存器的值是 0x600000DB,0xDB = 0b1011011

他的低4位表示的是CPU的运行模式,查看位的定义:

可以看到CPSR的最低5位,的确是11011,CPU进入未定义指令异常模式。

2.流程分析:

①上电复位,CPU从0地址开始运行,0地址是一条跳转指令,跳到Reset

②跳转到Reset向下继续执行,做了一系列初始化工作,比如设置系统模式的栈,设置系统时钟,初始化SDRAM,代码重定位,清除bss段,初始化串口等工作。

③执行到und_code,CPU发现是一条未定义指令。 发生未定义指令异常,硬件强制跳到0x4的位置执行。

跳转到何处根据向量表决定,如下,不同异常跳到不同的地址:

④在0x4地址是一条跳转指令,而在进行入异常之前系统已经帮我们完成了很多动作了,如芯片手册描述

1.在undefined模式下的寄存器的LR寄存器,保存了被中断模式中下一条即将执行指令的地址(比如在这个例子中,被中断之前是在用户模式下运行了,那么这个LR寄存器就保存了这个模式被中断位置下一条指令的地址)。

2.Undefigned模式下SPSR,保存了被中断模式的CPSR的值。

3.CPSR中的[M4-M0]被设置为11011

4.强制进入Undefined模式 PC跳到0x00000004的地址执行、

⑤跳转到异常的处理函数

每种模式都必须设置自己的堆栈空间,用于保存r0-r12,以及LR寄存器。

1.设置堆栈空间

2.因为系统模式和Undefined模式公用了r0-r12寄存器,所以需要先备份这些寄存器,以便处理异常返回到系统模式继续执行正常的工作。此外还需要保存此模式下LR寄存器的值,因为如果下面执行到bl指令跳转,就会改变LR的值,就没有办法回到中断之前的位置了。

3.处理异常,简单来说,就是当发生了这个异常,需要去做一些什么工作,在这里只是打印出当前程序状态寄存器(CPSR)的值,以及打印出一个字符串。

4.当处理完异常之后,就需要回到异常之前的模式了,也就是系统模式,如何回去呢?

恢复在中断之前的寄存器,也就是r0-r12,然后把pc地址指向刚刚保存的LR寄存器开始运行,恢复CPSR的值。

备注:此处的源代码名称为:Undefined异常模式


  • 改进程序

问题1:

在还没有进入main函数之前使用的是相对跳转,如果烧写到Nand Flash,程序在上电的时候,硬件把Nand Flash前4k的数据拷贝到SRAM,并从0地址开始运行,开始重定位,初始化的一些动作,只有执行到了main函数之后才进行绝对的跳转,使程序真正的在SDRAM运行。

但是现在有一个问题,我们的异常指令是在main函数跳转之前的,使用的是b/bl,相对跳转命令,那程序就仍然还是在SRAM运行。假如说在处理异常模式时,调用某个函数使用bl跳转,如果说这个程序小于4k的话不会有问题,但是如果大于4K的话,因为在SRAM之后4k的大小,所以Nand Flash大于4k的程序是没有办法拷贝到SRAM的,所以直接跳转就会找不到命令,导致出错。

那么可以怎样做呢?

因为程序在执行错误的指令之前已经全部重定位到SDRAM了,所以在发生异常跳转的时候就不要使用相对地址了,使用绝对地址就行了。

问题2:

ldr pc, =und_handle,是一条伪指令,最终会把und_handle的值保存在内存中,当需要去跳转的时候就去读内存中的值,这个值一般放置在bin文件的后面,但是如果这个bin文件的大小超出4K的话,而存放伪指令的值正好也是大于4k,因为此时程序仍然在SRAM执行,那么在这条伪指令读取地址的时候就会出错,导致整个程序没办法运行。

比如查修改完这条指令的反汇编代码:

为了避免这种情况发生,我们就指定让他去前4k的内存去读取und_handle这个数值。

编译看一下反汇编文件:

一旦执行完ldr pc,=0x30000008,直接就跳转到SDRAM里面执行了,就没有超不超出4k的问题了。

问题3:

在重定位完成之后就应该马上跳到SDRAM里面执行了,因为怕有bin文件大于4k,导致没有办法运行,如果跳转到SDRAM,因为哪里有完整代码的拷贝。

修改如下:

这里补充一点,为什么使用ldr pc,=sdram,就能跑到SDRAM里面运行呢。因为在还没有重定位之前使用的都是绝对地址。而且如果不使用绝对跳转的话程序就会在SRAM运行,正是因为有了相对地址,即使我们的即使在链接脚本指定了代码段从0x30000000开始,烧写到NOr Flash,和Nand Flash它仍然能够运行。

比如:

这个是我们bin文件的结构。

当烧写到Nor或者Nand的时候,此时相对地址0x30000000对于他们来说就是0地址。如下图:

现在查看一下代码的反汇编文件,可以看到地址都是从0x30000000开始的,那为什么能在NOR Flash运行,此时在反汇编文件的,0x30000000地址就相当于Nor Flash的0地址。

在NOR Flash运行,需要进行函数跳转的时候,使用的是相对跳转,bl或者b

 

跳转到Reset函数执行,跳转到0x30000050,那么在NOR Flash相对值就是0x50,即为跳转到0x50的地方执行函数。

除非你使用指令 ldr pc,=0x30000050,进行绝对跳转,才真正的挑战到0x30000050这个地址执行(也就是SDRAM的地址)。

所以说在还没有进行重定位之前不可使用绝对跳转,因为还没重定位之前SDRAM是没有指令的,绝对跳转过去之后读取不到指令,系统就挂了。

把上面程序修改后编译一下,查看反汇编代码修改的这一部分:

如果没有这句,下面那么串口初始化的函数就仍然是在NOR Flash 或者SRAM运行,如果串口初始化函数在4k之外,那就没有办法运行了,所以先使用绝对跳转使pc指向 0xeb000055的地方执行。从这以后,程序也就完全是在SDRAM运行了。

备注:此处的源代码名称为:Undefined异常模式02 


程序执行简图:


4.SWI异常模式实例

swi(software interrupt):软件中断

ARM的CPU共有七中模式,除了用户模式(系统模式),其他6种都是特权模式,处于特权模式可以通过修改CPSR寄存器切换到其他模式,但是处于用户模式,就不能修改CPSR寄存器进入其他模式。

在Linux系统,应用程序一般运行与用户模式,是一个受限的模式,不可访问硬件。

但是如果应用程序向访问硬件就必须切换模式,如何切换呢?

发生异常:中断或其他异常。或者使用软件中断swi

使用方式: swi #某个值, 且可以通过这个值分辨为何执行这个指令。


例子:

直接在前面未定义指令异常的例子上修改,中断的处理流程是类似的。

步骤:

1).复位之后CPU处于管理模式(SVC),需要在重定位之后切换到用户模式(usr)

因为复位处于管理模式,是一种特权模式,所以可以通过改变cpsr寄存器的位,来切换到usr模式,各位的描述如下:

需要设置CPSR的低5位为:10000

2).设置用户模式下的栈空间,指定某一块地址,设置在SDRAM的位置

代码如下:

3).使用绝对跳转到SDRAM执行,ldr pc, = sdram,其中sdram是一个地址值。

4).引入一条swi指令,如:swi 0x456,执行此命令产生swi异常,CPU从User用户模式,切换到管理员(svc)

软中断之后进入管理员模式。硬件强制跳转到0x8的位置执行,

代码如下:

 

5).在0x8的地址放一条跳转指令,跳转到异常处理函数,在异常处理函数做的事情和上面大致相同。

  • 设置软件(swi)模式下需要使用的栈控件
  • 保存现场(保存r0-r12,lr,cpsr寄存器)
  • 软件中断处理函数
  • 恢复现场(恢复r0-r12,pc,cpsr寄存器)

保存现场和恢复现场等工作就和undefined异常时一样的,下面贴出代码:

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

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

	/*保存现场*/
	/*1.保存r0-r12,因为这几个寄存器是和swi异常处于管理模式共用的
	 *在这个异常模式有可能会修改寄存器的值,先保存
	 *起来,等退出这个异常的时候再恢复回去
	 *2.在手册中可知,在swi异常返回时,直接把
	 *lr寄存器赋值给pc,没有偏移,所以直接保存lr
	 */
	stmdb sp!, {r0-r12, lr}
	/*处理undfined异常*/
	mrs r0, cpsr
	ldr r1, =swi_string
	bl Execption_print
	/*恢复现场*/
	/*1.恢复r0-r12的值,然后把lr寄存器赋值给pc,使程序
	 *返回到上一次异常的位置接着往下执行。
	 *2. ‘^’会把spsr的值恢复到cpsr寄存器
	 */
	ldmia sp!, {r0-r12,pc}^

swi_string:
	.string "SWI  exception!"

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

6).编译,下载运行,如下:

分析运行结果:

1).cpsr = 0x600000D0,开始可以看出[M4-M0] = 0b10000,处于User模式,在这个模式是不可以随意切换到其他模式的,因为这不是特权模式。

2).接着遇到一个指令:0xdeadc0de,CPU解析不出来,产生未定义(Undefined)指令异常,CPU进入Undefined模式,cpsr = 0x600000DB,则[M4-M0] = 0b11011,进入Undefined的异常的处理函数,然后返回到User模式继续执行,可以从cpsr的值看出来确实是回到了User模式了。

3).接着遇到指令:swi 0x456,产生swi异常,进入管理员模式(svc),cpsr = 0x600000D3,则[M4-M0] = 0b10011,接着进入swi异常处理函数,然后返回User模式继续运行接下来的函数,也就是跳转到主函数,打印出AaBbCc.....。

备注:此处源代码命名为:SWI异常01. 


应用程序可以根据swi指令传的值来判断为什么调用swi指令, 现在可以在异常处理函数中把这个值读取出来

修改上面的程序:

在execption.c中增加一个函数,用于打印这个值。函数如下:

void SWI_val_printf(unsigned int *Pswi)
{
	unsigned int val =*Pswi & ~0xff000000;
	putString("\r\n\r\n SWI_val = ");
	//打印SWI的值
	puthex(val);
	putString("\n\r");

}

 打印的时候忽略高8位:

如何得到swi的值,首先需要读出  swi 0x456这个指令,这个指令时保存在地址里面的,保存在哪里?

现在可以知道的是,执行swi 0x456会发生一个异常,那么异常模式里面的lr寄存器保存的是被中断模式中下一条指令的地址,那么swi 0x456这条指令的地址不就是lr地址减去4(上一条指令)嘛。

那么在进入swi异常处理函数,马上就把lr减去4的地址保存起来,接着打印出来swi 0x456这条指令的地址的值就可以了。

修改启动文件,在swi异常中打印swi异常的值:

编译,下载,运行:

备注:此处源代码命名为:SWI异常02.


 

源代码:

Undefined 异常模式    :https://download.csdn.net/download/qq_36243942/10913070

Undefined 异常模式02:https://download.csdn.net/download/qq_36243942/10913179

SWI异常01                  :https://download.csdn.net/download/qq_36243942/10914609

SWI异常02                  :https://download.csdn.net/download/qq_36243942/10914658

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

猜你喜欢

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