1、任務上下文
μCOS-II V2.83的任務上下文保存在任務的棧裏面,任務創建時,將任務的參數、運行地址(任務入口)、任務棧等信息保存在任務棧裏面,OS_TCB->OSTCBStkPtr指向任務棧頂(任務創建及掛起時,OS_TCB->OSTCBStkPtr即指向任務上下文)。
ARM棧是遞減棧(棧底地址高、棧頂地址低,地址由低到高爲:棧頂->棧數據->棧底)!
1.1、 任務創建時任務棧設置
以下爲os_cpu_c.c任務創建時任務棧初始化函數。
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
opt = opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
*(stk) = (OS_STK)task; /* Entry Point */
*(--stk) = (INT32U)0x14141414L; /* R14 (LR) (init value will cause fault if ever used) */
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
*(--stk) = (INT32U)0x00000013L; /* CPSR (Enable both IRQ and FIQ interrupts) */
return (stk);
}
從第一個*(--stk)看,是先將棧地址減一在存儲數據,因此該棧是個滿棧(即棧指針指向的地址有數據,前置遞減可以看出棧是個遞減棧,棧的增長是從高地址往低地址增長);入棧順序依次是{task, r14-r0, cpsr},task在內存高地址,cpsr在內存低地址。
棧中幾個關鍵的寄存器:r14(lr),這裏設置的是一個無意義的值,因此這種情況下程序結束時返回地址會異常(其他嵌入式實時系統會設置返回地址指向一個任務退出函數);r0任務運行參數指針;cpsr任務運行時的cpsr,這裏設置任務運行在SVC模式,另外任務運行時默認必須允許中斷,否則中斷處理器沒法處理任務調度定時器中斷信號。
寄存器在棧中的順序主要與上下文保存及恢復有關。
1.2、 第一個任務的運行
第一個任務是由main函數執行的,main函數沒有運行在任務上下文,操作系統運行之後也不會再返回到被中斷的main函數繼續執行,因此,第一個任務執行時沒有保存main函數上下文的必要。
.text
OSStartHighRdy:
#if (OS_TASK_SW_HOOK >= 1)
BL OS_TaskSwHook // OSTaskSwHook();
#endif
MSR CPSR_c, #(I_BIT | F_BIT | ARM_MODE_SVC) // Switch to SVC mode with IRQ and FIQ disabled
LDR R4, =OSRunning // OSRunning = TRUE;
MOV R5, #1
STRB R5, [R4]
LDR R0, =OSTCBCur // SWITCH TO HIGHEST PRIORITY TASK:
LDR R0, [R0] // Get stack pointer,
LDR SP, [R0] // Switch to the new stack,
LDMFD SP!, {R0} // Pop new task's CPSR,
MSR SPSR_cxsf, R0
LDMFD SP!, {R0-R12, LR, PC}^ // Pop new task's context.
OSStartHighRdy函數主要步驟是:
1、禁止中斷;
2、OSRunning = TRUE設置操作系統運行狀態(只有操作系統運行時,調度定時器纔有必要調用);
3、恢復任務棧;
獲取任務棧頂
LDR R0, =OSTCBCur // 獲取任務控制塊地址(彙編語言 "=變量名稱"用於獲取該變量的地址,等價於c語言的 r0 = &OSTCBCur)
LDR R0, [R0] // 獲取OSTCBStkPtr的地址(r0 = OSTCBCur)
LDR SP, [R0] // 獲取OSTCBStkPtr的值(即任務棧頂)(sp = OSTCBStkPtr->OSTCBStkPtr
任務棧恢復
LDMFD SP!, {R0} // 出棧(棧頂元素pop到r0,棧頂此時是任務創建時的cpsr)
MSR SPSR_cxsf, R0 // 任務的cpsr保存到spsr
LDMFD SP!, {R0-R12, LR, PC}^ // 寄存器出棧(1、ldmfd帶有pc及^標誌,指令會將spsr備份程序狀態寄存器的值恢復到cpsr程序狀態寄存器;2、r0-r12, lr, pc恢復,此次主要時將任務入口的參數恢復並將程序寄存器pc設置爲task入口函數指針;3、ldmfd之前的cpu已經切換到SVC模式,任務也是運行在SVC模式,因此sp寄存器並沒有在ldmfd恢復寄存器列表裏面,ldmfd sp!, ... 本身就是在操作任務的棧,任務切換前後的sp是同一個sp)
2、中斷處理
中斷處理主要保存任務/中斷上下文,恢復任務/中斷上下文。中斷處理彙編代碼如下:
/*
* IRQ interrupt
*/
.align 5
irq:
stmfd sp!, {r1-r3} // We will use r1-r3 as temporary registers
mov r1, sp // sp -> r1
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's cpsr)
/*
* save task's context onto old task's stack
*/
msr cpsr_cxsf, #(I_BIT | F_BIT | ARM_MODE_SVC) // Change to SVC mode
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 1f
ldr r4, =OSTCBCur // OSTCBCur->OSTCBStkPtr = sp;
ldr r5, [r4]
str sp, [r5] // }
1:
msr cpsr_c, #(I_BIT | F_BIT | ARM_MODE_IRQ) // Change to IRQ mode to use IRQ stack to handle interrupt
bl do_IRQ
msr cpsr_c, #(I_BIT | F_BIT | ARM_MODE_SVC) // 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
2.1、中斷上下文保存
stmfd sp!, {r1-r3} // 通用寄存器r1-r3保存到IRQ棧裏面(r1-r3將作爲臨時寄存器使用,即寄存器將被改變,sp = sp - 4 * 3)
mov r1, sp // r1保存IRQ模式的棧頂(中斷前任務/中斷處理程序r1-r3保存的地址)
add sp, sp, #12 // 恢復IRQ的棧指針(sp = sp + 4 * 3),保存的寄存器仍在內存中,只是移動了IRQ的棧指針而已,後續代碼不會操作IRQ的sp值,此處如果不恢復的話將會造成IRQ模式棧內存的泄露!
sub r2, lr, #4 // 中斷返回地址保存到r2中(中斷時,硬件會將中斷前的pc寄存器的值減去4然後保存到IRQ的lr寄存器裏面,中斷前的pc指向的是正在取指的指令地址,pc之前有正在指向的指令、正在譯碼的指令,pc - 8纔是正在執行指令的地址,正在執行的指令被中斷了,因此需要重新執行)
mrs r3, spsr // 中斷前的cpsr寄存器的值保存在spsr裏面,中斷前的cpsr保存到r3
目前已經保存的寄存器有,r1指向的內存(r1-r3),IRQ寄存器r2(pc)、r3(cpsr)。
2.2、任務/中斷處理上下文保存到任務棧
前一小結中的中斷前的部分寄存器保存到了IRQ的棧裏面,再中斷重入的情況下,會造成保存的上下文被覆蓋,因此IRQ棧裏面只是臨時保存了部分上下文數據而已,在再次允許中斷前,需要將IRQ棧裏面的數據保存到其他地方(任務的棧)。
msr cpsr_cxsf, #(I_BIT | F_BIT | ARM_MODE_SVC) // 切換到SVC模式並禁止中斷
stmfd sp!, {r2} // r2(pc)入棧(如同任務創建時一樣,棧底保存任務運行地址),此處已經是SVC模式了,因爲任務也運行在SVC模式,因此,此處操作的是任務的棧! 棧的內容爲: pc(低地址到高地址)
stmfd sp!, {r4-r12, lr} // {r4-r12, lr}入棧,棧的內容爲:r4-r12,lr,pc(低地址到高地址)
ldmfd r1!, {r4-r6} // r1指向IRQ棧保存上下文的地址,IRQ保存的棧數據(r1-r3)恢復到寄存器r4-r6
stmfd sp!, {r4-r6} // r4-r6(r1-r3)入棧,棧的內容爲:r1-r12,lr,pc(低地址到高地址)
stmfd sp!, {r0} // r0入棧(r0爲通用寄存器,在這之前的中斷處理一直沒有改變r0的值),棧的內容爲:r0-r12,lr,pc(低地址到高地址)
stmfd sp!, {r3} // cpsr入棧(r3爲通用寄存器,此前已經保存了中斷前的cpsr),棧的內容爲:cpsr,r0-r12,lr,pc(低地址到高地址)
至此,中斷前任務/中斷處理函數的所有寄存器都保存到了任務棧裏面了!
2.3、任務棧設置
中斷可能導致任務被重新調度,因此需要將任務上下文的地址保存到任務控制塊裏面,任務重新執行的時候纔可能找到上下文。
ldr r0, =OSIntNesting // 中斷嵌套計數器OSIntNesting加1
ldrb r1, [r0]
add r1, r1, #1
strb r1, [r0]
cmp r1, #1 // 判斷中斷嵌套計數器OSIntNesting是否爲1,如果爲1表示當前是第一次進入中斷,即中斷前是在執行操作系統的任務,需要更新被中斷任務的棧指針OSTCBStkPtr
bne 1f
ldr r4, =OSTCBCur // r4 = &OSTCBCur
ldr r5, [r4] // r5 = OSTCBCur
str sp, [r5] // OSTCBCur->OSTCBStkPtr = sp
2.4、中斷退出
(中斷處理的c函數略過,中斷處理c函數不會切換上下文,執行完成後會回到彙編代碼!)
bl OSIntExit // 調用中斷退出函數(操作系統調度定時器更新可能會喚醒某些等待超時的任務,當前任務優先級可能不再是最高優先級了)
// OSIntExit沒有執行任務切換(中斷嵌套/當前任務優先級最高),此處代碼將繼續執行,後續代碼僅恢復被中斷的任務/中斷處理函數(do_IRQ)
ldmfd sp!, {r4} // r4 = cpsr
msr spsr_cxsf, r4 // spsr = cpsr
ldmfd sp!, {r0-r12, lr, pc}^ // 從SVC棧恢復寄存器
3、任務搶佔
中斷處理函數除了調度定時器計數導致任務時間片用完、等到超時任務定時器超時外,其他硬件中斷處理也可能使某個睡眠任務被喚醒,因此當前任務可能被高優先級任務搶佔。OSIntExit在中斷退出時進行搶佔檢查,判斷是否有更高優先級任務。
3.1、OSIntExit中斷退出
主要是對中斷嵌套計數器OSIntNesting減1,如果是中斷嵌套,則返回中斷彙編代碼繼續處理,直到所有中斷退出,如果中斷都處理完成(OSIntNesting爲0),則調用OS_SchedNew更新就緒任務的最高優先級OSPrioHighRdy。
如果被中斷的任務的優先級最高,則返回中斷彙編代碼,中斷彙編代碼負責恢復中斷上下文;
如果有更高優先級的任務就緒,則更新OSTCBHighRdy、OSTCBHighRdy->OSTCBCtxSwCtr、OSCtxSwCtr,並調用OSIntCtxSw執行任務切換,OSIntCtxSw不同於任務主動睡眠的切換,OSIntCtxSw函數調用前已經保存了被切換出去的任務的上下文,因此OSIntCtxSw僅恢復下一任務的上下文即可。
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
if (OSRunning == OS_TRUE) {
OS_ENTER_CRITICAL();
if (OSIntNesting > 0) { /* Prevent OSIntNesting from wrapping */
OSIntNesting--;
}
if (OSIntNesting == 0) { /* Reschedule only if all ISRs complete ... */
if (OSLockNesting == 0) { /* ... and not locked. */
OS_SchedNew();
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
#if OS_TASK_PROFILE_EN > 0
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Keep track of the number of ctx switches */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
}
OS_EXIT_CRITICAL();
}
}
3.2、OSIntCtxSw任務上下文恢復
前面已經講了,中斷上下文已經保存,OSIntCtxSw僅恢復下一任務上下文。
OSIntCtxSw主要執行以下任務:
1、設置OSPrioCur、OSTCBCur,這兩個值用在不同的地方,OSPrioCur主要用於與最高就緒任務優先級比較,OSTCBCur主要用於任務上下文的切換及任務統計;
2、獲取下一任務的sp(棧頂保存了任務的上下文),恢復任務上下文。
OSIntCtxSw:
LDR R0, =OSPrioHighRdy // OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioCur
LDRB R0, [R0]
STRB R0, [R1]
LDR R0, =OSTCBHighRdy // OSTCBCur = OSTCBHighRdy;
LDR R1, =OSTCBCur
LDR R0, [R0]
STR R0, [R1]
LDR R0, =OSTCBHighRdy // SP = OSTCBHighRdy->task_stack;
LDR R0, [R0]
LDR SP, [R0]
LDMFD SP!, {R0} // RESTORE NEW TASK'S CONTEXT:
MSR SPSR_cxsf, R0 // Pop new task's CPSR,
LDMFD SP!, {R0-R12, LR, PC}^ // Pop new task's context.