天天看點

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

點選檢視第一章 點選檢視第二章

第3章

任務的定義與任務切換

本章我們真正開始從0到1寫RTOS。必須學會建立任務,并重點掌握任務是如何切換的。因為任務的切換是由彙編代碼來完成的,是以代碼看起來比較難懂,但是我們會盡力把代碼講得透徹。如果不能掌握本章内容,那麼後面的内容根本無從下手。

在本章中,我們會建立兩個任務,并讓這兩個任務不斷地切換,任務的主體都是讓一個變量按照一定的頻率翻轉,通過KEIL的軟體仿真功能,在邏輯分析儀中觀察變量的波形變化,最終的波形圖如圖3-1所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

其實,圖3-1所示的波形圖并不是真正的多任務系統中任務切換的效果圖,這個效果其實可以完全由裸機代碼實作,具體參見代碼清單3-1。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

在多任務系統中,兩個任務不斷切換的效果圖應該如圖3-2所示,即兩個變量的波形是完全一樣的,就好像CPU在同時做兩件事,這才是多任務的意義。雖然兩者的波形圖一樣,但是代碼的實作方式是完全不同的,由原來的順序執行變成了任務的主動切換,這是根本差別。本章隻是開始,我們先掌握好任務是如何切換的,在後面章節中,會陸續完善功能代碼,加入系統排程,實作真正的多任務。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.1 多任務系統中任務的概念

在裸機系統中,系統的主體就是main()函數中順序執行的無限循環,在這個無限循環中,CPU按照順序完成各種操作。在多任務系統中,根據功能不同,可以把整個系統分割成一個個獨立的且無法傳回的函數,這種函數稱為任務,也有人稱之為線程。任務的大概形式具體參見代碼清單3-2。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.2 建立任務

3.2.1 定義任務棧

先回想一下,在一個裸機系統中,如果有全局變量,有子函數調用,有中斷發生,那麼系統在運作時,全局變量放在哪裡?子函數調用時,局部變量放在哪裡?中斷發生時,函數傳回位址放在哪裡?如果隻是單純的裸機程式設計,可以不考慮上述問題,但是如果要寫一個RTOS,就必須明确這些參數是如何存儲的。在裸機系統中,它們統統放在棧中。棧是單片機RAM中一段連續的記憶體空間,其大小由啟動檔案中的代碼配置,具體參見代碼清單3-3,最後由C庫函數__main進行初始化。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

但是,在多任務系統中,每個任務都是獨立的、互不幹擾的,是以要為每個任務都配置設定獨立的棧空間,這個棧空間通常是一個預先定義好的全局數組。這些任務棧也存在于RAM中,能夠使用的最大的棧尺寸也是由代碼清單3-3中的Stack_Size決定的。隻是多任務系統中任務的棧就是在一個統一的棧空間裡面配置設定好一個個獨立的“房間”,每個任務隻能使用各自的房間,而需要在裸機系統中使用棧時,則可以天馬行空,在棧裡尋找任意空閑空間加以使用。

本章我們要實作兩個變量按照一定的頻率輪流翻轉,需要用兩個任務來實作,那麼就需要定義兩個任務棧,具體參見代碼清單3-4。在多任務系統中,有多少個任務就需要定義多少個任務棧。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-4(1):任務棧的大小由宏定義控制,在C/OS-III中,空閑任務的棧最小應該大于128,這裡的任務棧也暫且配置為128。

代碼清單3-4(2):任務棧其實就是一個預先定義好的全局資料,此處資料類型為CPU_STK。在C/OS-III中,凡是涉及資料類型的地方,C/OS-III都會将标準的C資料類型用typedef 重新設定一個類型名,命名方式則采用見名知義的方式且使用大寫字母。凡是與CPU類型相關的資料類型統一在cpu.h中定義,與作業系統相關的資料類型則在os_type.h中定義。CPU_STK就是與CPU相關的資料類型,具體參見代碼清單3-5。首次使用cpu.h,需要自行在C-CPU檔案夾中建立并添加到工程的C/CPU組中。代碼清單3-5中除了CPU_STK外,其他資料類型重定義是本章後面内容中需要用到的,這裡統一給出,後面将不再贅述。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.2.2 定義任務函數

