S3C2440中断代码的深层次分析
异常处理的基本过程如下:异常产生(在指令的临界中检测CPU的状态,一般实质在这条指令被执行完成,但是还没有执行下一条指令之前检测)——>保存状态寄存器,切换状态寄存器,保存LR=PC-4,强制PC跳转到对应异常向量(以上的过程都是CPU自动完成)——》调整返回地址,在栈中保存寄存器,便于恢复寄存器的值——》异常处理函数——》退出异常。
中断处理机制的两种形式:
1、采用在中断向量中存储简单的跳转指令,跳转到异常处理函数中,但是这种方式存在的缺点就是跳转指令的范围是有局限性的。
2、采用更新PC值的方法进行,具体的实现形式是在另一个固定地址处(handle_addr)保存对应异常处理函数的地址,然后采用LDR PC [PC, offset],其中offset = handle_addr – vect – 0x08;这种机制只要保证选择的地址恰当就能实现不同距离的跳转。
以上的分析和处理在上一次中已经分析,这次分析中断的处理过程,中断只是异常的一种特殊情况,对异常的处理得到了好的理解,那么对中断的处理也就比较方便了。
在ARM内核中只支持IRQ和IFQ两种类型的中断,但是不同的厂商提供不同类型的中断控制器实现对中断的扩展,使得实际的芯片更加适合我们的使用。但是中断控制器的差别也使得不同厂商的中断处理也有差别,但是基本的思想是一致的。

