3.4 异常中断处理
异常或中断是用户程序中最基本的一种执行流程和形态。这部分主要对ARM架构下的异常中断做详细说明。
ARM有7种类型的异常,按优先级从高到低的排列如下:复位异常(Reset)、数据异常(Data Abort)、快速中断异常(FIQ)、外部中断异常(IRQ)、预取异常(Prefetch Abort)、软件中断(SWI)和未定义指令异常(Undefined instruction)。
注意 |
在ARM文档中,使用术语Exception来描述异常。Exception主要是从处理器被动接受异常的角度出发,而Interrupt带有向处理器主动申请的色彩。在本书中,对“异常”和“中断”不做严格区分,两者都是指请求处理器打断正常的程序执行流程,进入特定程序循环的一种机制。 |
3.4.1 异常种类
ARM体系结构中,存在7种异常处理。当异常发生时,处理器会把PC设置为一个特定的存储器地址。这一地址放在被称为向量表(vector table)的特定地址范围内。向量表的入口是一些跳转指令,跳转到专门处理某个异常或中断的子程序。
存储器映射地址0x00000000是为向量表(一组32位字)保留的。在有些处理器中,向量表可以选择定位在存储空间的高地址(从偏移量0xffff0000开始)。一些嵌入式操作系统,如Linux和Windows CE就要利用这一特性。
表3.4列出了ARM的7种异常。
表3.4 ARM的7种异常
异 常 类 型 |
处理器模式 |
执行低地址 |
执行高地址 |
复位异常(Reset) |
特权模式 |
0x00000000 |
0xFFFF0000 |
未定义指令异常(Undefined interrupt) |
未定义指令中止模式 |
0x00000004 |
0xFFFF0004 |
软中断异常(Software Abort) |
特权模式 |
0x00000008 |
0xFFFF0008 |
预取异常(Prefetch Abort) |
数据访问中止模式 |
0x0000000C |
0xFFFF000C |
数据异常(Data Abort) |
数据访问中止模式 |
0x00000010 |
0xFFFF0010 |
外部中断请求IRQ |
外部中断请求模式 |
0x00000018 |
0xFFFF0018 |
快速中断请求FIQ |
快速中断请求模式 |
0x0000001C |
0xFFFF001C |
异常处理向量表如图3.5所示。
当异常发生时,分组寄存器r14和SPSR用于保存处理器状态,操作伪指令如下。
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
图3.5 异常处理向量表
异常返回时,SPSR内容恢复到CPSR,连接寄存器r14的内容恢复到程序计数器PC。
1.复位异常
当处理器的复位引脚有效时,系统产生复位异常中断,程序跳转到复位异常中断处理程序处执行。复位异常中断通常用在下面两种情况下。
· 系统上电。
· 系统复位。
当复位异常时,系统执行下列伪操作。
R14_svc = UNPREDICTABLE value
SPSR_svc = UNPREDICTABLE value
CPSR[4∶0] = 0b10011 /*进入特权模式*/
CPSR[5] = 0 /*处理器进入ARM状态*/
CPSR[6] = 1 /*禁止快速中断*/
CPSR[7] = 1 /*禁止外设中断*/
If high vectors configured then
PC = 0xffff0000
Else
PC = 0x00000000
复位异常中断处理程序将进行一些初始化工作,内容与具体系统相关。下面是复位异常中断处理程序的主要功能。
· 设置异常中断向量表。
· 初始化数据栈和寄存器。
· 初始化存储系统,如系统中的MMU等。
· 初始化关键的I/O设备。
· 使能中断。
· 处理器切换到合适的模式。
· 初始化C变量,跳转到应用程序执行。
2.未定义指令异常
当ARM处理器执行协处理器指令时,它必须等待一个外部协处理器应答后,才能真正执行这条指令。若协处理器没有相应,则发生未定义指令异常。
未定义指令异常可用于在没有物理协处理器的系统上,对协处理器进行软件仿真,或通过软件仿真实现指令集扩展。例如,在一个不包含浮点运算的系统中,CPU遇到浮点运算指令时,将发生未定义指令异常中断,在该未定义指令异常中断的处理程序中可以通过其他指令序列仿真浮点运算指令。
仿真功能可以通过下面步骤实现。
① 将仿真程序入口地址链接到向量表中未定义指令异常中断入口处(0x00000004或0xffff0004),并保存原来的中断处理程序。
② 读取该未定义指令的bits[27∶24],判断其是否是一条协处理器指令。如果bits[27∶24]值为0b1110或0b110x,该指令是一条协处理器指令;否则,由软件仿真实现协处理器功能,可以同过bits[11∶8]来判断要仿真的协处理器功能(类似于SWI异常实现机制)。
③ 如果不仿真该未定义指令,程序跳转到原来的未定义指令异常中断的中断处理程序执行。
当未定义异常发生时,系统执行下列的伪操作。
r14_und = address of next instruction after the undefined instruction
SPSR_und = CPSR
CPSR[4∶0] = 0b11011 /*进入未定义指令模式*/
CPSR[5] = 0 /*处理器进入ARM状态*/
/*CPSR[6]保持不变*/
CPSR[7] = 1 /*禁止外设中断*/
If high vectors configured then
PC = 0xffff0004
Else
PC = 0x00000004
3.软中断SWI
软中断异常发生时,处理器进入特权模式,执行一些特权模式下的操作系统功能。软中断异常发生时,处理器执行下列伪操作。
r14_svc = address of next instruction after the SWI instruction
SPSR_und = CPSR
CPSR[4∶0] = 0b10011 /*进入特权模式*/
CPSR[5] = 0 /*处理器进入ARM状态*/
/*CPSR[6]保持不变*/
CPSR[7] = 1 /*禁止外设中断*/
If high vectors configured then
PC = 0xffff0008
Else
PC = 0x00000008
4.预取指令异常
预取指令异常使由系统存储器报告的。当处理器试图去取一条被标记为预取无效的指令时,发生预取异常。
如果系统中不包含MMU时,指令预取异常中断处理程序只是简单地报告错误并退出。若包含MMU,引起异常的指令的物理地址被存储到内存中。
预取异常发生时,处理器执行下列伪操作。
r14_svc = address of the aborted instruction + 4
SPSR_und = CPSR
CPSR[4∶0] = 0b10111 /*进入特权模式*/
CPSR[5] = 0 /*处理器进入ARM状态*/
/*CPSR[6]保持不变*/
CPSR[7] = 1 /*禁止外设中断*/
If high vectors configured then
PC = 0xffff000C
Else
PC = 0x0000000C
5.数据访问中止异常
数据访问中止异常是由存储器发出数据中止信号,它由存储器访问指令Load/Store产生。当数据访问指令的目标地址不存在或者该地址不允许当前指令访问时,处理器产生数据访问中止异常。
当数据访问中止异常发生时,处理器执行下列伪操作。
r14_abt = address of the aborted instruction + 8
SPSR_abt = CPSR
CPSR[4∶0] = 0b10111
CPSR[5] = 0
/*CPSR[6]保持不变*/
CPSR[7] = 1 /*禁止外设中断*/
If high vectors configured then
PC = 0xffff000C10
Else
PC = 0x00000010
当数据访问中止异常发生时,寄存器的值将根据以下规则进行修改。
① 返回地址寄存器r14的值只与发生数据异常的指令地址有关,与PC值无关。
② 如果指令中没有指定基址寄存器回写,则基址寄存器的值不变。
③ 如果指令中指定了基址寄存器回写,则寄存器的值和具体芯片的Abort Models有关,由芯片的生产商指定。
④ 如果指令只加载一个通用寄存器的值,则通用寄存器的值不变。
⑤ 如果是批量加载指令,则寄存器中的值是不可预知的值。
⑥ 如果指令加载协处理器寄存器的值,则被加载寄存器的值不可预知。
6.外部中断IRQ
当处理器的外部中断请求引脚有效,而且CPSR寄存器的I控制位被清除时,处理器产生外部中断IRQ异常。系统中各外部设备通常通过该异常中断请求处理器服务。
当外部中断IRQ发生时,处理器执行下列伪操作。
r14_irq = address of next instruction to be executed + 4
SPSR_irq = CPSR
CPSR[4∶0] = 0b10010 /*进入特权模式*/
CPSR[5] = 0 /*处理器进入ARM状态*/
/*CPSR[6]保持不变*/
CPSR[7] = 1 /*禁止外设中断*/
If high vectors configured then
PC = 0xffff0018
Else
PC = 0x00000018
7.快速中断FIQ
当处理器的快速中断请求引脚有效且CPSR寄存器的F控制位被清除时,处理器产生快速中断请求FIQ异常。
当快速中断异常发生时,处理器执行下列伪操作。
r14_fiq = address of next instruction to be executed + 4
SPSR_fiq = CPSR
CPSR[4∶0] = 0b10001 /*进入FIQ模式*/
CPSR[5] = 0
CPSR[6] = 1
CPSR[7] = 1
If high vectors configured then
PC= 0xffff001c
Else
PC = 0x0000001c
3.4.2 异常优先级
每一种异常按表3.5中设置的优先级得到处理。
表3.5 异常优先级
优 先 级 |
异 常 |
最高 1 |
复位异常 |
2 |
数据中止 |
3 |
快速中断请求 |
4 |
中断请求 |
5 |
预取指令异常 |
6 |
软件中断 |
最低 7 |
未定义指令 |
异常可以同时发生,处理器按表3.5的优先级顺序处理异常。例如,复位异常的优先级最高,处理器上电时发生复位异常。所以当产生复位时,它将优先于其他异常得到处理。同样,当一个数据访问中止异常发生时,它将优先于除复位异常外的其他所有异常。
优先级最低的2种异常是软件中断和未定义指令异常。因为正在执行的指令不可能既是一条SWI指令,又是一条未定义指令,所以软件中断异常SWI和未定义指令异享有相同的优先级。
3.4.3 处理器模式和异常
每一种异常都会导致内核进入一种特定的模式。表3.6显示了ARM处理器异常及其对应的模式。此外,也可以通过编程改变CPSR,进入任何一种ARM处理器模式。
注意 |
用户和系统模式是仅有的不可通过异常进入的两种模式,也就是说,要进入这两种模式,必须通过编程改变CPSR。 |
表3.6 ARM处理器异常及其对应模式
异 常 |
模 式 |
用 途 |
快速中断请求 |
FIQ |
进行快速中断请求处理 |
外部中断请求 |
IRQ |
进行外部中断请求处理 |
SWI |
SVC |
进行操作系统的高级处理 |
复位 |
SVC |
进行操作系统的高级处理 |
预取指令中止异常 |
ABORT |
虚存和存储器保护 |
数据中止异常 |
ABORT |
虚存和存储器保护 |
未定义指令 |
Undefined |
软件模拟硬件协处理器 |
3.4.4 异常响应流程
1.判断处理器状态
当异常发生时,处理器自动切换到ARM状态,所以在异常处理函数中要判断在异常发生前处理器是ARM状态还是Thumb状态。这可以通过检测SPSR的T位来判断。
通常情况下,只有在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号软中断处理函数
:
2.向量表
如前面介绍向量表时提到的,每一个异常发生时总是从异常向量表开始跳转。最简单的一种情况是向量表里面的每一条指令直接跳向对应的异常处理函数。其中快速中断处理函数FIQ_handler()可以直接从地址0x1C处开始,省下一条跳转指令,如图3.6所示。
图3.6 异常处理向量表
但跳转指令B的跳转范围为±32MB,但很多情况下不能保证所有的异常处理函数都定位在向量的32MB范围内,需要更大范围的跳转,而且由于向量表空间的限制,只能由一条指令完成。具体实现方法有下面两种。
(1)MOV PC,#imme_value
这种办法将目标地址直接赋值给PC。但这种方法受格式限制不能处理任意立即数。这个立即数由一个8位数值循环右移偶数位得到。
(2)LDR PC,[PC+offset]
把目标地址先存储在某一个合适的地址空间,然后把这个存储器单元的32位数据传送给PC来实现跳转。
这种方法对目标地址值没有要求。但是存储目标地址的存储器单元必须在当前指令的±4KB空间范围内。
注意 |
在计算指令中引用offset数值的时候,要考虑处理器流水线中指令预取对PC值的影响。 |
3.4.5 从异常处理程序中返回
当一个异常处理返回时,一共有3件事情需要处理:通用寄存器的恢复、状态寄存器的恢复以及PC指针的恢复。通用寄存器的恢复采用一般的堆栈操作指令即可,下面重点介绍状态寄存器的恢复以及PC指针的恢复。
1.恢复被中断程序的处理器状态
PC和CPSR的恢复可以通过一条指令来实现,下面是3个例子。
· MOVS PC,LR
· SUBS PC,LR,#4
· LDMFD SP!,{PC}^
这几条指令是普通的数据处理指令,特殊之处在于它们把程序计数器寄存器PC作为目标寄存器,并且带了特殊的后缀“S”或“^”。其中“S”或“^”的作用就是使指令在执行时,同时完成从SPSR到CPSR的拷贝,达到恢复状态寄存器的目的。
2.异常的返回地址
异常返回时,另一个非常重要的问题就是返回地址的确定。前面提到过,处理器进入异常时会有一个保存LR的动作,但是该保持值并不一定是正确中断的返回地址。以一个简单的指令执行流水状态图来对此加以说明,如图3.7所示。
图3.7 3级流水线示例
在ARM架构里,PC值指向当前执行指令地址加8。也就是说,当执行指令A(地址0x8000)时,PC等于0x8000+8=0x8008,即等于指令C的地址。假设指令A是BL指令,则当执行时,会把PC值(0x8008)保存到LR寄存器。但是,接下来处理器会对LR进行一次自动调整,使LR=LR-0x4。所以,最终保存在LR里面的是图3.5中所示的B指令地址。所以当从BL返回时,LR里面正好是正确的返回地址。
同样的跳转机制在所有的LR自动保存操作中都存在。当进入中断响应时,处理器对保存的LR也进行一次自动调整,并且跳转动作也是LR=LR-0x04。由此,就可以对不同异常类型的返回地址依次比较。
假设在指令B处(地址0x8004)发生了异常,进入异常相应后,LR经过跳转保存的地址值应该是C的地址0x8008。
(1)软中断异常
如果发生软中断异常,即指令B为SWI指令,从SWI中断返回后下一条执行指令就是C,正好是LR寄存器保存的地址,所以只有直接把LR恢复给PC即可。
(2)IRQ或FIQ异常
如果发生的是IRQ或FIQ异常,因为外部中断请求中断了正在执行的指令B,当中断返回后,需要重新回到B指令执行,也就是说,返回地址应该是B(0x8004),需要把LR减4送PC。
(3)Data Abort数据中止异常
在指令B处进入数据异常的相应,但导致数据异常的原因却应该是上一条指令A。当中断处理程序恢复数据异常后,要回到A重新执行导致数据异常的指令,因此返回地址应该是LR加8。
为方便起见,表3.7总结了各异常和返回地址的关系
表3.7 异常和返回地址
异 常 |
地 址 |
用 途 |
复位 |
- |
复位没有定义LR |
数据中止 |
LR-8 |
指向导致数据中止异常的指令 |
FIQ |
LR-4 |
指向发生异常时正在执行的指令 |
IRQ |
LR-4 |
指向发生异常时正在执行的指令 |
预取指令中止 |
LR-4 |
指向导致预取指令异常的那条指令 |
SWI |
LR |
执行SWI指令的下一条指令 |
未定义指令 |
LR |
指向未定义指令的下一条指令 |
3.4.6 在应用程序中安装异常处理程序
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}
注意 |
可以使用Scatter文件定义加载向量表的地址,这样上述代码的拷贝工作由C库函数完成。 |
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);
}
(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);
}
3.4.7 FIQ和IRQ中断处理函数的设计
1.中断分支
ARM内核只有两个外部中断输入信号nFIQ和nIRQ。但对于一个系统来说,中断源可能多达几十个。为此,在系统集成的时候,一般都会有一个异常控制器来处理异常信号,如图3.8所示。
图3.8 中断系统
这时候用户程序可能存在多个IRQ/FIQ的中断处理函数。为了使从向量表开始的跳转始终能找到正确的处理函数入口,需要设置一套处理机制和方法。
多数情况下是由软件来处理异常分支的,因为软件可以通过读取中断控制器来获得中断源的信息,如图3.9所示。
有些芯片可能支持特殊的硬件分支功能,这需要查看具体的芯片说明。
因为软件的灵活性,可以设计出比图3.9更好的流程控制方法,如图3.10所示。
Int_vector_table是用户自己开辟的一块存储器空间,里面按次序存放异常处理函数的地址。IRQ_Handler()从中断控制器获取中断源信息,然后再从Int_vector_table中的对应地址单元得到异常处理函数的入口地址,完成一次异常响应的跳转。这种方法的好处是用户程序在运行过程中,能够很方便地动态改变异常服务内容。
图3.9 软件控制中断分支
图3.10 灵活的软件分支设计
进入异常处理程序后,用户可以完全按照自己的意愿来进行程序设计,包括调用Thumb状态的函数等。但对于绝大多数的系统来说,有两个步骤必须处理,一是现场保护,二是要把中断控制器中对应的中断状态标识清除,表明该中断请求已经得到响应,否则,中断函数退出以后,又会被再一次触发,从而进入周而复始的死循环。
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
3.4.8 SWI异常处理函数的设计
本小节主要介绍编写SWI处理程序时需要注意的几个问题,包括下面内容。
· 判断SWI中断号。
· 使用汇编语言编写SWI异常处理函数。
· 使用C语言编写SWI异常处理函数。
· 在特权模式下使用SWI异常中断处理。
· 从应用程序中调用SWI。
· 从应用程序中动态调用SWI。
1.判断SWI中断号
当发生SWI异常,进入异常处理程序时,异常处理程序必须提取SWI中断号,从而得到用户请求的特定SWI功能。
在SWI指令的编码格式中,后24位称为指令的“comment field”。该域保存的24位数,即为SWI指令的中断号,如图3.11所示。
图3.11 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
5.从应用程序中调用SWI
可从汇编语言或 C/C++ 中调用 SWI。
(1)从汇编应用程序中调用SWI
从汇编语言程序中调用SWI,只要遵循AAPCS标准即可。调用前,设定所有必须的值并发出相关的 SWI。例如:
MOV r0, #65 ; 将软中断的子功能号放到r0中
SWI 0x0
注意 |
SWI指令和其他所以ARM指令一样,可以被条件执行。 |
(2)从C应用程序中调用SWI
在C或C++应用程序中调用SWI,要将C语言的子程序用编译器扩展_swi声明,例如:
__swi(0) void my_swi(int);
……
……
……
my_swi(65);
编译器扩展_swi确保了SWI以内联方式进行编译,而没有额外的开销。但有如下的AAPCS限制。
· 函数调用参数只能使用r0~r3传递。
· 函数返回值只能通过r0~r3传递。
向内联的SWI函数传递参数和向实际的子函数传递参数基本类似。但返回值的情况比较复杂。如果有两到四个返回值,则必须告诉编译程序返回值是以结构形式返回的,并使用__value_in_regs 伪操作声明。这是因为基于结构值的函数通常被处理为一个void(空)型函数,且第一个自变量必须为存放结果结构的地址。
下面的例子显示了对编号为0x0、0x1、0x2和0x3的SWI软中断的调用。其中,SWI0x0和SWI0x1传递两个整型参数并返回一个单一结果;SWI0x2传递4个参数并返回一个单一结果;而SWI0x3传递4个参数并通过结构体返回4个结果。
#include <stdio.h>
#include "swi.h"
unsigned *swi_vec = (unsigned *)0x08;
extern void SWI_Handler(void);
int main( void )
{
int result1, result2;
struct four_results res_3;
Install_Handler( (unsigned) SWI_Handler, swi_vec );
printf("result1 = multiply_two(2,4) = %dn", result1 = multiply_two(2,4));
printf("result2 = multiply_two(3,6) = %dn", result2 = multiply_two(3,6));
printf("add_two( result1, result2 ) = %dn", add_two( result1, result2 ));
printf("add_multiply_two(2,4,3,6) = %dn", add_multiply_two(2,4,3,6));
res_3 = many_operations( 12, 4, 3, 1 );
printf("res_3.a = %dn", res_3.a );
printf("res_3.b = %dn", res_3.b );
printf("res_3.c = %dn", res_3.c );
printf("res_3.d = %dn", res_3.d );
return 0;
}
__swi(0) int multiply_two(int, int);
__swi(1) int add_two(int, int);
__swi(2) int add_multiply_two(int, int, int, int);
struct four_results
{
int a;
int b;
int c;
int d;
};
__swi(3) __value_in_regs struct four_results many_operations(int, int, int, int);
(3)应用程序中动态调用SWI
在某些情形下,需要调用直到运行时才会知道其编号的 SWI。例如,当有很多相关操作可在同一目标上执行,并且每一个操作都有其自己的 SWI 时,就会发生这种情况。在此情况下,上一小节的方法不适用。
解决的方法有两种。
· 在运行时得到SWI功能号,然后构造出相应的SWI指令的编码,将该编码保存在某个存储单元中,将PC指针指向该单元,执行指令。
· 使用一个通用的SWI异常中断处理程序,将运行时需要调用的SWI功能号作为参数传递给该通用的SWI异常处理程序,通用的SWI异常中断处理程序根据参数值调用相应的SWI处理程序完成需要的操作。
通过汇编语言可以实现第二种解决办法:通过寄存器(通常为r0或r12)传递所需要的操作数,这样可以重新编写SWI处理程序,对相应寄存器中的值进行处理。
但有些情况下,为了节省程序开销,需要直接使用SWI中断号对程序调用。例如,操作系统可能会使用单一的一条SWI指令并用寄存器来传递所需运算的编号。这使得其他SWI空间可用于特定应用程序的SWI。在一个特定的应用程序中,如果从指令中提取SWI编号的开销太大,就可使用这个方法。ARM(0x123456)和Thumb(0xAB)半主机方式的SWI就是这样实现的。
下面的例子显示了如何使用_swi将C函数调用映射到半主机方式的SWI。
#ifdef __thumb
/* Thumb 状态的Semihosting软中断处理*/
#define SemiSWI 0xAB
#else
/* ARM状态下的Semihosting的软中断处理*/
#define SemiSWI 0x123456
#endif
/* 使用Semihosting软中断输出一个字符*/
__swi(SemiSWI) void Semihosting(unsigned op, char *c);
#define WriteC(c) Semihosting (0x3,c)
void write_a_character(int ch)
{
char tempch = ch;
WriteC( &tempch );
}
编译程序含有一个机制,用以支持使用r12来传递所需运算的值。根据AAPCS标准,r12为IP寄存器,并且专用于函数调用。其他时间内可将其用作暂存寄存器。如前面所述,通用SWI参数和返回值通过r0~r3寄存器传递。而r12可用于传递通用SWI调用的中断功能编号。
下面的例子显示了通用SWI的C语言程序框架。
__swi_indirect(0x80)
unsigned SWI_ManipulateObject(unsigned operationNumber,
unsigned object,unsigned parameter);
unsigned DoSelectedManipulation(unsigned object,
unsigned parameter, unsigned operation)
{
return SWI_ManipulateObject(operation, object, parameter);
}
生成的汇编代码如下。
DoSelectedManipulation PROC
STMFD sp!,{r3,lr}
MOV r12,r2
SWI 0x80
LDMFD sp!,{r3,pc}
ENDP