任務是一個獨立的函數,函數主體無限循環且不能傳回。本章定義的兩個任務具體參見代碼清單3-6。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-6(1):如果要在KEIL邏輯分析儀中觀察波形的變量,則需要将其定義成全局變量,且要以Bit模式觀察,不能使用預設的模拟量。

代碼清單3-6(2)(3):正如介紹的那樣,任務是一個獨立的、無限循環且不能傳回的函數。

3.2.3 定義任務控制塊

在裸機系統中,程式的主體是CPU按照順序執行的,而在多任務系統中,任務的執行是由系統排程的。系統為了順利地排程任務,為每個任務都額外定義了一個任務控制塊(Task Control Block,TCB),這個任務控制塊相當于任務的身份證,裡面存有任務的所有資訊,比如任務棧、任務名稱、任務形參等。有了TCB,以後系統對任務的全部操作都可以通過這個TCB來實作。TCB是一個新的資料類型,在os.h頭檔案中聲明(第一次使用os.h時需要自行在檔案夾C/OS-IIISource中建立并添加到工程的C/OS-III Source組),有關TCB具體的聲明參見代碼清單3-7,使用它可以為每個任務都定義一個TCB實體。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-7(1):在C/OS-III中,所有的資料類型都會被重新設定一個名稱且用大寫字母表示。

代碼清單3-7(2):目前TCB裡面的成員還比較少,隻有棧指針和棧大小。為了以後操作友善,我們把棧指針作為TCB的第一個成員。

此處,在app.c檔案中為兩個任務定義的TCB具體參見代碼清單3-8。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.2.4 實作任務建立函數

任務棧、任務的函數實體、任務的TCB最終需要聯系起來才能由系統進行統一排程,這個聯系的工作由任務建立函數OSTaskCreate()實作,該函數在os_task.c中定義(第一次使用os_task.c時需要自行在檔案夾C/OS-IIISource中建立并添加到工程的C/OS-III Source組),所有與任務相關的函數都在這個檔案中定義。OSTaskCreate()函數的實作具體參見代碼清單3-9。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-9:OSTaskCreate()函數遵循C/OS-III中的函數命名規則,以OS開頭,表示這是一個外部函數,可以由使用者調用;以OS_開頭的函數則表示内部函數,隻能在C/OS-III内部使用。緊接着是檔案名,表示該函數放在哪個檔案中,最後是函數功能名稱。

代碼清單3-9(1):p_tcb是任務控制塊指針。

代碼清單3-9(2):p_task 是任務名,類型為OS_TASK_PTR,原型聲明在os.h檔案中,具體參見代碼清單3-10。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-9(3):p_arg是任務形參,用于傳遞任務參數。

代碼清單3-9(4):p_stk_base 用于指向任務棧的起始位址。

代碼清單3-9(5):stk_size 表示任務棧的大小。

代碼清單3-9(6):p_err 用于存儲錯誤碼。C/OS-III中為函數的傳回值預先定義了很多錯誤碼,通過這些錯誤碼可以知道函數出現錯誤的原因。為了友善,我們現在把C/OS-III中所有的錯誤碼都給出來。錯誤碼是枚舉類型的資料,在os.h中定義,具體參見代碼清單3-11。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-9(7):OSTaskStkInit()是任務棧初始化函數。當任務第一次運作時,加載到CPU寄存器的參數就放在任務棧中,在任務建立時,預先初始化好棧。OSTaskStkInit()函數在os_cpu_c.c中定義(第一次使用os_cpu_c.c時需要自行在檔案夾C-CPU中建立并添加到工程的C/CPU組),具體參見代碼清單3-12。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-12(1):p_task是任務名,表示任務的入口位址,在任務切換時,需要加載到R15,即PC寄存器,這樣CPU就可以找到要運作的任務。

代碼清單3-12(2):p_arg 是任務的形參,用于傳遞參數,在任務切換時,需要加載到寄存器R0。R0寄存器通常用來傳遞參數。

代碼清單3-12(3):p_stk_base 表示任務棧的起始位址。

代碼清單3-12(4):stk_size 表示任務棧的大小,資料類型為CPU_STK_SIZE,在Cortex-M3核心的處理器中等于4位元組,即一個字。

代碼清單3-12(5):擷取任務棧的棧頂位址,ARMCM3處理器的棧是由高位址向低位址生長的,是以在初始化棧之前,要擷取棧頂位址,然後将棧位址逐一遞減即可。

