关于ARM架构下ucos2任务切换函数OSCtxSw源码分析
关于ARM架构下ucos2任务切换函数OSCtxSw源码分析
看了很多博文和权威资料,终于搞清楚了ARM的任务切换机制,下面我就引用一些资料来解释 OSCtxSw 函数究竟是如何保护现场和完成任务切换的。
http://www.keil.com/dd/docs/datashts/arm/cortex_m3/r1p1/ddi0337e_cortex_m3_r1p1_trm.pdf
首先,我们看下《Cortex-M3权威指南.pdf》中第38页有关通用寄存器组和特殊功能寄存器组的内容,存储堆栈指针SP的寄存器R13有两个,但是同一时间只能有一个被看到,这也就是所谓的“Banked”寄存器:
以下切换代码取自上面网址提供的ARM手册5-26页 (取材于keil u4的帮助资料)
;Example Context Switch (Assumes Thread is already on PSP)
MRS r12, PSP ; Recover PSP into R12 ①
STMDB r12!, {r4-r11, LR} ; Push non-stack registers ②
LDR r0, =OldPSPValue ; Get pointer to old Thread Control Block ③
STR r12, [r0] ; Store SP into Thread Control Block ④
LDR r0, =NewPSPValue ; Get pointer to new Thread Control Block ⑤
LDR r12, [r0] ; Acquire new Process SP ⑥
LDMIA r12!, {r4-r11, LR} ; Restore non-stacked registers ⑦
MSR PSP, r12 ; Set PSP to R12 ⑧
BX lr ; Return back to Thread ⑨
①显然第一行汇编码,我们是将SP堆栈地址保存到R12寄存器中,即sp地址->R12.
②这里因为R12中已经是sp栈地址,因此我们是将R4-R11, LR,依次按递减4方式压入栈中,
假如此时sp栈地址为4000H,则栈空间如下所示,R12后的“!”号表示存储完成后同时更新R12寄存器,因此,经过递减后,当前R12值为3964H.
|
|
栈空间 |
|
R12 |
→ |
|
4000H |
|
|
LR |
3996H |
|
|
R11 |
3992H |
|
|
R10 |
3988H |
|
|
R9 |
3984H |
|
|
R8 |
3980H |
|
|
R7 |
3976H |
|
|
R6 |
3972H |
|
|
R5 |
3968H |
R12’ |
→ |
R4 |
3964H |
③然后我们将要保存老的堆栈地址OldPSPValue的寄存器的地址,加载到R0中。
④将此时R12中老的堆栈地址OldPSPValue保存到R0所指的该寄存器中,到此,我们就已经保护现场完毕,接下来我们需要切换新的现场。
⑤继续将保存有新的堆栈地址NewPSPValue 的寄存器地址,加载到R0中。
⑥将新的堆栈地址加载到R12中。
⑦弹栈操作,将原来某个现场的信息弹出到R4-R11, LR中,方式与之前压栈刚好相反,不再赘述。
⑧将此时R12中的新堆栈地址返回给PSP
⑨跳转并返回继续执行。
以上写得比较啰嗦,不知道大家是否明白了,如果有不清楚汇编指令的,请参见我的附件【ARM汇编指令手册.pdf】和《Cortex-M3权威指南.pdf》赚点小分,还请见谅,呵呵。
堆栈的初始化函数需要根据不同的处理器进行设置,关于堆栈函数的处理首先要明白几点:
1.在《Cortex-M3权威指南.pdf》的第35页有如下内容:
接下来我再分析下,为什么ucos-ii中的OSCtxSw 代码,似乎没有以上冗长的保护现场的代码,而是一段很简短的代码,以下是STM32平台下,ucos-ii中的源码:
OSCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常 (causes context switch)
LDR R5, =NVIC_PENDSVSET
STR R5, [R4]
POP {R4, R5}
BX LR
OSIntCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常 (causes context switch)
LDR R5, =NVIC_PENDSVSET
STR R5, [R4]
POP {R4, R5}
BX LR
NOP
其中NVIC_INT_CTRL的值即0xE000ED04(中断控制及状态器ICSR地址,见权威指南P131),而NVIC_PENDSVSET的值为0x10000000即第28位PENDSVSET写1,悬起PendSV中断,因此这段代码的功能其实就是在不破坏R4,R5内容的前提下,触发PendSV中断,其实就是利用中断来实现保护现场的功能,那么,为什么uCOS保护现场要兜这么大一个圈子呢,后面我会引入权威指南中的相关内容,解析这样做的好处,下面我们先来看看在PendSV中断中我们做了什么,以下仍然是STM32平台下,ucos-ii中的源码:
PendSV_Handler
CPSID I ; 首先我们在任务切换过程中关闭中断
MRS R0, PSP ;保存线程栈PSP内容到R0
CBZ R0, PendSV_Handler_Nosave ; 如果之前没有任务执行那么,我们跳过
;现场保护这个过程
SUBS R0, R0, #0x20 ; 这里有人可能有疑问了,为什么要减32,这是 ;搞什么?其实之前我也提过了进入异常服务例程 ; 时,自动压栈了R0-R3,R12,LR,PSR,PC刚好8个寄 ; 存器,共占8*4个字节的地址空间。我们将要保存 ; 的R4-R11就接在这后面,所以需要偏移。
STM R0, {R4-R11}
LDR R1, =OSTCBCur ; 你会发现OSTCBCur是从C代码中IMPORT引入的,
;这里就是那个指针变量的地址
LDR R1, [R1] ;取得指针OSTCBCur所指的OSTCB结构首地址
STR R0, [R1] ; OSTCB结构的第一个成员就是OSTCBStkPtr , 将 ;保存有我们的R4-R11内容的栈地址保存到 ;OSTCBStkPtr 中
PendSV_Handler_Nosave
PUSH {R14} ; 这段我们只是为了调用一下OSTaskSwHook函数
;这个函数是开放给我们钩取线程切换过程的
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ;这里我们开始切换到新的任务的内容,包含优先 ;级和OSTCB结构的切换
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy;
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ;我们将新的栈地址送到R0
LDM R0, {R4-R11} ;将R4-R11从栈弹出
ADDS R0, R0, #0x20 ;加回我们偏移的地址,这样在执行BX跳转指令 ;时,硬件能从正确的栈地址自动弹出 ;R0-R3,R12,LR,PSR,PC的内容
MSR PSP, R0 ; 将R0中的栈地址送到PSP线程栈
ORR LR, LR, #0x04 ; 这句是确保后面执行BX命令时使用的是线程栈 ;PSP,而不是主堆栈MSP
CPSIE I ;开总中断
BX LR ; 执行中断返回,此时R0-R3,R12,LR,PSR,PC自动 ;出栈
这里我要补充说一下关于“ORR LR, LR, #0x04 ”的内容,异常返回(BX跳转)时,硬件会根据LR的内容,POP相应的堆栈,这句保证了从PSP堆栈POP出R0等寄存器,以下引用一篇博文中的内容来说明原因http://blog.chinaunix.net/uid-26817832-id-3162243.html
产生异常时,两个值我们需要,一个是pc,一个是LR,通过LR找到栈
1、 如果LR=0xFFFFFFF9说明产生异常的时候使用的是MSP
2、 如果LR=0xFFFFFFFD明产生异常的时候使用的是PSP
由此我们看出变化的恰是0x04,其实我们并不关心此时LR的值,我们只是做个标记,保证返回时,能从PSP出栈,出栈后LR会自动获取到正确的值,并覆盖我们这个LR值。
好了,说完,以上的内容,现在我将引用《Cortex-M3权威指南.pdf》中P123页给出的为什么这么做的原因。
个人感觉权威指南这几张图说得十分透彻和直观了,我就不啰嗦了,此外当然还有一个原因,就是间接保存PC寄存器的内容了,因为部分ARM不支持直接访问PC寄存器的内容。
希望以上我说了这么多废话,引用这么多资料,有把我的理解解释清楚,因为这些也只是我学习的一些笔记,如果有理解不对的地方,还请大虾门指出,谢谢了。