1. 程式人生 > >UCOS-III學習筆記——OS獲取CPU控制權深入理解

UCOS-III學習筆記——OS獲取CPU控制權深入理解

μC/ OS III Version: v3.03.01

μC/ CPU Version: v1.29.01

μC/ LIB Version: v1.37.00

FOR: NXP LPC1768 CPU

UCOS-III學習筆記——main函式中的配置與初始化一文中我們瞭解了在main()函式中ucos系統的初始化過程,在main()函式的末尾通過OSStart()函式中的OSStartHighRdy()函式,將CPU的控制權交給OS,啟動多工處理。

這一過程主要通過控制PendSV中斷,儲存上文恢復下文,以實現任務的轉換,大多使用的是底層的組合語言,所以跟核心關係密切,這裡我們使用的是cotex-M3核心進行實驗,適用於STM32,LPC1768等微控制器。下面我們將分析程式碼深入理解這一過程的具體步驟和原理,以下函式均在os_cpu_a.s檔案內。

1.  OSStartHighRdy()

OSStartHighRdy
    LDR     R0, =NVIC_SYSPRI14                                  ; 設定PendSV中斷的優先順序
    LDR     R1, =NVIC_PENDSV_PRI
    STRB    R1, [R0]

    MOVS    R0, #0                                              ; 將PSP指標設定為0
    MSR     PSP, R0

    LDR     R0, =OS_CPU_ExceptStkBase                           ; 初始化MSP指標
    LDR     R1, [R0]
    MSR     MSP, R1    

    LDR     R0, =NVIC_INT_CTRL                                  ; 觸發PendSV中斷
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    
    CPSIE   I                                                   ; 開中斷

OSStartHang
    B       OSStartHang                                         ; 程式永遠不會執行到這裡
  • 首先,配置PendSV優先順序為0xff最低。PendSV是可懸起異常,如果我們把它配置最低優先順序,那麼如果同時有多個異常被觸發,它會在其他異常執行完畢後再執行,而且任何異常都可以中斷它。更詳細的內容在《Cortex-M3 權威指南》裡有介紹。

  • 然後,將PSP指標設定為0(為後面的內容做準備),並將MSP賦值。其中,PSP為任務堆疊指標,MSP為主堆疊指標。在任務切換過程中主要使用的是PSP。
  • 然後觸發PendSV中斷,但由於在main函式的初始化過程中關閉了所有中斷,所以此時還需要手動開啟所有中斷。

  • 之後,程式將立即跳轉到PendSV中斷函式中,即OS_CPU_PendSVHandler。

2.  OS_CPU_PendSVHandler()

OS_CPU_PendSVHandler
    CPSID   I                                                   ; 關中斷,防止切換過程中發生中斷
    MRS     R0, PSP                                             ; 賦予r0為PSP的值
    CBZ     R0, OS_CPU_PendSVHandler_nosave                     ; 如果為0則跳轉(第一次必跳轉)

    SUBS    R0, R0, #0x20                                       ; 儲存 r4-11 暫存器
    STM     R0, {R4-R11}

    LDR     R1, =OSTCBCurPtr                                    ; OSTCBCurPtr->OSTCBStkPtr = SP;
    LDR     R1, [R1]
    STR     R0, [R1]                                            ; r0是當前任務的堆疊指標
  • 首先,為了避免切換任務被打斷,需要關閉總中斷。
  • 然後,讀取PSP的值,第一次進入中斷時,由於之前已經給PSP賦值為0,所以一定會直接跳轉到下面的OS_CPU_PendSVHandler_nosave()函式開始執行。
  • 若不是第一次進入中斷,則需要做儲存上文的工作。則在進入中斷時,CPU將自動把當前任務的暫存器xPSR, PC, LR, R12, R0-R3存到當前任務堆疊中,PSP指向R0;剩下的r4-11暫存器則需要手動儲存到任務堆疊中,STM指令即為入棧的意思,PSP指標不變,改變的是R0暫存器,PSP依舊指向的是自動儲存後的地址。
  • 暫存器進棧完畢後,讀取OSTCBCurPtr(當前任務TCB指標,即堆疊指標OSTCBStkPtr)的值,將其賦予R0,因為OS_TCB結構體第一個成員為任務的堆疊指標OSTCBStkPtr,OSTCBStkPtr的地址與任務TCB地址相同。
  • 每個OS_TCB的OSTCBStkPtr的值在建立任務時被賦值,指向的是每個任務堆疊的R4的位置。所以當前OSTCBCurPtr的值為圖中StkPtr所示。