代碼清單3-12(6):任務第一次運作時,加載到CPU寄存器的環境參數要預先初始化好。初始化的順序固定,首先是異常發生時自動儲存的8個寄存器,即xPSR、R15、R14、R12、R3、R2、R1和R0。其中xPSR寄存器的位24必須是1,R15 PC指針必須存儲任務的入口位址,R0必須是任務形參。對于R14、R12、R3、R2和R1,為了調試友善,應填入與寄存器号相對應的十六進制數。

代碼清單3-12(7):剩下的是8個需要手動加載到CPU寄存器的參數,為了調試友善,應填入與寄存器号相對應的十六進制數。

代碼清單3-12(8):傳回棧指針p_stk,這時p_stk指向剩餘棧的棧頂。

代碼清單3-9(8):将剩餘棧的棧頂指針p_sp儲存到TCB的第一個成員StkPtr中。

代碼清單3-9(9):将任務棧的大小儲存到TCB的成員StkSize中。

代碼清單3-9(10):函數執行到這裡表示沒有錯誤,即OS_ERR_NONE。

任務建立好之後,需要把任務添加到就緒清單,表示任務已經就緒,系統随時可以排程。将任務添加到就緒清單的代碼具體參見代碼清單3-13。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-13(1)(2):把TCB指針放到OSRdyList數組中。OSRdyList是一個類型為OS_RDY_LIST的全局變量,在os.h中定義,具體參見代碼清單3-14。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-14(1):OS_CFG_PRIO_MAX是一個定義,表示這個系統支援多少個優先級(剛開始暫時不支援多個優先級,後面的章節中會支援),目前僅用來表示這個就緒清單可以存儲多少個TCB指針。具體的宏在os_cfg.h中定義(第一次使用os_cfg.h時需要自行在檔案夾C/OS-IIISource中建立并添加到工程的C/OS-III Source組),具體參見代碼清單3-15。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-14(2):OS_RDY_LIST是就緒清單的資料類型,在os.h中聲明,具體參見代碼清單3-16。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-16(1):C/OS-III中會為每個資料類型重新設定一個字母大寫的名稱。

代碼清單3-16(2):OS_RDY_LIST中目前隻有兩個TCB類型的指針,一個是頭指針,一個是尾指針。本章實驗隻用到頭指針,用來指向任務的TCB。隻有當後面講到同一個優先級支援多個任務時才需要使用頭尾指針來将TCB串成一個雙向連結清單。

代碼清單3-14(3):OS_EXT是一個在os.h中定義的宏,具體參見代碼清單3-17。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

這段代碼的意思是,如果沒有定義OS_GLOBALS這個宏,那麼OS_EXT就為空,否則為extern。

在C/OS-III中,需要使用很多全局變量,這些全局變量都在os.h頭檔案中定義,但是os.h會被包含進很多檔案中,那麼編譯時os.h中定義的全局變量就會出現重複定義的情況,而我們隻想将os.h中的全局變量隻定義一次,涉及包含os.h頭檔案時隻是聲明。有人提出可以加extern,那麼該如何加?

通常采取的做法是在C檔案中定義全局變量,然後在頭檔案中需要使用全局變量的位置添加extern聲明,但是C/OS-III中檔案非常多,這種方法可行,但不現實,是以就有了在os.h頭檔案中定義全局變量,然後在os.h檔案的開頭加上代碼清單3-17中宏定義的方法。但是這樣還沒有成功,C/OS-III另外建立了一個os_var.c檔案(第一次使用os_var.c時需要自行在檔案夾C/OS-IIISource中建立并添加到工程的C/OS-III Source組),其中包含了os.h,且隻在這個檔案中定義OS_GLOBALS這個宏,具體參見代碼清單3-18。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

經過這樣的處理之後,在編譯整個工程時,隻有var.c中os.h的OS_EXT才會被替換為空,即變量的定義,其他包含os.h的檔案因為沒有定義OS_GLOBALS這個宏,是以OS_EXT會被替換成extern,即變成了變量的聲明。這樣就實作了在頭檔案中定義變量。

