在uc/os-II的移植过程中存在一个通用的irq中断处理函数,其中的实现过程如下:
-
OS_CPU_IRQ_ISR
-
STMFD SP!, {R1-R3} ; We will use R1-R3 as temporary registers
-
MOV R1, SP
-
ADD SP, SP, #12 ;Adjust IRQ stack pointer
-
SUB R2, LR, #4 ;Adjust PC for return address to task
-
MRS R3, SPSR ; Copy SPSR (Task CPSR)
-
MSR CPSR_cxsf, #SVCMODE|NOINT ;Change to SVC mode
-
; SAVE TASK''S CONTEXT ONTO OLD TASK''S STACK
-
STMFD SP!, {R2} ; Push task''s PC
-
STMFD SP!, {R4-R12, LR} ; Push task''s LR,R12-R4
-
LDMFD R1!, {R4-R6} ; Load Task''s R1-R3 from IRQ stack
-
STMFD SP!, {R4-R6} ; Push Task''s R1-R3 to SVC stack
-
STMFD SP!, {R0} ; Push Task''s R0 to SVC stack
-
STMFD SP!, {R3} ; Push task''s CPSR
-
-
LDR R0,=OSIntNesting ;OSIntNesting++
-
LDRB R1,[R0]
-
ADD R1,R1,#1
-
STRB R1,[R0]
-
-
CMP R1,#1 ;if(OSIntNesting==1){
-
BNE %F1
-
LDR R4,=OSTCBCur ;OSTCBHighRdy->OSTCBStkPtr=SP;
-
LDR R5,[R4]
-
STR SP,[R5] ;}
-
-
1 MSR CPSR_c,#IRQMODE|NOINT ;Change to IRQ mode to use IRQ stack to handle interrupt
-
-
LDR R0, =INTOFFSET
-
LDR R0, [R0]
-
-
LDR R1, IRQIsrVect
-
MOV LR, PC ; Save LR befor jump to the C function we need return back
-
LDR PC, [R1, R0, LSL #2] ; Call OS_CPU_IRQ_ISR_handler();
-
-
MSR CPSR_c,#SVCMODE|NOINT ;Change to SVC mode
-
BL OSIntExit ;Call OSIntExit
-
-
LDMFD SP!,{R4} ;POP the task''s CPSR
-
MSR SPSR_cxsf,R4
-
LDMFD SP!,{R0-R12,LR,PC}^ ;POP new Task''s context
-
-
IRQIsrVect DCD HandleEINT0
这个函数是irq中断的通用处理形式,我对其中的代码做一下简要的分析和讨论。
首先我需要简要的分析一下中断处理过程中我们应该完成的任务,首先cpu会自动完成一些操作,其中包括中断的关闭已经返回地址的保存,还有模式的切换等。我们程序员则需要完成一些寄存器的保存工作以及跳转到具体的处理函数中,最后完成返回控制。
需要注意的是,本文中的UC/OS-II任务都运行在SVC模式下,而不是SYS模式下。任务栈中保存的寄存器也是这种模式下对应的值。
我按照注释号对代码进行解释:
2、STMFD SP!, {R1-R3}; 主要是完成几个寄存器的压栈操作,为什么这么做呢?因为我们接下来将要使用这写寄存器。为什么需要呢?这是因为当前CPU工作的irq模式下,因此这里的SP并不是SVC模式下的SP指针,但是在UC/OS-II的移植过程中通常将各个任务工作在SVC模式下,同时还需要对寄存器的保存,在中断发生以后,需要保存所有的寄存器以及CPSR的值,而当前的SP并不是任务的栈,此时除了SP、R14的值发生了变化以外,其他的寄存器并没有发生变化。但是由于后面需要使用这些寄存器,因此需要压栈。
3、MOV R1, SP; 将R14_irq的值保存到R1中,这个SP的保存主要是为了通过这个值访问IRQ模式下的堆栈空间,实现对数据的访问。
4、ADD SP, SP, #12; 调整IRQ模式下的堆栈指针SP_irq,将这个指针指向IRQ堆栈的开始位置,方便下一次中断的处理操作。
5、SUB R2, LR, #4 ;这个操作主要是调整返回地址,了解异常返回地址的理解其中的含义,这时候的返回地址实质上就是任务的返回地址,也就是将来需要加载到PC中的值。
6、MRS R3, SPSR;因为发生了IRQ中断,此时CPU进入IRQ模式中,这时的SPSR_irq中保存了svc模式下的CPSR状态。而任务堆栈中保存的刚好是SVC模式下的状态寄存器,因此需要将SVC模式下的状态寄存器首先读出来,然后保存进任务的堆栈中,因此用R3来保存CPSR值。
7、MSR CPSR_cxsf, #SVCMODE|NOINT; 因为任务的堆栈空间位于SVC模式下,因此首先需要将CPU的状态切换到SVC模式下,然后进行任务情景的切换操作。
9、STMFD SP!, {R2}; 这时候的SP是指在SVC模式下的R13_svc。中断发生以后,SVC模式下的SP并没有发生改变,返回以后,该值仍然存在,仍然指向任务的栈顶位置。根据任务栈空间的分布,首先需要保存PC值,然后是R14-R0,CPSR的值,最后是保存SP到任务中,这种分布状态是和堆栈初始化过程的分布一致的。而通过调整的返回地址刚好就保存在了R2中,因此需要压栈保存PC值。
10、STMFD SP!, {R4-R12, LR};因为在之前的一系列操作中,并没有对R4-R12,R14的值进行破坏,因此可以直接进行压栈操作,实现任务堆栈中R4-R12,LR的保存操作。
接下来的几句代码是重点:实现了在对其他模式下堆栈空间的访问问题。
11、LDMFD R1!, {R4-R6};是指将R1地址处加载一些数据到R4-R6,这三个寄存器的值已经被压入栈中,对他们的修改并不会导致错误的产生,因此这三个寄存器实质上是作为中间地址。其中加载的顺序满足高地址对应高的高编号的寄存器值。R1我在前面就强调了用来访问IRQ模式堆栈空间的参考地址。从这个地址向上分别保存了压入栈中的R1-R3寄存器的值,也就是被中断任务R1-R3的值。
12、STMFD SP!, {R4-R6};也就是完成了对任务寄存器R1-R3的压栈操作。
13、STMFD SP!, {R0} ;前面的代码中并没有修改R0的值,因此其中的值仍然是任务的R0值,因此也需要压栈操作。通过上面的几句代码就完成了任务的R0-R15所有寄存器的保存操作。接下的就应该完成CPSR的保存。
14、STMFD SP!, {R3};当前的R3中保存了实际上是前面所说的SPCR_irq中的值,也就是SVC模式下的状态寄存器的值,因此可以认为就是完成了任务的状态寄存器的保存。
16-19、接下来的操作本应该是完成将SP的值保存到任务堆栈空间的,但是在UC/OS-II中存在一个全局变量OSIntNesting,它表明了中断嵌套的次数,因此需要对这个值进行一次加1操作。
21、接下来的操作就是判断是否在中断嵌套中,也就是对全局变量进行比较操作,如果这个值是1,则认为只有一个中断产生,如果不等于1,则认为实在中断嵌套中。
CMP R1,#1 ;if(OSIntNesting==1){
22、BNE %F1;如果是在中断嵌套中,则直接跳转到下面的中断处理函数中
23-25、
LDR R4,=OSTCBCur ;OSTCBHighRdy->OSTCBStkPtr=SP;
LDR R5,[R4]
STR SP,[R5]
说明是从任务到中断的过程,也就是只有一个中断产生,不是在中断嵌套中,这时就需要将SP的值保存到任务控制块中。 以上的操作也就完成了任务情景的保存操作,接下来的操作就应该是真正的中断处理函数啦。
27、1 MSR CPSR_c,#IRQMODE|NOINT;实质上是完成CPU模式的切换操作,进入到IRQ模式下。
接下来的实际处理过程就如前面两篇文章中讨论的中断处理过程。
29-30、
LDR R0, =INTOFFSET
LDR R0, [R0]
得到INTOFFSET的值,实际上就是得到偏移量,实质上就是中断号产生中断,通过这个寄存器可以快速的确定在二级向量表中该中断的向量位置,该向量表中就保存了对应中断处理函数的函数地址。
32、LDR R1, IRQIsrVect
43、IRQIsrVect DCD HandleEINT0
这两句说明了我前面的分析,IRQIsrVect实际上就是一个标号,其中存储了HandleEINT0,HandleEINT0又是我们IRQ中断向量的入口地址(前一篇文章已经说明),也就是说HandleEINT0是二级向量表的开始地址。因此此时R1中实际上就保存了HandleEINT0。
33、MOV LR, PC;就是完成简单的保存过程,这个过程实质上就是保存了函数调用的返回地址。
34、LDR PC, [R1, R0, LSL #2];这句代码的意义是将地址R1+R0*4处的内容加载到PC中,也就是实现函数的跳转,即函数的调用过程。其中R1=HandleEINT0,而R0恰好又是一个偏移量,每一个指针的空间是4个字节,那么R1+R0*4地址处刚好就是对应中断号的向量。其中就保存了对应中断函数的地址。因此PC就保存了这个调用函数的入口地址。也就是实现了处理函数调用过程。
35、 MSR CPSR_c,#SVCMODE|NOINT; 执行这句代码的前提就是被调用的函数执行完毕了,相关的入栈出栈操作也已经完成,恢复到了调用前的状态。此时需要将CPU的模式切换到SVC模式下。
36、BL OSIntExit ;这个操作完成了中断的切换,如果不是在中断嵌套中,那么最高优先级的任务就会被执行,进入最高优先的任务之后就不会再返回了,这是UC/OS-II中任务的特点,之后的代码也就不会执行了。这是特别需要注意的。但是如果任务处在中断嵌套中,那么OSIntExit只是减少中断嵌套的次数,并不完成其他的操作。那么这时候就需要恢复之前被中断的任务了,也就是需要完成任务堆栈的弹出操作。
39-41、
LDMFD SP!,{R4} ;POP the task''s CPSR
MSR SPSR_cxsf,R4
LDMFD SP!,{R0-R12,LR,PC}^
这几句代码的实现实质上是完成了在中断嵌套中时的任务切换操作。
讨论:
不知道我理解的对不对,我认为这段代码存在一定的问题,具体的问题如下,因为在中断嵌套中,CPU执行的肯定就是中断服务函数,此时的任务处于低优先级的,并不需要我们保存任务的信息。为什么这段代码能够运行的原因我认为主要是因为这种处理的方式是不可能导致中断嵌套问题产生的。因为我们在进入中断以后关闭了中断使能位,不会产生中断嵌套也就看不出问题所在。我认为如果在支持中断嵌套的CPU中,应该首先检测是否在中期嵌套中,如果在中断嵌套中,则不需要任务寄存器的保存,如果不在,则需要保存。
关闭中断的方式避免了中断嵌套产生的可能,这也说明一直需要保存任务的情景,使得这段代码是有效的。
总结:
在讨论ARM的移植过程中,我觉得首先应该搞清楚每一种情况下CPU的工作模式,同时搞清楚寄存器的特殊性,同时搞清楚中断处理的一般过程。