本文感谢 郑星 朋友
2440支持IRQ(普通中断)和FIQ(快速中断)。2440有60个中断源,不支持中断嵌套。
CPU每执行一条指令都会检查CPSR寄存器,当发现I或F位被置1时,就进行中断处理。需要两次查表过程(为什么要查两次表??没有办法,ARM把所有的中断都归纳成一个IRQ中断异常和一个FIQ中断异常;第一次查表主要是查出是什么异常,可我们总要知道是这个中断异常中的什么中断呀!没办法还需要查第二次)。第一步跳入异常向量表:
地址 |
异常名称 |
指令 |
0x00 |
复位异常 |
B RestHandler |
0x04 |
未定义指令异常 |
B HandlerUndef |
0x08 |
软件中断异常 |
B HandlerSWI |
0x0C |
指令预取异常 |
B HandlerPabort |
0x10 |
数据预取异常 |
B HandlerDabort |
0x14 |
保留 |
|
0x18 |
IRQ中断异常 |
B HandlerIRQ |
0x1C |
FIQ中断异常 |
B HandlerFIQ |
如果S3C2440刚上电或者是复位,那么pc指针被硬件强制转换到0x00地址处,那么按照2440init.s中的指令“B ResetHandler”,pc会跳转到ResetHandler处继续执行程序。
我们以外部中断为例,当发生外部中断后,PC指针指向0x18,执行B HandlerIRQ 指令,接着跳转到HandlerIRQ标号处执行,S3C2440有很多的中断源,所以不可能把中断函数的地址直接赋给HandlerIRQ。这中间应该还有一个转换。就是根据不同的、具体的中断源,HandlerIRQ对应于不同的中断处理函数的地址。那么接下来看看HandlerIRQ标号后面的内容吧:
HandlerIRQ HANDLER HandleIRQ
很明显,这里还有一个宏定义在里面,要知道HANDLER的内容,我们可以在2440init.s中查到:
;下面这个宏是用于第一次查表过程的实现中断向量的重定向,如果你比较细心的话就是发现 ;在_ISR_STARTADDRESS=0x33FF_FF00里定义的第一级中断向量表是采用型如Handle***的方式的. ;而在程序的ENTRY处(程序开始处)采用的是b Handler***的方式. ;在这里Handler***就是通过HANDLER这个宏和Handle***建立联系的. ;这种方式的优点就是正真定义的向量数据在内存空间里,而不是在ENTRY处的ROM(FLASH)空间里, ;这样,我们就可以在程序里灵活的改动向量的数据了. ;======================================================= ;这段程序用于把中断服务程序的首地址装载到pc中,有人称之为“加载程序”。 ;本初始化程序定义了一个数据区(在文件最后),34个字空间,存放相应中断服务程序的首地址。每个字 ;空间都有一个标号,以Handle***命名。 ;在向量中断模式下使用“加载程序”来执行中断服务程序。 ;这里就必须讲一下向量中断模式和非向量中断模式的概念 ;向量中断模式是当cpu读取位于0x18处的IRQ中断指令的时候,系统自动读取对应于该中断源确定地址上的; ;指令取代0x18处的指令,通过跳转指令系统就直接跳转到对应地址 ;函数中 节省了中断处理时间提高了中断处理速度标 例如 ADC中断的向量地址为0xC0,则在0xC0处放如下 ;代码:ldr PC,=HandlerADC 当ADC中断产生的时候系统会 ;自动跳转到HandlerADC函数中 ;非向量中断模式处理方式是一种传统的中断处理方法,当系统产生中断的时候,系统将interrupt ;pending寄存器中对应标志位置位 然后跳转到位于0x18处的统一中断 ;函数中 该函数通过读取interrupt pending寄存器中对应标志位 来判断中断源 并根据优先级关系再跳到 ;对应中断源的处理代码中 MACRO ;宏定义的开始 $HandlerLabel HANDLER $HandleLabel $HandlerLabel ;标号 sub sp,sp,#4 ;(1)减少sp(用于存放转跳地址) stmfd sp!,{r0} ;(2)把工作寄存器压入栈(lr does not push because it return to original address) ldr r0,=$HandleLabel ;将HandleXXX的址址放入r0 ldr r0,[r0] ;把HandleXXX所指向的内容(也就是中断程序的入口)放入r0 str r0,[sp,#4] ;(3)把中断服务程序(ISR)压入栈 ldmfd sp!,{r0,pc} ;(4)用出栈的方式恢复r0的原值和为pc设定新值(也就完成了到ISR的转跳) MEND ;宏定义的结束
这个宏的作用其实就是在不改变任何寄存器的前提下,把pc指针指向$HandleLabel。这里是将PC指针直接由HandlerIRQ指向HandleIRQ。(在这里Handler***就是通过HANDLER这个宏和Handle***建立联系的.
;这种方式的优点就是正真定义的向量数据在内存空间里,而不是在ENTRY处的ROM(FLASH)空间里,
;这样,我们就可以在程序里灵活的改动向量的数据了.)
下面我们就得关注一下HandleIRQ这个标号了。它在2440init.s中设这样定义的:
^ _ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00 HandleReset #4 HandleUndef #4 HandleSWI #4 HandlePabort #4 HandleDabort #4 HandleReserved #4 HandleIRQ #4 HandleFIQ #4 ………
(其中 “^”表示MAP指令,“#”表示FIELD指令)
很简单,这里定义了一个内存地址块,首地址:_ISR_STARTADDRESS代表了地址为0x33FF_FF00的内存区域,每隔4个字节,定义一个标号。很很容易就找到了 HandleIRQ这个我们需要找的标号。那么它所代表的内存区域自然就是0x33FF_FF00+0x04*6的内存地址了(跳到此地址执行,这个过程由硬件自动完成)。那么接下来的工作就是要把真正的、具体的中断处理函数的地址赋给HandleIRQ了。这里大家先看一下下面的两端代码:
1):
; 进入C语言前的最后一步了,就是把我们用说查二级向量表 ; 的中断例程安装到一级向量表(异常向量表)里. ldr r0,=HandleIRQ ;This routine is needed ldr r1,=IsrIRQ ;if there isn't 'subs pc,lr,#4' at 0x18, 0x1c str r1,[r0]
(2):
这一段程序就是用来进行第二次查表的过程了.
;如果说第一次查表是由硬件来完成的,那这一次查表就是由软件来实现的了. IsrIRQ sub sp,sp,#4 ;给PC寄存器保留 reserved for PC stmfd sp!,{r8-r9} ;把r8-r9压入栈 ldr r9,=INTOFFSET ;把INTOFFSET的地址装入r9 INTOFFSET是一个内部的寄存器,存着中断的偏移 ldr r9,[r9] ;I_ISR ldr r8,=HandleEINT0 ;这就是我们第二个中断向量表的入口的,先装入r8 ;=================================================================================== ;哈哈,这查表方法够好了吧,r8(入口)+index*4(别望了一条指令是4 bytes的喔), ;这不就是我们要找的那一项了吗.找到了表项,下一步做什么?肯定先装入了! ;================================================================================== add r8,r8,r9,lsl #2 ;地址对齐,因为每个中断向量占4个字节,即isr = IvectTable + Offeset * 4 ldr r8,[r8] ;装入中断服务程序的入口 str r8,[sp,#8] ;把入口也入栈,准备用旧招 ldmfd sp!,{r8-r9,pc};施招,弹出栈,哈哈,顺便把r8弹出到PC了,跳转成功!
首先我们来看一下第(1)段程序,前面已经提到,此时的的pc已经指向了HandeIRQ所表示的内存了,但是现在该内存还是空的,pc跳转到这里也不能接着往下运行了。所以才有了第1段代码,它的作用就是给HandleIRQ安装句柄了,把IsrIRQ的入口地址填充到了HandeIRQ里面了。所以程序接着就会跳转到IsrIRQ处执行。也就是上面的第(2)段程序了。这段程序具体讲解我就不说了(2次查表),跟最上面的宏定义很类似,其实就是让PC跳转到另外一个地方(pc=HandleEINT0+INTOFFSET*4)。而那个地方正是真正的中断函数。那我们再来看看这个地址是怎么算出来的。首先HandleEINT0就是上面那个MAP定义里面的一个内存区域,有没有发现它是第一个中断源,紧接着它,就是其它各种类型的中断源了。而INTOFFSET则是S3C2440的一个特殊功能寄存器了,某个类型的中断发生了,它的值就会发生变化。而后面为什么要乘以4呢,因为文字池中定义的标号都是4字节的,其实是因为S3C2440中指针变量就是占据4个字节的,这个可以用sizeof(*p)来验证。所以此时pc指针就指向了另外一个地方,那就是刚才说的中断表了。而在c语言中,我们通常会有这样的中断函数句柄安装语句:pISR_EINTn = (unsigned int )key_interrupt;
这里的pISR_EINTn其实是有定义的,我们以pISR_EINT0为例,宏定义如下:
#define pISR_EINT0 (*(unsigned *)(_ISR_STARTADDRESS+0x20))
看这个地址,其实就是上面那个中断表里面的:
^ _ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00 ……0-7 HandleEINT0 # 4 HandleEINT1 # 4 HandleEINT2 # 4 HandleEINT3 # 4 ……
大家发现了吧,HandleEINT0的地址是不是就是 _ISR_STARTADDRESS+0X20,所以说c语言中我们写的中断函数安装句柄,就是这个作用。
好的,现在让我们来总结一下吧。(以外部中断0为例)
首先是在c语言的函数中,我们已经执行了这样一条语句pISR_EINTn = (unsigned int )key_interrupt;它的作用是把中断处理函数key_interrupt的地址赋给了中断表中的HandleEINT0。
然后当某个时刻,发生了外部中断0,那么pc指针被强制指向了0x18处,执行指令:
b HandlerIRQ
跳转到HandlerIRQ之后,执行如下代码:(已经把宏定义屏蔽)
HandlerIRQ sub sp,sp,#4 stmfd sp!,{r0} ldr r0,=HandleIRQ ldr r0,[r0] str r0,[sp,#4] ldmfd sp!,{r0,pc}
然后pc之争有指向了HandleIRQ这个内存区域。而又由于HandlerIRQ已经被安装了IsrIRQ的句柄,所以紧接着pc又跳转到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}
它的作用是让pc指针根据INTOFFSET的值跳转到中断向量表中的HandleEINT0处。而在此处已经被安装了c语言中的中断处理函数的句柄,所以pc又跳到了中断处理函数中区执行中断函数了。