在C/OS-III中,将任務添加到就緒清單其實是在OSTaskCreate()函數中完成的。每當任務建立好就把任務添加到就緒清單,表示任務已經就緒,隻是目前這裡的就緒清單的實作還比較簡單,不支援優先級,也不支援雙向連結清單,隻是簡單地将TCB放到就緒清單的數組中。第8章将專門講解就緒清單,等完善就緒清單之後,再把這部分的操作放回OSTaskCreate()函數中。

3.3 作業系統初始化

作業系統初始化一般是在硬體初始化完成之後進行的,主要是初始化C/OS-III中定義的全局變量。作業系統初始化用OSInit()函數實作。OSInit()函數在檔案os_core.c中定義(第一次使用os_core.c時需要自行在檔案夾C/OS-IIISource中建立并添加到工程的C/OS-III Source組),具體實作參見代碼清單3-19。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-19(1):系統用一個全局變量OSRunning訓示其運作狀态,剛開始初始化系統時,預設為停止狀态,即OS_STATE_OS_STOPPED。

代碼清單3-19(2):全局變量OSTCBCurPtr是系統用于指向目前正在運作的任務的TCB指針,在任務切換時用得到。

代碼清單3-19(3):全局變量OSTCBHighRdyPtr用于指向就緒清單中優先級最高的任務的TCB,在任務切換時用得到。本章暫時不支援優先級,則用于指向第一個運作的任務的TCB。

代碼清單3-19(4):OS_RdyListInit()用于初始化全局變量OSRdyList[],即初始化就緒清單。OS_RdyListInit()在os_core.c檔案中定義,具體實作參見代碼清單3-20。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-19(5):代碼運作到這裡表示沒有錯誤,即OS_ERR_NONE。

代碼清單3-19中的全局變量OSTCBCurPtr和OSTCBHighRdyPtr均在os.h中定義,具體參見代碼清單3-21。OS_STATE_OS_STOPPED 這個表示系統運作狀态的宏也在os.h中定義,具體參見代碼清單3-22。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.4 啟動系統

任務建立好且系統初始化完畢之後,就可以啟動系統了。系統啟動函數OSStart()在os_core.c中定義,具體實作參見代碼清單3-23。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-23(1):如果系統是第一次啟動,則if為真,繼續往下運作。

代碼清單3-23(2):OSTCBHighRdyPtr 指向第一個要運作的任務的TCB。因為暫時不支援優先級,是以系統啟動時先手動指定第一個要運作的任務。

代碼清單3-23(3):OSStartHighRdy()用于啟動任務切換,即配置PendSV的優先級為最低,然後觸發PendSV異常,在PendSV異常服務函數中進行任務切換。該函數不再傳回,在檔案os_cpu_a.s中定義(第一次使用os_cpu_a.s時需要自行在檔案夾C/OS-IIIPorts中建立并添加到工程的C/OS-III Ports組),用彙編語言編寫,具體實作參見代碼清單3-24。os_cpu_a.s檔案中涉及的ARM彙編指令的用法如表3-1所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-24中涉及的NVIC_INT_CTRL、NVIC_SYSPRI14、NVIC_PENDSV_PRI和NVIC_PENDSVSET這4個常量在os_cpu_a.s的開頭定義,具體參見代碼清單3-25,有關這4個常量的含義參見代碼注釋即可。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-24(1):配置PendSV的優先級為0XFF,即最低。在C/OS-III中,上下文切換是在PendSV異常服務程式中執行的,配置PendSV的優先級為最低,進而排除了在中斷服務程式中執行上下文切換的可能。

代碼清單3-24(2):設定PSP的值為0,開始第一個任務切換。在任務中,使用的棧指針都是PSP,後面如果判斷出PSP為0,則表示第一次任務切換。

代碼清單3-24(3):觸發PendSV異常,如果中斷啟用且編寫了PendSV異常服務函數,則核心會響應PendSV異常,去執行PendSV異常服務函數。

代碼清單3-24(4):開中斷,因為有些使用者在main()函數中會先關掉中斷,等全部初始化完成後,在啟動作業系統時才開中斷。為了快速地開關中斷,ARM CM3專門設定了一條 CPS指令,有4種用法,具體參見代碼清單3-26。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-26中,PRIMASK和FAULTMASK是ARM CM3中3個中斷屏蔽寄存器中的兩個,還有一個是BASEPRI,有關這3個寄存器的詳細用法如表3-2所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.5 任務切換

