UCOSIII之STM32上下文切換理解
程序上下文(context)
上下文(context),指的是什么呢,個人可以理解為一個任務或者線程控制的一些變量及CPU的寄存器狀態,就是說任務被打斷執行以后還可以還原回來。所以上下文就指的是兩個操作,被打斷任務狀態的保存及就緒作務的還原。如果說一個任務的狀態完全恢復就指的是,其CPU寄存器的值都被還原,以及其操作的內存及其他外設的狀態是一致的,如果說其操作的內存被其他作務或線程改變,那這個就是多線程的一個資源共享及鎖定的問題。
STM32 CPU寄存器
CORTEX-M3總共19個寄存器
R0-R15,MSP,PSP,特殊寄存器(三個狀態寄存器合用一個32位的寄存器)
那接下來就看下UCOSIII中STM32是如何實現上下文切換的。
PENDSV中斷
在發生任務切換時,是由任務激活PENDSV中斷(cortex-m3專業用於OS的中斷),在中斷函數中進行上下文切換。同時這個PENDSV的中斷優先級設置的是最低優先級。
在發生中斷時,CORTEX-M3自動做了如下操作
1.入棧,把8個寄存器壓入棧,R0~R3 R12 LR PC xPSR按一定的順序入棧保存
自動入棧只是入了部分寄存器,並沒將所有的寄存器入棧,應該是考慮到效率跟STACK的使用效率,不過只要在編譯的時候,只用到入棧的幾個寄存器就不影響使用了,如果要使用到其他寄存器,那么由編譯器來負責PUSH與POP,就是C=>匯編時,編譯器要做的事情。
2.取向量:即取得中斷響應函數地址的PC
3.更新當前的寄存器,更新SP,LR,PC
CORTEX-M3在中斷中使用的都是MSP,因此如果任務代碼使用的PSP那么,SP內里會被更新為MSP,LR更新為EXC_RETRUN(系統自動生成),而不是函數的返回地址,不過中斷返回時也會用到這個被更新的LR,PC改為之前取向量所得到的值。
具體的PENDSV中斷操作
OS_CPU_PendSVHandler
;進入中斷,此時已保存8個寄存器到SP,任務切換時是使用的PSP,因此自動保存至PSP
CPSID I ; Prevent interruption during context switch
;關中斷
MRS R0, PSP ; PSP is process stack pointer
;PSP存入R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time
;第一次開始使用的時候直接跳轉,之后都不會跳轉
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
;保存R4~R11的寄存器至當前PSP
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
;更新PSP的地址為保存寄存器之后的值至當前OSTCBCurPtr
; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
;保存R14(LR),防止調用函數時被覆蓋
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
;LDR的偽指令
BLX R0
;調用OSTaskSwHook,鈎子函數,當然這里去掉也是不影響系統正常使用,只是少了一個功能
;調用OSTaskSwHook函數時,肯定也會用到一些寄存器,只是這些寄存器的PUSH與POP都由OSTaskSwHook自動來完成
POP {R14}
;還原R14,用於中斷返回
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
;更新當前READY的高優先級
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
;更新當前READY的高優先級任務塊指針
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
;R2還是之前的OSTCBCurPtr(更新之后的),不過這里的使用跟OS_TCB的定義有關,OS_TCB的定義是把StkPtr放在最前面,因此這里可以直接把任務的棧地址,直接以這種方式調進來。
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
;還原R4~R11
ADDS R0, R0, #0x20
;更新棧地址
MSR PSP, R0 ; Load PSP with new process SP
;更新PSP地址
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
;將LR相應置位,保證返回時使用的是PSP,不過這個話,只對第一次的任務調用有效
;因為默認棧用的是MSP,而從PSP因中斷切換為MSP是,中斷返回時會自動跳轉到PSP
CPSIE I
;開中斷
BX LR ; Exception return will restore remaining context
;中斷返回,返回根據當前的PSP來還原起始被PUSH的8個寄存器
END
下面是UCOS啟動時的代碼
手動將PSP置0,才會有上面OS_CPU_PendSVHandler_nosave跳轉只發生一次的情形
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OS_CPU_ExceptStkBase ; Initialize the MSP to the OS_CPU_ExceptStkBase
LDR R1, [R0]
MSR MSP, R1
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
任務棧初始化
任務棧初始化時,必須將相應的CPU寄存器預留相應的位置,這里面的一些值是起始的任務函數地址,及傳入的函數,還有一些寄存器的初始化,只是必須要把棧的地址給空出來,不然任務第一恢復運行的時候,就不會正常工作。
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK *p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
{
CPU_STK *p_stk;
(void)opt; /* Prevent compiler warning */
p_stk = &p_stk_base[stk_size]; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*--p_stk = (CPU_STK)0x01000000u; /* xPSR */
*--p_stk = (CPU_STK)p_task; /* Entry Point */
*--p_stk = (CPU_STK)OS_TaskReturn; /* R14 (LR) */
*--p_stk = (CPU_STK)0x12121212u; /* R12 */
*--p_stk = (CPU_STK)0x03030303u; /* R3 */
*--p_stk = (CPU_STK)0x02020202u; /* R2 */
*--p_stk = (CPU_STK)p_stk_limit; /* R1 */
*--p_stk = (CPU_STK)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*--p_stk = (CPU_STK)0x11111111u; /* R11 */
*--p_stk = (CPU_STK)0x10101010u; /* R10 */
*--p_stk = (CPU_STK)0x09090909u; /* R9 */
*--p_stk = (CPU_STK)0x08080808u; /* R8 */
*--p_stk = (CPU_STK)0x07070707u; /* R7 */
*--p_stk = (CPU_STK)0x06060606u; /* R6 */
*--p_stk = (CPU_STK)0x05050505u; /* R5 */
*--p_stk = (CPU_STK)0x04040404u; /* R4 */
return (p_stk);
}
個別匯編指令備注
EXC_RETURN詳解
這是為什么要使用ORR LR,LR,#0x04這個指令的原因,將EXC_RETURN的第2bit置1,使其返回線程模式,返回后使用PSP,而FREERTOS中並沒有使用這個語句,是因為FREERTOS的第一個任務調用跟UCOS的實現方式是不一樣,因此可以不使用這個置位操作
中斷返回
一般使用BX LR這個指令來返回,但這種返回方式並不是唯一。
上面這個過程就實現了任務的上下文切換,不過關於的任務的調度是沒有在PENDSV這個中斷中實現的,先找到高優先級的任務再使用PENDSV來進行上下文切換。