arm中断学习

ARM 85浏览

 基于这个文档作为注释和处理,地址http://download.csdn.net/detail/xzongyuan/7335877

我总结下:

所有的单片机或者ARM都是把中断向量表放在内存开头的,然后通过如下的跳转指令跳到IRQ中断请求函数:

b HandlerIRQ  ;handler for IRQ interrupt

这里的中断Handler其实只是个宏,如下:

HandlerIRQ HANDLER HandleIRQ

为啥要定义一个相似名字的宏?(注意IRQ前面那个词,前者的是handler多了个"r") 

原因是,在处理IRQ(中断请求)之前,需要保护现场,因此,定义如下的宏

MACRO
$HandlerLabel HANDLER $HandleLabel

$HandlerLabel
sub sp,sp,#4 			;decrement sp(to store jump address)
stmfd sp!,{r0}			 ;PUSH the work register to stack(lr does not push because it return to original address)
ldr r0,=$HandleLabel 		;load the address of HandleXXX to r0
ldr r0,[r0] 				;load the contents(service routine start address) of HandleXXX
str r0,[sp,#4] 				;store the contents(ISR) of HandleXXX to stack
ldmfd sp!,{r0,pc} 			;POP the work register and pc(jump to ISR)
MEND

这个宏的$HandlerLabel和$HandleLabel是个变量。 分别被HandlerIRQ HANDLER HandleIRQ对应位置的字符替代。

因此,实际上,HandlerIRQ的label下的代码实际是这样的(删除了多余的注释代码)

HandlerIRQ
sub sp,sp,#4
stmfd sp!,{r0}
ldr r0,=HandleIRQ
ldr r0,[r0]
str r0,[sp,#4]
ldmfd sp!,{r0,pc} 

注意上面的代码,有如下一行代码

ldr r0,=HandleIRQ 

这才是真正的中断请求Handle。其余的都是保护中断现场的代码——即把一些重要的寄存器压入堆栈。

接下来看具体的HandleIRQ地址

AREA RamData, DATA, READWRITE
_ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00
HandleReset # 4
HandleUndef # 4
HandleSWI # 4
HandlePabort # 4
HandleDabort # 4
HandleReserved # 4
HandleIRQ # 4 对应上面第二个标号

上面代码意思是,从_ISR_STARTADDRESS开始,为每个Handle分配4字节的空间。这里的Handlexxx说白了,就是函数指针。

那HandleIRQ又有什么用?它其实没什么用,真正的处理函数时IsrIRQ,因此,它的作用是同一名字,让人一看就知道是Handlexxx吧。这样,就要求把IsrIRQ的地址给HandleIRQ了,如下代码

; Setup IRQ handler
ldr r0,=HandleIRQ ;This routine is needed
ldr r1,=IsrIRQ ;if there is not ''subs pc,lr,#4'' at 0x18, 0x1c
str r1,[r0]

然后看看IsrIRQ的代码是怎样的:

IsrIRQ
sub sp,sp,#4 ;reserved for PC
stmfd sp!,{r8-r9}
ldr r9,=INTOFFSET
ldr r9,[r9]
ldr r8,=HandleEINT0
add r8,r8,r9,lsl #2
ldr r8,[r8]
str r8,[sp,#8]
ldmfd sp!,{r8-r9,pc}

注意上面的其中一行

ldr r8,=HandleEINT0

这时,我们需要区分一个概念,Handlexxx是用来区分7种不同的中断异常,而IRQ是其中的一种,“好像”用来处理外部中断,而外部中断一般有几十个,因此,单纯IsrIRQ是分辨不了那么多中断的。要利用偏移量offset。

看看HandleEINT0的定义

_ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00
HandleReset # 4
HandleUndef # 4
HandleSWI # 4
HandlePabort # 4
HandleDabort # 4
HandleReserved # 4
HandleIRQ # 4
HandleFIQ # 4
HandleEINT0 # 4
HandleEINT1 # 4
HandleEINT2 # 4
HandleEINT3 # 4
.......

前面几个是异常,后面的则是IRQ异常向量的存放地址。 HandleEINT0 就是所有IRQ中断向量表的入口,在这个地址上面,加上一个适当的偏移量,INTOFFSET ,那么我们知道现在,到底是哪个IRQ在申请中断了。这个方法通过IsrIRQ中的两行代码实现:

ldr r8,=HandleEINT0
add r8,r8,r9,lsl #2

外部中断源虽然有很多,EINT0、EINT1、EINT2、EINT3...,但因为他们之间的地址都是4字节递增,因此,只需要拿到EINT0的地址,然后加上偏移量就行了。

下面再看看,怎么跳转,继续看 IsrIRQ 里面就实现了跳转了
str r8,[sp,#8]
ldmfd sp!,{r8-r9,pc}
其实最核心就是这两句了,先查找到当前中断服务程序的地址,将他放到 R8 里面,然后出栈,弹出给PC那么PC很自然就跳到中断服务程序了。至于这里的堆栈问题又是一个非常棘手的,需要好好的参透ARM的中断架构,需要了解的可以自己仔细的阅读 《ARM体系结构与编程》里面说的很详细。我们这里的重点是研究怎么跳转。

***********上面分析的是汇编中的实现,下面讲的是C语言的实现,道理相同,不要以为是另外的知识*****************************
最后,我们看看在C代码中是怎么安装终端向量的,示例看 按键的外部中断,是怎么具体设置的,参看/src/keyscan.c 里面的
代码很简单,里面只有3个函数
KeyScan_Test 是按键测试的主函数
Key_ISR 是按键中断服务函数
在 KeyScan_Test里面,我们发现了有这么一句
pISR_EINT0 = pISR_EINT2 = pISR_EINT8_23 = (U32)Key_ISR;

Key_ISR就是上面提到的按键中断服务函数,函数的名字,代表的就是函数的地址!!!!
将中断服务函数的地址,注意了,是地址,这是一个 U32型的变量。送到几个变量,我们以pISR_EINT0 作为例子,查看头文
件定义,在 2440addr.h 里面找到

// Interrupt vector
#define pISR_EINT0 (*(unsigned *)(_ISR_STARTADDRESS+0x20))

注意上面pISR_EINT0,就相当于汇编中的中断向量表的一条数据

所以说到底,C语言中的代码:

pISR_EINT0 = (U32)Key_ISR;

完成的操作就是,将 Key_ISR 的地址存放到汇编中的向量表里面,相当于:

HandleEINT0 # 4

看到没有?_ISR_STARTADDRESS 不就是刚才说的那个异常向量的入口地址?加上一个 0x20 之后实际上指向的,就是
HandleEINT0 !!!这么说来,上面的意思就是,将 Key_ISR 处理函数的入口地址,送到 HandleEINT0 中。(你也可以把它送到HandleEINT1,HandleEINT2,HandleEINT3...中)
再来看 Key_ISR ,这是一个典型的服务程序,加了_irq 作为编译关键字,告诉编译器,这个函数是中断服务程序
得保存需要的寄存器,免得被破坏。具体可以参考 《ARM体系结构与编程》P283 页的描述。
static void __irq Key_ISR(void)
{
.......
}
加上 _irq 关键字之后,编译器就会处理好所有的保存动作了,并不需要多关心。但是这个是 ARM-CC 编译器的关键字,GCC
中并没有这个东西,所以GCC处理中断的时候最好还是自己保存一下。
 

所以,整个过程是:

HandlerIRQ=>HandleIRQ=>IsrIRQ=>HandleEINTxx(这个要在c代码中定义)=>Ker_ISR(开发者自己定义的函数)