當調用OSStartHighRdy()函數,觸發PendSV異常後,就需要編寫PendSV異常服務函數,然後在其中進行任務切換。PendSV異常服務函數具體參見代碼清單3-27。PendSV異常服務函數名稱必須與啟動檔案向量表中PendSV的向量名一緻,如果不一緻,則核心無法響應使用者編寫的PendSV異常服務函數,隻響應啟動檔案中預設的PendSV異常服務函數。啟動檔案中為每個異常都編寫好了預設的異常服務函數,函數體都是一個死循環,當發現代碼跳轉到這些啟動檔案中預設的異常服務函數時,就要檢查異常函數名稱是否寫錯了,是否與向量表中的一緻。PendSV_Handler函數中涉及的ARM彙編指令的講解如表3-3所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-27中,PendSV異常服務中主要完成兩項工作,一是儲存上文,即儲存目前正在運作的任務的環境參數;二是切換下文,即把下一個需要運作的任務的環境參數從任務棧中加載到CPU寄存器,進而實作任務的切換。

PendSV異常服務中用到了OSTCBCurPtr和OSTCBHighRdyPtr這兩個全局變量,它們在os.h中定義,要想在彙編檔案os_cpu_a.s中使用,必須将這兩個全局變量導入os_cpu_a.s中,具體導入方法參見代碼清單3-28。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-28(1):使用IMPORT關鍵字将os.h中的OSTCBCurPtr和OSTCBHighRdyPtr這兩個全局變量導入該彙編檔案,進而該彙編檔案可以使用這兩個變量。如果是函數,也可以使用IMPORT導入。

代碼清單3-28(2):使用EXPORT關鍵字導出該彙編檔案中的OSStartHighRdy和PendSV_Handler函數,讓外部檔案可見。除了使用EXPORT導出外,還要在某個C的頭檔案中聲明這兩個函數(在C/OS-III中是在os_cpu.h中聲明),這樣才可以在C檔案中調用這兩個函數。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

接下來具體講解代碼清單3-27中主要代碼的含義。

代碼清單3-27(1):關中斷,NMI和HardFault除外,防止上下文切換被中斷。在上下文切換完畢之後,會重新開中斷。

代碼清單3-27(2):将PSP的值加載到R0寄存器。MRS是ARM 32位資料加載指令,功能是加載特殊功能寄存器的值到通用寄存器。

代碼清單3-27(3):判斷R0,如果值為0,則跳轉到OS_CPU_PendSVHandler_nosave。進行第一次任務切換時,PSP在OSStartHighRdy初始化為0,是以這時R0肯定為0,是以跳轉到OS_CPU_PendSVHandler_nosave。CBZ是ARM 16位轉移指令,用于比較,結果為0則跳轉。

代碼清單3-27(4):當第一次切換任務時,會跳轉到這裡運作。當執行過一次任務切換之後,則順序執行到這裡。這個标号以後的内容屬于下文切換。

代碼清單3-27(5):加載 OSTCBCurPtr 指針的位址到R0。在ARM彙編中,操作變量都屬于間接操作,即要先擷取這個變量的位址。這裡LDR屬于僞指令,不是ARM指令。例如“LDR Rd, = label”,如果label是立即數,那麼Rd等于立即數;如果label是一個辨別符,比如指針,那麼存到Rd的就是label這個辨別符的位址。

代碼清單3-27(6):加載 OSTCBHighRdyPtr 指針的位址到R1,這裡LDR也屬于僞指令。

代碼清單3-27(7):加載 OSTCBHighRdyPtr 指針到R2,這裡LDR屬于ARM指令。

代碼清單3-27(8):存儲 OSTCBHighRdyPtr 到 OSTCBCurPtr,實作将下一個要運作的任務的TCB存儲到OSTCBCurPtr。

代碼清單3-27(9):加載 OSTCBHighRdyPtr 到 R0。TCB中第一個成員是棧指針StkPtr,是以這時R0等于StkPtr,後續操作任務棧都是通過操作R0來實作,不需要操作StkPtr。

