STM32下的uCOS底層調度深度分析

[size=10.5000pt]第一次發帖,終於明白了實時系統的調度,寫了一下分享給大家。絕對原創。
大家用嵌入式系統都知道,可以運行多任務,那系統究竟是怎麼從一個任務切換到另一個任務的呢。
[size=10.5000pt]這裏以uCOS爲例,以STM32爲硬件平臺。分析uCOS底層的任務調度。其他硬件平臺的任務切換有待研究,不過應該類似。
[size=10.5000pt]STM32採用Cortex-M3內核,ARM公司在設計時考慮到了運行實時系統的要求。所以這裏有一些重要的特性,給內核提供了很大的便利。
[size=10.5000pt]現在討論的任務切換使用了PendSV,和堆棧。
[size=10.5000pt]首先是PendSV
[size=10.5000pt]可懸掛請求,可以說是個軟件中斷,在進入PendSV中斷處理程序時,處理器硬件也會做保護現場的操作。
[size=10.5000pt]它的使用是可以延遲任務調度,一般系統會在定時中斷(即系統心跳)做任務調度。
[size=10.5000pt]不過這時可能出現心跳中斷髮生時在執行一箇中斷,此時又不能執行調度。那隻能等下一個心跳。這時其實已經讓任務的實時性下降了。而如果一箇中斷和心跳是同頻率的的話,則系統調度永遠不會發生。
[size=10.5000pt]有了PendSV之後就可以在心跳來到時通過設置ICSR懸起中斷等到其他中斷結束時便會自動執行PendSV中斷處理程序完成任務切換。這裏會把PendSV優先級設爲最低。
[size=10.5000pt]**********************************引用*****************************************


file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-15739.png
[size=10.5000pt]上圖摘至Cortex-M3權威指南---重點參考書目,如果要移植操作系統。
[size=10.5000pt]Cortex-M3的堆棧有兩個MSPPSPMSP用於handle模式,PSP用於線程模式。不過這些都是有操作系統的情況下,平時寫的前後臺程序沒有使用PSP。這兩個堆棧指針同一時刻只有一個可以看到(banked)。同一時刻只有一個可以看到其實是指PUSHPOP操作的對象只能是一個。
[size=10.5000pt]比如入棧
[size=10.5000pt]PUSH {R0}
[size=10.5000pt]出棧
[size=10.5000pt]POP{R0}
[size=10.5000pt]這裏PUSHPOP的操作對象其實是SPR13),而SP可以映射到MSP也可以映射到PSP
[size=10.5000pt]所以具體的PUSH{R0}可能是對MSP操作的也可以是對PSP操作的,可以通過控制寄存器CONTROL操作
[size=10.5000pt]**********************************引用*****************************************

file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-14627.png
[size=10.5000pt]上圖摘至Cortex-M3權威指南。
[size=10.5000pt]而平時的前後臺程序程序一運行就是在特權級下的而且SP映射MSP的。
[size=10.5000pt]下面看uCOS 調度函數
[size=10.5000pt]void  OS_Sched (void)
[size=10.5000pt]{
[size=10.5000pt]#if OS_CRITICAL_METHOD == 3                           
[size=10.5000pt]    OS_CPU_SR  cpu_sr = 0;
[size=10.5000pt]#endif

[size=10.5000pt]    OS_ENTER_CRITICAL();
[size=10.5000pt]if (OSIntNesting == 0) {                           
[size=10.5000pt]        if (OSLockNesting == 0) {                     
[size=10.5000pt]            OS_SchedNew();
[size=10.5000pt]            if (OSPrioHighRdy != OSPrioCur) {         
[size=10.5000pt]                OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
[size=10.5000pt]#if OS_TASK_PROFILE_EN > 0
[size=10.5000pt]                OSTCBHighRdy->OSTCBCtxSwCtr++;         
[size=10.5000pt]#endif
[size=10.5000pt]                OSCtxSwCtr++;                                         [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]        [size=10.5000pt]OS_TASK_SW();                        
[size=10.5000pt]            }
[size=10.5000pt]        }
[size=10.5000pt]    }
[size=10.5000pt]    OS_EXIT_CRITICAL();
[size=10.5000pt]}
[size=10.5000pt]上面註釋部分刪掉了,可以看的更清晰。
[size=10.5000pt]前面部分是通過就緒表查找就緒態的最高優先級的任務。
[size=10.5000pt]計算後會得到
[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]最高優先級就緒態任務的優先級
[size=10.5000pt]OSTCBHighRdy
[size=10.5000pt]最高優先級就緒態任務的[size=10.5000pt]TCB控制塊
[size=10.5000pt]#define  OS_TASK_SW()         OSCtxSw()
[size=10.5000pt]然後執行[size=10.5000pt]OS_TASK_SW()[size=10.5000pt]其實就是[size=10.5000pt]OSCtxSw()
[size=10.5000pt]OSCtxSw
[size=10.5000pt]    LDR     R0, =NVIC_INT_CTRL  ; Trigger the PendSV exception (causes context switch)
[size=10.5000pt]    LDR     R1, =NVIC_PENDSVSET
[size=10.5000pt]    STR     R1, [R0]
[size=10.5000pt]BX      LR
[size=10.5000pt]這裏可以找到
[size=10.5000pt]NVIC_INT_CTRL   EQU     0xE000ED04
[size=10.5000pt]NVIC_PENDSVSET  EQU    0x10000000
[size=10.5000pt]其實[size=10.5000pt]0xE000ED04[size=10.5000pt]就是[size=10.5000pt]ICSR的地址。我把上面彙編用類似C的僞代碼翻譯一下。因爲是僞代碼所以請大家不要在意語法。
[size=10.5000pt]僞代碼中把Ri當做32位整型變量
[size=10.5000pt]LDR     R0, =NVIC_INT_CTRL [size=10.5000pt]R0 = [size=10.5000pt]0xE000ED04
[size=10.5000pt]LDR     R1, =NVIC_PENDSVSET [size=10.5000pt]R1 = [size=10.5000pt]0x10000000
[size=10.5000pt]STR     R1, [R0] [size=10.5000pt]*U32*R0 = R1   
[size=10.5000pt]BX      LR [size=10.5000pt]return
[size=10.5000pt]其實就是把ICSRPENDSVSET位置1,之後機會觸發PendSV的中斷處理程序。
[size=10.5000pt]在中斷向量表中找到PendSV的中斷處理程序


