简介
本文内容主要从四个方面介绍ARM中断。
1、中断和异常相关的内容。
2、ARM中断响应和退出过程。
3、异常中断程序的安装
4、FIQ和IRQ异常中断处理程序
5、SWI软件中断异常处理程序
异常和中断
异常和中断是这样一个过程:当CPU内部或者外部出现某种事件(中断源)需要处理时,暂停正在执行的程序(断点),转去执行请求中断的那个事件的处理程序(中断服务程序),执行完成后,在返回被暂停执行的程序(中断返回),从断点处继续执行。其执行流程如下图所示。
异常和中断的处理过程基本一致,但二者并不是完全等同,习惯上将CPU内部产生中断叫异常,外部控制器产生中断叫中断。本文无特别说明,统一将中断和异常称为异常。
ARM有7种类型的异常,按优先级从高到低的排列如下:复位异常(Reset)、数据异常(Data Abort)、快速中断异常(FIQ)、外部中断异常(IRQ)、预取异常(Prefetch Abort)、软件中断(SWI)和未定义指令异常(Undefined instruction)。
异常种类和异常向量表
ARM体系结构中,存在7种异常。当异常发生时,处理器会把PC设置为一个特定的存储器地址。这一地址放在被称为向量表(vector table)的特定地址范围内。向量表的入口是一些跳转指令,跳转到专门处理某个异常或中断的子程序。存储器映射地址0x00000000是为向量表(一组32位字)保留的。在有些处理器中,向量表可以选择定位在存储空间的高地址(从偏移量 0xffff0000开始)。前面文档中已经说过,ARM有7种工作模式,处理用户模式外其他6种工作模式属于特权模式,特权模式中除了系统模式之外其他的都是异常模式,5种异常模式对应着系统的7种异常。对应的异常关系如下表所示。
向量表的入口是一些跳转指令,跳转到专门处理某个异常或中断的子程序,常见的跳转指令有:
(1) B <Addr>
这条分支指令实现了相对于PC的分支跳转,跳转的范围为PC向前或者向后的32M的空间。
(2) LDR PC, [PC , #offset]
这条指令是把异常处理程序的入口地址装载到PC,该地址是一个32位的绝对地址,可以访问到存储器的4G的空间范围。
(3) MOV PC, #Immediate
这条MOV指令把一个立即数复制到PC,该指令和第二指令一样可以跨越全部的地址空间,但是该指令受到地址对齐的限制,即立即数必须是一个8位立即数循环右移偶数次得到的结果。例如“MOV PC, #0x30000000”是合法的,因为0x30000000可以通过0x03循环右移4位得到。
每个异常发生时,总是从异常向量表开始起跳的,如下图所示,向量表里的每一条指令直接跳向对应的异常处理函数,其中,FIQ_Handleer()可以直接从地址0x1C处开始,从而节省了一条跳转指令,提高了FIQ的处理速度。
异常优先级
异常可以同时发生,当多个异常同时发生时,系统根据固定的优先级决定异常的处理次序,异常优先级由高到低排列次序如下表所示:
例如,复位异常的优先级最高,处理器上电时发生复位异常。所以当产生复位时,它将优先于其他异常得到处理。同样,当一个数据访问中止异常发生时,它将优先于除复位异常外的其他所有异常。优先级最低的2种异常是软件中断和未定义指令异常。因为正在执行的指令不可能既是一条SWI指令,又是一条未定义指令,所以软件中断异常SWI和未定义指令异享有相同的优先级。
ARM异常中断的处理过程
异常中断的响应过程
当一个异常出现以后,ARM微处理器会执行以下几步操作(由硬件自动执行):
1、保存处理器当前状态、中断屏蔽位以及各条件标志位。
这是通过将当前程序状态寄存器CPSR的内容赋值到要执行异常中断对应模式下的SPSR寄存器中实现的。
2、设置当前程序状态寄存器CPSR中对应的位。
- 改变处理器状态进入ARM状态。
- 改变处理器模式进入相应的异常模式。
- 设置中断禁止位禁止相应的中断。
3、保存返回地址
将下一条指令的地址存入相应的连接寄存器LR,以便程序在异常返回时能从正确的位置重新开始执行。若异常是从ARM状态进入,则LR寄存器中保存的是下一条指令的地址(发生中断时的指令PC+4或者+8,与异常类型有关);从Thumb状态进入的内容将在Thumb小节中介绍。
4、执行中断处理程序。
强制PC从相关的异常向量地址取下一条指令执行,从而跳转到相应的异常处理程序。
当异常发生时,操作伪指令如下:
R14_<exception_mode> = return link
SPSR_<exception_mode> = CPSR
CPSR[4∶0] = exception mode number
CPSR[5] = 0 /*进入 ARM 状态*/
If <exception_mode> = = reset or FIQ then
CPSR[6] = 1 /*屏蔽快速中断 FIQ*/
CPSR[7] = 1 /*屏蔽外部中断 IRQ*/
PC = exception vector address
处理器的状态判断
当异常发生时,处理器自动切换到ARM状态,但是在异常处理函数中要判断异常发生前处理器是ARM状态还是Thumb状态。这可以通过检测SPSR的T位来判断。
通常情况下,只有在SWI处理函数中才需要知道异常发生前处理器的状态。主要原因是响应SWI异常时,需要读取swi指令获取swi指令的请求立即数, 由于ARM状态和Thumb状态下swi指令的长度一样,所以Thumb状态下,调用SWI软中断异常必须注意以下两点。
① 发生异常的指令地址为(lr-2)而不是(lr-4)。
② Thumb状态下的指令是16位的,在判断中断向量号时使用半字加载指令LDRH。
下面的例子显示了一个标准的SWI处理函数,在函数中通过SPSR的T位判断异常发生前的处理器状态。
T_bit EQU 0x20 ; bit 5. SPSR 中的 ARM/Thumb 状态位,
:
:
SWIHandler
STMFD sp!, {r0-r3,r12,lr} ; 寄存器压栈,保护程序现场
MRS r0, spsr ; 读 SPSR 寄存器,判断异常发生前的处理器状态
TST r0, #T_bit ; 检测 SPSR 的 T 位,判断异常发生前是否为 Thumb 状态
LDRNEH r0,[lr,#-2] ; 如果是 Thumb 状态,使用半字加载指令读取发生异常的指令地址
BICNE r0,r0,#0xFF00 ; .提取中断向量号.
LDREQ r0,[lr,#-4] ; 如果是 ARM 状态,使用字加载指令,读取发生异常的指令地址
BICEQ r0,r0,#0xFF000000 ; 提取中断向量号并将中断向量号存入 r0
; r0 存储中断向量号
CMP r0, #MaxSWI ; 判断中断是否超出范围
LDRLS pc, [pc, r0, LSL#2] ; 如果未超出范围,跳转到软中断向量表 Switable
B SWIOutOfRange ; 如果超出范围,跳转到软中断越界处理程序
switable
DCD do_swi_1
DCD do_swi_2
:
:
do_swi_1
; 1 号软中断处理函数
LDMFD sp!, {r0-r3,r12,pc}^ ; Restore the registers and return.
; 恢复寄存器并返回
do_swi_2 ; 2 号软中断处理函数
:
异常中断的返回
从异常中断的程序中返回包括下面3个基本的操作(由软件实现中断程序的返回):
1)、恢复通用寄存器中的值,通用寄存器的恢复一般采用堆栈操作指令。
2)、恢复状态寄存器的值,将SPSR_mode寄存器的内容复制到当前程序状态寄存器CPSR中。
3)、修改PC的值,使程序能返回到发生异常中断指令的下一条指令处开始执行。该操作是将LR_mode寄存器中的内容复制到程序计数器PC中。
PC和CPSR的恢复可以通过下面3条指令中的一条指令来实现:
• MOVS PC,LR
• SUBS PC,LR,#4
• LDMFD SP!,{PC}^
这几条指令是普通的数据处理指令,特殊之处在于它们把程序计数器寄存器 PC 作为目标寄存器,并且带了特殊的后缀“ S ”或“ ^ ”,在特权模式下,“ S ”或者“ ^ ”的作用就是使指令在执行时,同时完成从 SPSR 到 CPSR 的拷贝,达到恢复状态寄存器的目的。
异常的返回地址
异常返回时,另一个非常重要的问题就是返回地址的确定。前面提到过,处理器进入异常时会有一个保存LR的动作,但是该保持值并不一定是正确中断的返回地址。以3级指令执行流水状态图来对此加以说明:
在ARM架构里,PC值指向当前执行指令地址加8。也就是说,当执行指令A(地址0x8000)时,PC等于0x8000+8=0x8008,即等于指令C的地址。假设指令A是BL指令,则当执行时,会把 PC 值(0x8008)保存到LR寄存器。但是,接下来处理器会对LR进行一次自动调整,使LR=LR-0x4。所以,最终保存在LR里面的是上图中所示的B指令地址。所以当从BL返回时,LR里面正好是正确的返回地址。
同样的跳转机制在所有的LR自动保存操作中都存在。当进入中断响应时,处理器对保存的LR也进行一次自动调整,并且跳转动作也是LR=LR-0x04。由此,就可以对不同异常类型的返回地址依次比较。
假设在指令B处(地址 0x8004)发生了异常,进入异常相应后,LR经过跳转保存的地址值应该是C的地址0x8008。
(1)SWI和未定义指令异常中断处理程序的返回
SWI和未定义指令异常中断是由当前执行的指令自身产生的,当SWI和未定义指令异常中断产生时,程序计数器PC的值还没有更新(PC=0x8008),它指向当前执行指令后面的第二条指令(对于ARM指令来说,它指向当前指令地址加8个字节的位置;对Thumb指令来说它指向当前指令加4个字节的位置,如下图所示)。当SWI和未定义指令异常发生时,处理器将PC-4保存到异常模式下的寄存器LR_mode中,从Thumb状态进入异常时LR_mode保存的值为PC-2。因此LR_mode保存的是当前指令的下一条指令的地址。SWI和未定义异常的返回直接把LR恢复给PC即可。
(2)IRQ或FIQ异常中断处理程序的返回
通常,处理器执行当前指令后查询IRQ中断引脚即FIQ中断引脚,并且查看系统是否允许IRQ中断及FIQ中断。如果中断引脚有效,并且系统允许该中断产生,则处理器将产生IRQ异常中断或者FIQ异常中断。当IRQ和FIQ异常中断产生时,程序计数器的值已经更新,它指向当前指令后面的第三条指令(对于ARM指令来说,它指向当前指令地址加12个字节的位置;对Thumb指令来说它指向当前指令加6个字节的位置,如下图所示)。当IRQ或者FIQ中断发生时,处理器将PC-4保存到异常模式下的寄存器LR_mode中,这时LR_mode寄存器指向当前指令的第二条指令的地址,从Thumb状态进入异常时LR_mode保存的值为PC的值,因此返回操作通过指令“SUBS PC,LR,#4”来实现。
(3)指令预取中止异常中断处理程序的返回
在指令预取时,如果目的地址是非法的,该指令将被标记成有问题的指令。这时,流水线上该指令之前的指令继续执行。当执行到该被标记成有问题的指令时,处理器产生指令预取中止异常中断。
当发生指令预取中止异常中断时,程序要返回到该有问题的指令处,重新读取并执行该指令。因此,指令预取中止异常中断程序应该返回到产生该指令预取中止异常中断的指令处,而不是像前面两种情况下返回到发生中断的指令的下一条指令。
指令预取中止异常中断是由当前执行额指令自身产生的,当指令预取中止异常产生时,程序计数器PC的值还没有更新,它指向当前指令后面第二条指令(对于ARM指令来说,它指向当前指令地址加8个字节的位置;对Thumb指令来说它指向当前指令加4个字节的位置,如下图所示)。当指令预取中止异常发生时,处理器将PC-4保存到异常模式下的寄存器LR_mode中,这时LR_mode寄存器指向当前指令的第二条指令的地址,从Thumb状态进入异常时LR_mode保存的值为PC的值,因此返回操作通过指令“SUBS PC,LR,#4”来实现。
(4)Data Abort数据中止异常中断处理程序的返回
当发生数据访问中止异常中断时,程序要返回到该有问题的数据访问处重新访问该数据。因此,数据访问中止异常中断处理程序应该返回到产生该数据访问中止异常中断的指令。
数据访问中止异常中断是由数据访问指令自身产生,当数据访问中止异常产生时,程序计数器PC的值已经更新, 它指向当前指令后面的第三条指令(对于ARM指令来说,它指向当前指令地址加12个字节的位置;对Thumb指令来说它指向当前指令加6个字节的位置,如下图所示)。当数据访问中止异常发生时,处理器将PC-4保存到异常模式下的寄存器LR_mode中,这时LR_mode寄存器指向当前指令的第二条指令的地址,从Thumb状态进入异常时LR_mode保存的值为数据访问中止异常指令的地址加8的值,因此返回操作通过指令“SUBS PC,LR,#8”来实现。
总结各异常和返回地址的关系如下表所示
说明:复位异常不需要返回。
Thumb状态下异常进入时LR的值
无论处理器处于什么状态,所有的异常都使处理器返回到ARM状态,并完成异常处理。所以无论从哪种状态进入的异常最后退出方式都是一样的。应该注意的是,ARM异常返回指令需要根据ARM流水线的行为对返回地址进行调整。由于Thumb指令是2字节长,而ARM指令是4字节长,所以由Thumb执行状态进入异常时其自然偏移量应与ARM不同(ARM状态下,拷贝到链接寄存器的值PC-4)。为了减少编程的复杂性,ARM体系结构中设置了硬件逻辑,以实现Thumb状态的自动地址偏移调整,使ARM和Thumb状态编程一致。下表列出了Thumb状态下发生异常时LR的值。
Thumb状态下发生未定义指令异常时,未定义指令异常模式的LR寄存器保存的是未定义指令地址+2(未定义指令的下一条Thumb指令),所以从异常中返回时,只需要将LR的值复制到PC即可。与ARM状态相同。与此类推,从Thumb状态下进入其他异常时,LR保存的地址会根据异常进行调整,以匹配异常的退出操作,保证异常返回地址的正确。
在应用程序中安装异常处理程序
1.使用汇编语言安装异常处理程序
如果系统启动不依赖于 Debug 或 Debug monitor 软件,可以使用汇编语言在系统启动时直接安装异常处理程序。
下面的例子显示了系统从 0x0 地址启动,直接安装异常处理程序的方法。
Vector_Init_Block
LDR PC, Reset_Addr
LDR PC, Undefined_Addr
LDR PC, SWI_Addr
LDR PC, Prefetch_Addr
LDR PC, Abort_Addr
NOP ;保留向量
LDR PC, IRQ_Addr
LDR PC, FIQ_Addr
Reset_Addr DCD Start_Boot
Undefined_Addr DCD Undefined_Handler
SWI_Addr DCD SWI_Handler
Prefetch_Addr DCD Prefetch_Handler
Abort_Addr DCD Abort_Handler
DCD 0 ;保留向量
IRQ_Addr DCD IRQ_Handler
FIQ_Addr DCD FIQ_Handler
有些情况下,系统 0x0 地址不一定是 ROM。如果 0x0 地址为 RAM,那么就系统将中断向量表从 ROM 复制 RAM,下面的例子显示了这样一个过程。
MOV R8, #0
ADR R9, Vector_Init_Block
LDMIA R9!,{r0-r7} ;复制中断向量表 (8 words)
STMIA R8!,{r0-r7}
LDMIA R9!,{r0-r7} ;复制由伪操作 DCD 定义的地址
STMIA R8!,{r0-r7}
2.使用 C 语言安装异常处理程序
程序中有时需要在 main()函数中使用 C 语言安装中断向量表。这就要求指令经编译后的
解码能安装在内存的正确位置。
(1)向量表中使用跳转指令的情况
如果在向量表中使用跳转指令,使用下面的步骤完成向量表的安装。
① 读取异常处理程序的地址。
② 从异常处理程序地址中减去向量表中的偏移。
③ 为适应指令流水线,将上一步得到的地址减 8。
④ 将得到的结果右移 2 位,得到以字为单位的地址偏移量。
⑤ 将结果的高 8 位清零,得到跳转指令的 24 位偏移量。
⑥ 将上一步得到的结果和 0xea000000(无条件跳转指令编码)做逻辑与操作,从而得
到要写到向量表中的跳转指令的正确编码。
下面的例子显示了这样一个标准过程。
unsigned Install_Handler (unsigned routine, unsigned *vector)
{
unsigned vec, oldvec;
vec = ((routine - (unsigned)vector - 0x8)>>2);
if ((vec & 0xFF000000))
{
/* diagnose the fault */
prinf ("Installation of Handler failed");
exit (1);
}
vec = 0xEA000000 | vec;
oldvec = *vector;
*vector = vec;
return (oldvec);
}
**注意:**相信很多读者在看到上面的函数的时候会有点懵,主要懵的地方是((routine - (unsigned)vector - 0x8)>>2)算法。下面我详细介绍一下。
- routine:传进来的是异常中断函数接口的绝对物理地址。比如IRQ_Handler 编译后被分配在0x2000的绝对地址。
- vector:vector的参数传进来的是异常中断函数在中断向量表中的偏移地址。比如IRQ_Handler在向量表中的偏移地址0x18。
- routine - (unsigned)vector的意思是:实际异常中断函数相对于向量表中对应异常入口地址的偏移量。计算方式如下图所示。
- 减去0x8:是因为CPU按照流水线一条一条的执行指令,如取指-》译码-》执行。当异常中断发生时,CPU的PC被置为对应中断的入口地址进行取指。假设外部IRQ中断发生时,PC = 0x18,取出“B IRQ_Handle()”的执行进行译码和执行,当在执行“B IRQ_Handle()”指令时,PC已经自增到0x20的地址,而B跳转指令跳转范围以PC为基准的,可以理解为“B IRQ_Handle()跳转范围是0x18地址前后的32M的空间。但是在执行0x18地址的跳转指令时,PC已经变为了0x20,减去0x08的实际意义是调整PC的自增偏移。
- >> 2:如下是跳转指令格式,B跳转指令计算偏移地址时,将24位的立即数扩展到32位并进行左移2位。所以在组合B指令之前需要将偏移地址右移动2位。
- vec = 0xEA000000 | ver:这条语句是组合一条32位的绝对跳转指令。
- *vector = vec:往对应的向量表地址填写跳转指令。
(2)在向量表中使用加载 PC 指令
在向量表中使用加载 PC 指令,按照下面的步骤完成。
① 读取异常处理程序地址。
② 从异常处理程序地址中减去向量表中的偏移。
③ 为适应指令流水线,将上一步得到的地址减 8。
④ 保留结果的后 12 位
⑤ 将结果与 0xe59ff000(LDR PC, [PC,#offset])做逻辑或操作,从而得到要写到向量表
中的跳转指令的正确编码。
⑥ 将异常处理程序的地址放到相应的存储单元。
下面的例子显示了一个标准的 C 语言过程。
unsigned Install_Handler (unsigned location, unsigned *vector)
{
unsigned vec, oldvec;
vec = ((unsigned)location - (unsigned)vector - 0x8) | 0xe59ff000;
oldvec = *vector;
*vector = vec;
return (oldvec);
}
FIQ 和 IRQ 中断处理函数的设计
1.中断分支
ARM内核只有两个外部中断输入信号nFIQ和nIRQ。但对于一个系统来说,中断源可能多达几十个。为此,在系统集成的时候,一般都会有一个异常控制器来处理异常信号,如下图所示。
这时候用户程序可能存在多个IRQ/FIQ的中断处理函数。为了使从向量表开始的跳转始终能找到正确的处理函数入口,需要设置一套处理机制和方法。是由软件来处理异常分支的,软件可以通过读取中断控制器来获得中断源的信息,中断实现框图如下图所示。
因为软件的灵活性,可以设计出比上图更好的流程控制方法,如图所示。
Int_vector_table是用户自己开辟的一块存储器空间,里面按次序存放异常处理函数的地址。IRQ_Handler()从中断控制器获取中断源信息,然后再从Int_vector_table中的对应地址单元得到异常处理函数的入口地址,完成一次异常响应的跳转。这种方法的好处是用户程序在运行过程中,能够很方便地动态改变异常服务内容。
2.ARM 编译器对中断处理函数编写的扩展
考虑到中断处理函数在现场保护和返回地址的处理上与普通函数的不同之处,不能直接把普通函数体连接到异常向量表上,需要在上面加上一层封装,下面是一个例子。
IRQ_Handler ;中断相应函数
STMFD SP!,{r0-r12,lr} ;保护现场,一般只需要保护{r0-r3,LR}
BL IrqHandler ;进入普通处理函数,C 或汇编均可
……
LDMFD sp!,{r0-r12,LR} ;恢复现场
SUBS pc,lr,#4 ;中断返回,注意返回地址
为了方便使用高级语言直接编写异常处理函数,ARM编译器对此做了特定的扩展,可以使用函数声明关键字_irq,这样编译出来的函数就可以满足异常响应对现场保护和恢复的需要,并且自动加入LR 减4的处理,符合IQR和FIQ中断处理的要求。
下面的例子显示了使用_irq对中断处理函数产生的影响。
C 语言源程序如下:
__irq void IRQHandler (void)
{
volatile unsigned int *base = (unsigned int *) 0x80000000;
if (*base == 1)
{
/*调用 C 语言中断处理函数*/
C_int_handler();
}
/*清楚中断标志*/
*(base+1) = 0;
}
使用armcc编译出的汇编代码如下。
IRQHandler PROC
STMFD sp!,{r0-r4,r12,lr}
MOV r4,#0x80000000
LDR r0,[r4,#0]
SUB sp,sp,#4
CMP r0,#1
BLEQ C_int_handler
MOV r0,#0
STR r0,[r4,#4]
ADD sp,sp,#4
LDMFD sp!,{r0-r4,r12,lr}
SUBS pc,lr,#4
ENDP
如果不使用_irq子程序声明关键字,编译出的汇编代码如下。
IRQHandler PROC
STMFD sp!,{r4,lr}
MOV r4,#0x80000000
LDR r0,[r4,#0]
CMP r0,#1
BLEQ C_int_handler
MOV r0,#0
STR r0,[r4,#4]
LDMFD sp!,{r4,pc}
ENDP
3.可重入中断设计
在缺省情况下,ARM中断是不可重入的。因为一旦进入异常响应状态,ARM自动关闭中断使能。如果在异常处理过程中,简单地打开中断使能而发生中断嵌套时,显然新的异常处理将破坏原来的中断现场而导致出错。但有时需要中断必须是可重入的,因此要通过程序设计来解决这个问题。其中有两个关键问题。
① 新中断使能之前,必须要保护好前一个中断的现场信息。比如LR_irq和 SPSR_irq等。
② 中断处理过程中对BL进行保护。
在中断处理函数中发生函数调用BL是很常见的,假设有下面一种情况。
IRQ_Handler:
……
BL Foo
ADD
其中,
Foo:
STMFD SP!,{r0-r3,LR}
……
LDMFD SP!{r0-r3,PC}
上述程序,在IRQ处理函数IRQ_Handler()中调用了函数Foo()。若是在 IRQ_Handler()里面中断可重入的话,可能发生问题,考察下面的情况:当新的中断请求恰好在“BL Foo”指令执行完成后发生。这时候LR_irq寄存器(因在IRQ模式下,所以是LR_irq)的值将调整为BL指令的下一条指令(ADD)地址,使其能从Foo()正确返回;但是因为这时候发生了中断请求,接下来要进行新中断的响应,处理器在新中断响应过程中也要进行LR_irq保存。这次对LR_irq的操作发生了冲突,当新中断返回后,往下执行STMFD指令,这时候压栈的LR已不是原来的ADD指令地址,从而使子程序Foo()无法正确返回。这个问题无法通过增加额外的现场保护指令来解决。一个办法就是在重新使能中断之前改变处理器模式,也就是使上面程序的“BL Foo”指令不要运行在IRQ模式下。这样当新的中断发生时,就不会造成LR寄存器的冲突。考虑ARM的所有运行模式,采用SYSTEM模式是比较合适的,因为它是特权模式,不是IRQ模式,与中断响应无关。
下面的例子显示了标准的IRQ/FIQ异常中断处理程序。
PRESERVE8
AREA INTERRUPT, CODE, READONLY
IMPORT C_irq_handler
IRQ
SUB lr, lr, #4 ;跳转返回地址
STMFD sp!, {lr} ;保存返回地址
MRS r14, SPSR ;读取 SPSR
STMFD sp!, {r12, r14} ;保存寄存器
; 清除中断源
MSR CPSR_c, #0x1F ;切换到 SYSTEM 模式,
STMFD sp!, {r0-r3, lr} ;保存 lr_USR 和其他使用到的寄存器
BL C_irq_handler ;跳转到 C 中断处理函数
LDMFD sp!, {r0-r3, lr} ;恢复用户模式寄存器
MSR CPSR_c, #0x92 ;切换回 irq 模式
LDMFD sp!, {r12, r14}
MSR SPSR_cf, r14
LDMFD sp!, {pc}^
END
SWI 异常处理函数的设计
本小节主要介绍编写 SWI 处理程序时需要注意的几个问题,包括下面内容。
• 判断 SWI 中断号。
• 使用汇编语言编写 SWI 异常处理函数。
• 使用 C 语言编写 SWI 异常处理函数。
• 在特权模式下使用 SWI 异常中断处理。
• 从应用程序中调用 SWI。
• 从应用程序中动态调用 SWI。
1.判断 SWI 中断号
当发生SWI异常,进入异常处理程序时,异常处理程序必须提取SWI中断号,从而得到用户请求的特定SWI功能。在SWI指令的编码格式中,后24位称为指令的“comment field”。该域保存的24位数(Thumb指令为8位),即为SWI指令的中断号,SWI的指令格式如下图所示。
第一级的SWI处理函数通过LR寄存器内容得到SWI指令地址,并从存储器中得到 SWI指令编码。通常这些工作通过汇编语言、内嵌汇编来完成。
下面的例子显示了提取中断向量号的标准过程。
PRESERVE8
AREA TopLevelSwi, CODE, READONLY ;第一级 SWI 处理函数.
EXPORT SWI_Handler
SWI_Handler
STMFD sp!,{r0-r12,lr} ;保存寄存器
LDR r0,[lr,#-4] ;计算 SWI 指令地址.
BIC r0,r0,#0xff000000 ;提取指令编码的后 24 位
;
; 提取出的中断号放 r0 寄存器,函数返回
;
LDMFD sp!, {r0-r12,pc}^ ;恢复寄存器
END
例子中,使用LR-4得到SWI指令的地址,再通过“BIC r0, r0, #0xFF000000”指令提取SWI 指令中断号。
2.汇编语言编写SWI异常处理函数
最简单的方法是利用得到的中断向量号,使用跳转表直接跳转到实现相应 SWI 功能的处理程序。
下面的例子,使用汇编语言实现了这种跳转。
CMP r0,#MaxSWI ;中断向量范围检测
LDRLS pc, [pc,r0,LSL #2]
B SWIOutOfRange
SWIJumpTable
DCD SWInum0
DCD SWInum1
; 使用 DCD 定义各功能函数入口地址
SWInum0 ;0 号中断
B EndofSWI
SWInum1 ;1 号中断
B EndofSWI
;
EndofSWI
3.使用C语言编写SWI异常处理函数
虽然第一级SWI处理函数(完成中断向量号的提取)必须用汇编语言完成,但第二级中断处理函数(根据提取的中断向量号,跳转到具体处理函数)就可以使用 C 语言来完成。因为第一级的中断处理函数已经将中断号提取到寄存器r0 中,所以根据AAPCS函数调用规则,可以直接使用BL指令跳转到C语言函数,而且中断向量号作为第一个参数被传递到C函数。
例如汇编中使用了“BL C_SWI_Handler”跳转到C语言的第二级处理函数,则第二级的 C 语言函数示例如下所示。
void C_SWI_handler (unsigned number)
{
switch (number)
{
case 0 : /* SWI number 0 code */
break;
case 1 : /* SWI number 1 code */
break;
...
default : /* Unknown SWI - report error */
}
}
另外,如果需要传递的参数多于1个,那么可以使用堆栈,将堆栈指针作为函数的参数传递给C类型的二级中断处理程序,就可以实现在两级中断之间传递多个参数。
例如:
MOV r1, sp ;将传递的第二个参数(堆栈指针)放到 r1 中
BL C_SWI_Handler ;调用C函数
相应的C函数的入口变为:
void C_SWI_handler(unsigned number, unsigned *reg)
同时,C函数也可以通过堆栈返回操作的结果
4.在特权模式下使用 SWI 异常处理
在特权模式下使用SWI异常处理,和IRQ/FIQ 中断嵌套基本类似。当执行 SWI 指令后,处理器执行下面操作。
① 处理器进入特权模式。
② 将程序状态字内容CPSR保存到SPSR_svc。
③ 返回地址放入LR_svc。
如果处理器已经处于特权模式,再发生SWI异常,则LR_svc和SPSR_svc寄存器的值将丢失。
所以在特权模式下,调用SWI软中断异常,必须先将LR_svc和SPSR_svc寄存器的值压栈保护。下面的例子显示了一个可以在特权模式下调用的SWI处理函数。
AREA SWI_Area, CODE, READONLY
PRESERVE8
EXPORT SWI_Handler
IMPORT C_SWI_Handler
T_bit EQU 0x20
SWI_Handler
STMFD sp!,{r0-r3,r12,lr} ;寄存器压栈保护
MOV r1, sp ;堆栈指针放 r1 作为参数传递.
MRS r0, spsr ;读取 spsr.
STMFD sp!, {r0, r3} ;将 spsr 压栈保护
;
;
LDR r0,[lr,#-4] ;计算 SWI 指令地址.
BIC r0,r0,#0xFF000000 ;读取 SWI 中断向量号.
; r0 存放中断向量号
; r1 堆栈指针
BL C_SWI_Handler ;调用 C 程序的 SWI 处理函数.
LDMFD sp!, {r0, r3} ;从堆栈中读取 spsr.
MSR spsr_cf, r0 ;恢复 spcr
LDMFD sp!, {r0-r3,r12,pc}^ ;恢复其他寄存器并返回.
END