代碼清單3-27(10):将任務棧中需要手動加載的内容加載到CPU寄存器R4~R11,同時會遞增R0,讓R0指向空閑棧的棧頂。LDMIA中的I是increase的縮寫,A是after的縮寫,R0後面的“!”表示會自動調節R0中存儲的指針。當任務被建立時,任務的棧會被初始化,初始化的流程是先讓棧指針StkPtr指向棧頂,然後從棧頂開始依次存儲異常退出時會自動加載到CPU寄存器的值和需要手動加載到CPU寄存器的值,具體代碼實作參見代碼清單3-12中的OSTaskStkInit()函數,棧空間的分布情況如圖3-3所示。當把需要手動加載到CPU的棧内容加載完畢之後,棧空間的分布圖和棧指針指向如圖3-4所示,注意這時StkPtr不變,改變的是R0。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-27(11):更新PSP的值,這時PSP與圖3-4中R0的指向一緻。

代碼清單3-27(12):設定LR寄存器的位2為1,確定異常退出時使用的棧指針是PSP。當異常退出後,就切換到就緒任務中優先級最高的任務繼續運作。

代碼清單3-27(13):開中斷。上下文切換已經完成了3/4,剩下的就是異常退出時自動儲存的部分。

代碼清單3-27(14):異常傳回,這時任務棧中的剩餘内容将會自動加載到xPSR、PC(任務入口位址)、R14、R12、R3、R2、R1、R0(任務的形參)寄存器,同時PSP的值也将更新,即指向任務棧的棧頂,這樣就切換到了新的任務。這時棧空間的分布具體如圖3-5所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-27(15):手動存儲CPU寄存器R4~R11的值到目前任務棧。當出現異常,進入PendSV異常服務函數時,目前CPU寄存器xPSR、PC(任務入口位址)、R14、R12、R3、R2、R1、R0會自動存儲到目前任務棧,同時遞減PSP的值,這個時候目前任務的棧空間分布如圖3-6所示。當執行“STMDB R0!, {R4-R11}”後,目前任務的棧空間分布圖如圖3-7所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-27(16):加載 OSTCBCurPtr 指針的位址到R1,這裡LDR屬于僞指令。

代碼清單3-27(17):加載 OSTCBCurPtr 指針到R1,這裡LDR屬于ARM指令。

代碼清單3-27(18):存儲R0的值到OSTCBCurPtr->OSTCBStkPtr,這時R0存儲的是任務空閑棧的棧頂。執行到了這裡,才完成了上文的儲存。這時目前任務的棧空間分布和棧指針指向如圖3-8所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.6 main()函數

main()函數在檔案app.c中編寫,app.c檔案的完整代碼參見代碼清單3-29。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

代碼清單3-29中的所有代碼在本小節之前都有循序漸進的講解,這裡隻是融合在一起放在main()函數中。Task1和Task2并不會真正自動切換,而是在各自的函數體中加入了OSSched()函數來實作手動切換。OSSched()函數的實作具體參見代碼清單3-30。

OSSched()函數的排程算法很簡單,即如果目前任務是任務1,那麼下一個任務就是任務2,如果目前任務是任務2,那麼下一個任務就是任務1,然後調用OS_TASK_SW()函數觸發PendSV異常,再在PendSV異常中實作任務的切換。在此後的章節中,我們将繼續完善,加入SysTick中斷,進而實作系統排程的自動切換。OS_TASK_SW()函數其實是一個宏定義,具體是往中斷及狀态控制寄存器SCB_ICSR的位28(PendSV異常啟用位)寫入1,進而觸發PendSV異常。OS_TASK_SW()函數在os_cpu.h檔案中實作(第一次使用os_cpu.h時需要自行在檔案夾C-CPU中建立并添加到工程的C/CPU組),檔案的内容具體參見代碼清單3-31。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

3.7 實驗現象

本章代碼講解完畢,接下來是軟體調試仿真,具體過程如圖3-9~圖3-13所示。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

至此,本章講解完畢。但是隻是把本章的内容看完,再仿真看看波形是遠遠不夠的,應該是把任務棧、TCB、OSTCBCurPtr和OSTCBHighRdyPtr這些變量統統添加到觀察視窗,然後單步執行程式,觀察這些變量是如何變化的,特别是任務切換時,CPU寄存器、任務棧和PSP是如何變化的,讓機器執行代碼的過程在腦海中示範一遍。如圖3-14所示就是我們在進行仿真調試時出現的觀察視窗。

帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章
帶你讀《C/OS-III核心實作與應用開發實戰指南:基于STM32》之三:任務的定義與任務切換第3章

繼續閱讀