file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-4224.png
[size=10.5000pt]就是[size=10.5000pt]OS_CPU_PendSVHandler
[size=10.5000pt]先看下OS_CPU_PendSVHandler上面的註釋
[size=10.5000pt]4) Since PendSV is set to lowest priority in the system (by OSStartHighRdy() above), we
[size=10.5000pt];              know that it will only be run when no other exception or interrupt is active, and
[size=10.5000pt];            therefore safe to assume that context being switched out was using the process stack (PSP).
[size=10.5000pt]前面三條要慢慢分析下。
[size=10.5000pt]看第四條。可以看到[size=10.5000pt]PendSV設置成最低優先級,所以PendSV發生時不會再中斷上下文中。
[size=10.5000pt]最後這句[size=10.5000pt]therefore safe to assume that context being switched out was using the process stack (PSP).[size=10.5000pt]在[size=10.5000pt]OS_CPU_PendSVHandler中斷返回時切換到PSP
[size=10.5000pt]壓軸的來了。
[size=10.5000pt]一邊貼源代碼一邊分析。
[size=10.5000pt]CPSID   I [size=10.5000pt]關中斷
[size=10.5000pt]MRS     R0, PSP [size=10.5000pt]R0 = PSP(注意這裏用的是PSP
[size=10.5000pt]CBZ [size=10.5000pt]  [size=10.5000pt]R0, OS_CPU_PendSVHandler_nosave [size=10.5000pt]IfR0 == 0
[size=10.5000pt]{
[size=10.5000pt]     [size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]();
[size=10.5000pt]}
[size=10.5000pt]這裏如果R0等於0表示是第一次調度,就會執行[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]()
[size=10.5000pt]因爲之前都是用MSP所以第一次調度PSP等於0
[size=10.5000pt]這裏先看下[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]( )
[size=10.5000pt]PUSH    {R14} [size=10.5000pt]簡單的入棧,不過注意這裏是用MSP因爲在handle模式必須是MSP
[size=10.5000pt]LDR     R0, =OSTaskSwHook [size=10.5000pt]OSTaskSwHook[size=10.5000pt]這裏是一個函數名
[size=10.5000pt]即這裏[size=10.5000pt]R0等於[size=10.5000pt]OSTaskSwHook[size=10.5000pt]的地址
[size=10.5000pt]OSTaskSwHook[size=10.5000pt]內其實是空的由用戶改寫,先不管它。
[size=10.5000pt]BLX     R0 [size=10.5000pt]OSTaskSwHook[size=10.5000pt]()
[size=10.5000pt]POP     {R14} [size=10.5000pt]出棧。這個R14LR
[size=10.5000pt]這裏打斷說下LR,在程序中執行BLBLX跳轉指令時會保存當前PC+4LR就是R14
[size=10.5000pt]在子函數中要返回時調用BX LR就可以返回到調用它的地方。
[size=10.5000pt]BX  LR
[size=10.5000pt]和BX   R14效果是一樣的。
[size=10.5000pt]不過在中斷處理中就不一樣了這時LR並不是跳轉時的PC+4。因爲中斷的返回和函數返回是不一樣的。中斷返回要恢復現場。比如51中中斷返回是RETI函數返回是RET
[size=10.5000pt]下面一段引用Cortex-M3權威指南
[size=10.5000pt]在Cortex-M3在進入異常服務程序後,將自動更新LR的值爲特殊的EXC_RETURN。這是一個高28位全爲1的值,只有[3:0]的值有特殊含義,如表9.3所示。當異常服務例程把這個值送往PC時,就會啓動處理器的中斷返回序列。因爲LR的值是由CM3自動設置的,所以只要沒有特殊需求,就不要改動它。
file:///C:\Users\wql\AppData\Local\Temp\ksohtml\wps_clip_image-20252.png

[size=10.5000pt]**********************************引用*****************************************


[size=10.5000pt]繼續分析:
[size=10.5000pt]LDR     R0, =[size=10.5000pt] [size=10.5000pt]OSPrioCur [size=10.5000pt]R0 = &OSPrioCur
[size=10.5000pt]LDR     R1, =[size=10.5000pt] [size=10.5000pt]OSPrioHighRdy [size=10.5000pt]R1 = &[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]LDRB    R2, [R1] [size=10.5000pt]R2 = *(U16*)R1
[size=10.5000pt]STRB    R2, [R0] [size=10.5000pt] *(U16*)R1 = R2
[size=10.5000pt] 上面4句就是OSPrioCur =[size=10.5000pt]OSPrioHighRdy
[size=10.5000pt]看到這裏不要吐槽彙編。原因就是,想想這裏爲什麼用匯編。
[size=10.5000pt]LDR     R0, =OSTCBCur [size=10.5000pt] R0 = &[size=10.5000pt]OSTCBCur
[size=10.5000pt]LDR     R1, =OSTCBHighRdy [size=10.5000pt] R1 = &[size=10.5000pt]OSTCBHighRdy
[size=10.5000pt]LDR     R2, [R1] [size=10.5000pt] R2 = *([size=10.5000pt]U32[size=10.5000pt]*)R1
[size=10.5000pt]STR     R2, [R0] [size=10.5000pt] *([size=10.5000pt]uU32[size=10.5000pt]*)R0 = R2
[size=10.5000pt]效果和前面一樣不過列出類要幫助下面分析。這兩個數據都是[size=10.5000pt]OS_TCB[size=10.5000pt]類型,原型只截取最前面
[size=10.5000pt]typedef struct os_tcb {
[size=10.5000pt]OS_STK          *OSTCBStkPtr;
[size=10.5000pt]......................
[size=10.5000pt]}[size=10.5000pt] OS_TCB;
[size=10.5000pt]這個[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]就是指向任務堆棧的棧頂指針。它是個二級指針。
[size=10.5000pt]把它放在結構體最前面就是方便彙編操作。因爲這樣[size=10.5000pt]OSTCBCur[size=10.5000pt] 的地址和[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]的地址就一樣了。
[size=10.5000pt]LDR     R0, [R2] [size=10.5000pt]注意上面[size=10.5000pt]R2的值
[size=10.5000pt]R2 = &([size=10.5000pt]OSTCBHighRdy[size=10.5000pt]->[size=10.5000pt]OSTCBStkPtr[size=10.5000pt])
[size=10.5000pt]R0 = [size=10.5000pt]OSTCBStkPtr
[size=10.5000pt]LDM     R0, {R4-R11} [size=10.5000pt]for(i=0; i<8; i++)
[size=10.5000pt]R(i+4) = *(OSTCBStkPtr+i)
[size=10.5000pt]其實就是恢復R4-R11的值
[size=10.5000pt]這裏只保存R4-R11是因爲其他重要的寄存器中斷髮生時硬件會保存
[size=10.5000pt]ADDS    R0, R0, #0x20 [size=10.5000pt]R0 = R0 + 0x20
[size=10.5000pt]MSR     PSP, R0 [size=10.5000pt]PSP = R0
[size=10.5000pt]ORR     LR, LR, #0x04 [size=10.5000pt]LR |= 0x04切換到線程模式
[size=10.5000pt]這時SP就映射到了PSP
[size=10.5000pt]之後返回線程模式在執行PUSH,POP等就使用PSP了。
[size=10.5000pt]CPSIE   I [size=10.5000pt]開中斷
[size=10.5000pt]BX      LR [size=10.5000pt]中斷返回(注意是中斷返回)

[size=10.5000pt]重要的地方********************************************************************

[size=10.5000pt]注意這裏是中斷返回和跳轉時不一樣的。它不是直接跳轉到指定位置,而是啓動內核中斷返回序列。由內核硬件恢復。這裏關鍵地方來了。內核自動恢復的包括PC。而內核是從堆棧中恢復的PC,而前面執行了
[size=10.5000pt]LDR     R0, [R2]
[size=10.5000pt]LDM     R0, {R4-R11}
[size=10.5000pt]ADDS    R0, R0, #0x20
[size=10.5000pt]MSR     PSP, R0
[size=10.5000pt]PSP被你改成了一個高優先級的任務的控制塊。
[size=10.5000pt]也就是說你進來中斷時內核幫你保存了PC等等數據,按理說中斷返回時內核會恢復到原來的位置。也就是PC還等於中斷前的位置,可是你改動了PSP中斷返回時把這時的PSP當做原來的堆棧。這個PC已經不是進來中斷時的PC了,等你返回時內核會跳到從你的[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]恢復出來的[size=10.5000pt]PC,這就實現了切換。PC進中斷指向A任務的代碼,出中斷就變成指向B任務的代碼。
[size=10.5000pt]說了這麼多還有一點代碼,之後在總結一下就清晰了。最後一點應該不用我分析了吧。
[size=10.5000pt]實際上[size=10.5000pt]OS_CPU_PendSVHandler[size=10.5000pt]代碼是:
[size=10.5000pt]CPSID   I                                                
[size=10.5000pt]MRS     R0, PSP                                             
[size=10.5000pt]CBZ     R0, OS_CPU_PendSVHandler_nosave                     
[size=10.5000pt]SUBS    R0, R0, #0x20                                          
[size=10.5000pt]STM     R0, {R4-R11}
[size=10.5000pt]LDR     R1, =OSTCBCur                                       
[size=10.5000pt]LDR     R1, [R1]
[size=10.5000pt]STR     R0, [R1]
[size=10.5000pt]OS_CPU_PendSVHandler_nosave
[size=10.5000pt]PUSH    {R14}                                               
[size=10.5000pt]LDR     R0, =OSTaskSwHook   
[size=10.5000pt]..........................
[size=10.5000pt]...........................[size=10.5000pt]                              
[size=10.5000pt]BX      LR
[size=10.5000pt]END
[size=10.5000pt]流程是先判斷PSP等與0
[size=10.5000pt]等於0就執行[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt](第一次調度)
[size=10.5000pt]執行[size=10.5000pt]OS_CPU_PendSVHandler_nosave[size=10.5000pt]後注意就直接中斷返回了,不會執行下面的代碼了。

[size=10.5000pt]如果不等於0就把當前任務的R4-R11保存起來
[size=10.5000pt]其實這之前內核已經把
[size=10.5000pt]xPSP, PC, LR, R12, R3 ,R2, R1, R0, 保存起來了。此時
[size=10.5000pt]OSTCBCur[size=10.5000pt]->[size=10.5000pt]OSTCBStkPtr等頂端是
[size=10.5000pt]xPSPPCLRR12R3R2R1R0R4R5R6R7R8R9R10R11
[size=10.5000pt]而[size=10.5000pt]OSTCBCur[size=10.5000pt]->[size=10.5000pt]OSTCBStkPtr指向R11
[size=10.5000pt]接着把[size=10.5000pt]OSTCBCur[size=10.5000pt] = [size=10.5000pt]OSTCBHighRdy
[size=10.5000pt]恢復之前上次保存的[size=10.5000pt]R4-R11
[size=10.5000pt]LDM     R0, {R4-R11}
[size=10.5000pt]ADDS    R0, R0, #0x20
[size=10.5000pt]切換堆棧到[size=10.5000pt]OSTCBHighRdy[size=10.5000pt]的堆棧,執行中斷返回內核從[size=10.5000pt]OSTCBHighRdy[size=10.5000pt]-[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]中讀取[size=10.5000pt]PC完成跳轉。
[size=10.5000pt]在之前我有一個問題,我想任務切換不就是保存通用寄存器嗎,而通用寄存器就那麼多,至少是固定數量的,比如15個,建立任務時給它15個深度堆棧不就夠了嗎。
[size=10.5000pt]這時才知道每個任務都是用自己的[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]當做堆棧使用。
[size=10.5000pt]也就是你在任務裏執行[size=10.5000pt]PUSH的時候其實用的是自己[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]裏的資源。那程序裏的複雜運算就要藉助堆棧實現,比如一個數組U32 P[30]這就耗掉30*4字節,任務裏調用函數,那麼在你調用的函數用的也是你的[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]裏的資源。如果[size=10.5000pt]OSTCBStkPtr[size=10.5000pt]不夠就會串到別的地方,就會跑飛了。
[size=10.5000pt]寫了這麼多,不知道說清楚了沒有。對於想深刻理解嵌入式內核的朋友,有幫助嗎。
編輯了好多次把純文本去掉了一保存怎麼還有亂七八糟的東西。我在WPS下編輯拷貝過來的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章