3.  OS_CPU_PendSVHandler_nosave()

OS_CPU_PendSVHandler_nosave
    PUSH    {R14}                                               ; 儲存LR
    LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();
    BLX     R0
    POP     {R14}

    LDR     R0, =OSPrioCur                                      ; OSPrioCur   = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]

    LDR     R0, =OSTCBCurPtr                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;
    LDR     R1, =OSTCBHighRdyPtr
    LDR     R2, [R1]
    STR     R2, [R0]

    LDR     R0, [R2]                                            ; R0 新任務的堆疊指標 
    LDM     R0, {R4-R11}                                        ; POP出 r4-11 
    ADDS    R0, R0, #0x20
    MSR     PSP, R0                                             ; 給PSP賦值
    ORR     LR, LR, #0x04                                       ; 確保使用的是PSP不是MSP
    CPSIE   I
    BX      LR                                                  ; 跳轉LR的地址

    END
  • 首先,函式首先利用儲存上文與恢復下文的間隙,呼叫執行OSTaskSwHook()使用者定義的任務切換對外介面函式,之後返回。
  • 然後,將OSPrioCur(當前的優先順序)和OSTCBCurPtr(當前的任務TCB指標)更新(在進入中斷前,已經將OSPrioHighRdy和OSTCBHighRdyPtr通過相應的函式賦好了新的值,指向了新的高優先順序的任務,例如第一次賦值是在OSStart()函式中進行的)。
  • 通過LDR指令,獲取任務的堆疊指標OSTCBStkPtr,將R4-R11手動POP出堆疊,存入CPU的暫存器中,因為退出中斷時剩餘暫存器將自動POP出堆疊,只需將R0當前的值賦予PSP即可,所以剩餘的暫存器我們無需操作。當剩餘暫存器自動POP出時,PSP將指向堆底。整個過程正好與儲存上文的過程相反。

  •  ORR     LR, LR, #0x04 這一步確保在跳轉後使用的是PSP,《Cortex-M3 權威指南》裡有介紹。

  • 之後開啟中斷,跳轉到LR的地址繼續執行新的任務。

總結來說:

OSStartHighRdy()函式完成的是OS開始前的準備工作;

OS_CPU_PendSVHandler()函式完成了上文儲存工作;

OS_CPU_PendSVHandler_nosave()函式完成的是切換任務恢復下文並跳轉執行的工作;

涉及到任務切換時,無論是任務級別的切換還是中斷級別的切換,實際的切換操作都是在PendSV異常中斷函式中實現的,實現過程如下:

1.如果系統是剛開機,準備執行第一個任務,此時PSP還為0,則呼叫切換函式之前已經得到OSTCBPrioRdy和OSPrioHighRdy的值,此時該任務的cpu各暫存器內容已經儲存在任務堆疊中,此時我們所做的就是將壓入堆疊的內容彈回cpu的各個暫存器,恢復執行現場(確切點說第一次不能用恢復),恢復現場之後將此時的OSTCBCur->OSTCBStkPtr給予cpu的sp,此時cpu的sp暫存器就儲存了現在執行任務的堆疊棧頂地址。

2.如果此次任務切換不是第一次,則此時PSP儲存的是切換之前任務的堆疊棧頂地址,此時我們可以用PUSH指令將R4-R11的內容根據PSP所提供的堆疊棧頂地址壓入堆疊。然後根據步驟1我們可以知道,在呼叫切換函式之前我們已經知道了OSTCBPrioRdy和OSPrioHighRdy,此時我們可以我們可以知道即將執行的任務的堆疊地址,根據堆疊地址我們用POP命令彈出堆疊儲存的值,恢復cpu的執行現場,彈出之後將OSTCBCur->OSTCBStkPtr的值給予cpu的sp暫存器,此時cpu的sp暫存器就儲存了現在執行任務的堆疊棧頂地址。