在前一段时间分析了ARM异常处理机制的处理方式,分析了在异常产生以后CPU自动完成的相关处理以及程序员应该完成的基本操作。着重分析了异常代码的返回地址分析已经采用通用代码处理各种异常的可能性。
异常处理的基本过程如下:异常产生(在指令的临界中检测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
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
1、搞清楚ARM中的MACRO伪指令,这个伪指令就是我们在汇编中的宏定义,我们都知道宏的实现能够避免代码的重复型以及代码的可修复性。关于ARM汇编中的宏定义基本的形式如下:
MACRO
{$label} macroname {$parameter} {$parameter}…
Code
MEND
其中$label 宏指令被展开时,label可被替换为相应的符号,一般为一个标号
macroname 所定义的宏的名称
$parameter 宏指令的参数,当宏指令被展开时被替换成对应的值。
2、依据上面的定义我们可以知道当前这段代码定义了一个宏指令,HANDLER,其中标号为$HandlerLabel,参数为$HandleLabel
基本的实现代码分析如下:
sub sp,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,形式如下:
HandlerIRQ HANDLER HandleIRQ
2、HandlerIRQ HANDLER HandleIRQ
根据上面的宏定义,可以将这句代码进行扩展,得到如下的形式:
HandlerIRQ
sub sp,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
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}
还是一句一句的分析:
sub sp,sp,#4 ;为保存PC值预留一个栈区域,这个区域与上面的处理过程是异曲同工的。
stmfd sp!,{r8-r9} ;保存r8,r9中的值,因为接下来将使用这两个寄存器
ldr r9,=INTOFFSET; 这是一个伪指令操作,实质上是将寄存器INTOFFSET的地址加载到r9中。
ldr r9,[r9];得到寄存器中的值,这个寄存器中的值恰好保存了当前最高优先级中断的中断号(优先级是可以调节的,而中断号是一个固定值,因此选择中断号比较恰当),这样也就知道了具体是那个中断源产生了中断。
ldr r8,=HandleEINT0;这句的ldr是伪指令,意思是将标号的地址加载到r8中
add r8,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预留的区域中。
ldmfd sp!,{r8-r9,pc};这句也恰好验证了上面的分析,PC中的值恰好就是之前的sp+8处的内容,这样中断处理函数的地址就到了PC中。[page]
小结:
我们可以将ARM中采用2级向量表的形式实现异常的中断处理,其中第一级是CPU中定义好的向量表,也就是异常向量表。在这一级的向量表中,实现跳转到对应的异常公共处理函数,另外每一种异常问题都存在自己的子问题,这时候采用第二级的向量表就可以解决各种子问题。第一级的向量表一般来说都是CPU定义好的,而第二级向量表则是我们在程序设计中人工实现的。
5、那么又是如何得到C语言中的函数呢,实质上已经很简单了,具体的分析如下:
//S3c2440init.s
^ _ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00
HandleReset # 4
HandleUndef # 4
HandleSWI # 4
HandlePabort # 4
HandleDabort # 4
HandleReserved # 4
HandleIRQ # 4
HandleFIQ # 4
这边就可以看做第二级中断向量表
;@0x33FF_FF20
HandleEINT0 # 4
HandleEINT1 # 4
…
HandleUART0 # 4
….
HandleSPI1 # 4
HandleRTC # 4
HandleADC # 4
_ISR_STARTADDRESS在s3c2440中是一个具体的地址值,这个地址值可以在option.h中找到。因此依据这个值我们就可以知道我们的二级向量表的实际位置,这种处理的方式存在一定的巧妙性,同时中断地址的选择也需要我们恰当的设置。这里的“^” 其实就是 MAP ,这段程序的意思是,从 _ISR_STARTADDRESS 开始,预留一个变量,每个变量一个标号,预留的空间为 4个字节,也就是 32BIT,其实这里放的是真正的C写的处理函数的地址,说白了,就是函数指针,这样做就很灵活了。
//option.h
#define _ISR_STARTADDRESS 0x33ffff00
同时在s3c2440addr.h中又可以找到下面的定义:
//s3c2440addr.h
// Exception vector(异常向量,不是CPU的异常向量)
#define pISR_RESET (*(unsigned *)(_ISR_STARTADDRESS+0x0))
#define pISR_UNDEF (*(unsigned *)(_ISR_STARTADDRESS+0x4))
#define pISR_SWI (*(unsigned *)(_ISR_STARTADDRESS+0x8))
#define pISR_PABORT (*(unsigned *)(_ISR_STARTADDRESS+0xc))
#define pISR_DABORT (*(unsigned *)(_ISR_STARTADDRESS+0x10))
#define pISR_RESERVED (*(unsigned *)(_ISR_STARTADDRESS+0x14))
#define pISR_IRQ (*(unsigned *)(_ISR_STARTADDRESS+0x18))
#define pISR_FIQ (*(unsigned *)(_ISR_STARTADDRESS+0x1c))
// Interrupt vector(中断向量)
#define pISR_EINT0 (*(unsigned *)(_ISR_STARTADDRESS+0x20))
#define pISR_EINT1 (*(unsigned *)(_ISR_STARTADDRESS+0x24))
#define pISR_EINT2 (*(unsigned *)(_ISR_STARTADDRESS+0x28))
#define pISR_EINT3 (*(unsigned *)(_ISR_STARTADDRESS+0x2c))
#define pISR_EINT4_7 (*(unsigned *)(_ISR_STARTADDRESS+0x30))
…
#define pISR_SPI1 (*(unsigned *)(_ISR_STARTADDRESS+0x94))
#define pISR_RTC (*(unsigned *)(_ISR_STARTADDRESS+0x98))
#define pISR_ADC (*(unsigned *)(_ISR_STARTADDRESS+0x9c))
从上面的代码中我们可以知道pISR_EINT0之类的实质上就是一个地址,如果我们在这个地址中填充处理函数的地址值也就形成了函数指针,实际上只需要将函数名赋值给对应的中断向量即可。这样也就找到了适当的处理方式.基本的形式如下所示:
Void main()
{
…
pISR_EINT0 = (U32)Button_ISR;
…
While(1)
{
…
}
}
/*中断服务函数*/
static void _irq Button_ISR(void)
{
…
}
几个分析的比较清晰的网址可以去看看:
总结:
问题?在其中的代码中,我并没有看到返回地址的操作问题,我找了很多的代码,但是好像都不是特别的准确。也就是没有找到对应的 subs pc, lr, =0x04操作。
代码中经典的片段就是如何实现了代码的跳转问题:
sub sp,sp, #0x04;为保存PC值预留空间
stmfd sp!,{r0}; 保存需要使用到的寄存器值,需要使用多少,就压多少的堆栈
…//使用r0进行相关的操作
ldr r0,[r0];
str r0, [sp,#0x04]; //这个操作类似于函数调用中的问题
ldmfd sp!,{r0, pc};//出栈操作,实现了对PC值的赋值