S3C2440的中断控制器一个支持60种中断源,基本的实现如上图所示。基本的寄存器包括SRCPND、INTPND(有且仅有1bit会被置位,可以通过这个寄存器判断中断源,找出那个IRQ源发生中断)、INTMOD、INTMSK、PRIORITY(用来改变中断的优先级顺序,但是其中还是存在一些固有的顺序,具体的参看手册)、INTOFFSET(用来表示IRQ中INTPND的那个bit被置位,这样每一类的中断源都存在一个固定的偏移量,这个寄存器可以用来用来计算偏移量以及通过这个偏移量找到对应的中断处理函数地址存储位置等),当然也存在一些关于多个中断源构成的子中断寄存器,SUBSRCPND、INTSUBMSK。
在S3C2440的启动代码中描述了关于中断处理过程的基本过程和原理。
首先需要搞清楚下面的一个宏定义:
MACRO
$HandlerLabel HANDLER $HandleLabel
$HandlerLabel
subsp,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
1、搞清楚ARM中的MACRO伪指令,这个伪指令就是我们在汇编中的宏定义,我们都知道宏的实现能够避免代码的重复型以及代码的可修复性。关于ARM汇编中的宏定义基本的形式如下:
MACRO
{$label} macroname {$parameter} {$parameter}…
Code
MEND
其中$label 宏指令被展开时,label可被替换为相应的符号,一般为一个标号
macroname所定义的宏的名称
$parameter宏指令的参数,当宏指令被展开时被替换成对应的值。
2、依据上面的定义我们可以知道当前这段代码定义了一个宏指令,HANDLER,其中标号为$HandlerLabel,参数为$HandleLabel
基本的实现代码分析如下:
subsp,sp,#4;在栈中预留一个区域,用来保存PC的值
stmfd sp!,{r0};由于r0还需要被使用,因此需要被压栈
ldr r0,=$HandleLabel ;这里的ldr是一个伪指令,主要是将标号$HandleLabel的地址加载到r0中,这也是压栈r0的原因。
ldr r0,[r0] ;这是ARM的ldr指令,主要是将$HandleLabel对应地址中的内容加载到r0中。如果在$HandleLabel中保存的是一个中断处理函数的地址,那么只需要将这个值加载到PC即可实现了中断任务跳转,实际上这个过程就是采用了异常处理的第二种方式:
即加载PC的方式,而不是简单的跳转方式。
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)
这两句代码正是这段代码的精髓。基本形式如下:
str r0,[sp,#4],是指将r0的内容,也就是异常处理函数的地址保存到栈中的SP-4位置处,这个位置也恰好是之前sub sp,sp,#4;用来预留给保存PC值的位置,这时将异常处理函数的地址保存在这个地址处,接下来的ldmfd sp!,{r0,pc}刚好就是将栈中的内容加载到R0和PC中,这样也就实现了将异常处理函数地址加载到PC.实现了跳转过程。
高地址 | |
SP_0/SP_3 | … |
SP_1 | Handle_addr |
SP_2 | R0 |
低地址 |
从上面的分析可以知道这种中断处理的方式,并不是中断处理中的简单跳转方式(因为跳转范围的局限性)而是采用更新PC值的形式实现的。
接下来分析IRQ,这种在我们实际开发中使用比较多的中断形式进行分析。
首先可以发现存在:
1、b HandlerIRQ;handler for IRQ interrupt
这种情况下发生在中断产生过程中,是在IRQ向量中执行的,也就是在0x18处执行,其中HandlerIRQ实质上是一个标号,对应一个具体的地址。其中保存的内容就是对应IRQ处理函数的地址。但是在代码中只有一个HandlerIRQ,形式如下:
HandlerIRQHANDLER HandleIRQ
2、HandlerIRQ HANDLER HandleIRQ
根据上面的宏定义,可以将这句代码进行扩展,得到如下的形式:
HandlerIRQ
subsp,sp,#4
stmfd sp!,{r0}
ldr r0,= HandleIRQ
ldr r0,[r0]
str r0,[sp,#4]
ldmfd sp!,{r0,pc}
3、关于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]
其中可以看到,在HandleIRQ中保存的内容是IsrIRQ的地址,而IsrIRQ我们可以知道是一个中断服务函数,因为在写代码的过程中进程会遇到这个特殊字符__ISR,这段代码是在启动代码中执行的。
4、IsrIRQ实现问题
IsrIRQ
subsp,sp,#4 ;reserved for PC
stmfd sp!,{r8-r9}
ldr r9,=INTOFFSET
ldr r9,[r9]
ldr r8,=HandleEINT0
addr8,r8,r9,lsl #2
ldr r8,[r8]
str r8,[sp,#8]
ldmfd sp!,{r8-r9,pc}
还是一句一句的分析:
subsp,sp,#4 ;为保存PC值预留一个栈区域,这个区域与上面的处理过程是异曲同工的。
stmfd sp!,{r8-r9} ;保存r8,r9中的值,因为接下来将使用这两个寄存器
ldr r9,=INTOFFSET;这是一个伪指令操作,实质上是将寄存器INTOFFSET的地址加载到r9中。
ldr r9,[r9];得到寄存器中的值,这个寄存器中的值恰好保存了当前最高优先级中断的中断号(优先级是可以调节的,而中断号是一个固定值,因此选择中断号比较恰当),这样也就知道了具体是那个中断源产生了中断。
ldr r8,=HandleEINT0;这句的ldr是伪指令,意思是将标号的地址加载到r8中
addr8,r8,r9,lsl #2;从指令的意义分析:r8 = r8 + r9>>2 = r8+r9*4;
其实这两句结合一下S3C2440的中断资料就不难分析得出,因为HandleEINT0实质上是指存储外部中断0处理函数地址的地方,那么我们可以将这一块内存地址看做是一个IRQISR中断向量表,而EINT0恰好是中断优先级最高的中断,那么可以将这个地址HandleEINT0作为IRQ中断向量表的入口地址,其他中断号的地址,只需要通过偏移地址就能得到,由于指针的大小恰好为4个字节,因此得到的相应中断号的入口地址是
HandleEINT0 = HandleEINT0 + INTOFFSET*4,
这些地址中都保存了对应中断处理函数的函数地址。
ldr r8,[r8]是指将r8的内容加载到r8中,也就是将对应中断处理函数的地址加载到r8中。
str r8,[sp,#8];这句代码的作用实质上就是和上面的分析一样,也就是将r8的值保存到之前为PC预留的区域中。
评论