任务的定义、任务切换的原理及实现
文章目錄
- 1 任務的定義
- 2 任務切換的原理
- 3 任務切換的實現
- 3.1 設計目標
- 3.2 設計實現
- 3.3 代碼實現
- 3.4 PendSV_Handler的另一種實現
1 任務的定義
任務的外觀:一個永遠不會返回的函數。
任務的內在:棧、堆、數據區、代碼區、內核寄存器相關信息。
我們可以按如下方式進行任務定義:
2 任務切換的原理
任務切換的本質:保存前一任務的當前運行狀態,恢復后一任務之前的運行狀態。
任務狀態數據:
- 代碼、數據區:由編譯器自動分配,各個任務相互獨立,并不沖突。
- 堆:不使用。
- 棧:硬件只支持兩個堆棧空間,不同任務能夠共用?
- 內核寄存器:編譯器會在某些時間將值保存到棧中,如函數調用、異常處理。未保存的如何處理呢?
- 其它的狀態數據,如何處理呢?
解決方法:為每個任務配置獨立的棧,用于保存該任務的所有狀態數據。
總結一下任務切換:
任務切換的關鍵步驟:
- 保存當前任務的狀態(主要是寄存器信息)
- 恢復下一個要運行的任務(主要是恢復寄存器信息)
在哪里切換任務?
對于Cortex-M3內核的單片來說,我們可以通過觸發PendSVC異常(為了保證中斷的實時響應,PendSVC的中斷優先級應該設置為最低),然后在異常中進行任務切換。
切換任務是需要保存當前正在執行的任務的哪些寄存器信息?
對于一個任務來說,要想被中斷后恢復到之前的運行狀態,那么其所對應的寄存器信息和棧里的內容必須和之前一樣。
對于寄存器:當發生異常時,處理器會自動將xpsr、r15(pc)、r14(lr)、r12、r3~r0壓入棧中。不過我們需要知道的是,Cortex-M3提供了兩個棧指針(PSP和MSP),進入異常時強制使用MSP,執行正常程序時可以通過在退出異常前設置LR的值決定是使用MSP還是PSP。發生異常前使用的PSP,處理器就會把相關的寄存器壓入到PSP對應的棧中;發生異常前使用的MSP。處理器就會把相關的寄存器壓入到MSP對應的棧中。CPU復位后會自動使用MSP,在第一次切換任務前使用的都是MSP。在一般的RTOS設計中,所有的任務都使用PSP,異常使用MSP。
對于棧:為了使得任務能夠被正常恢復,我們需要給每個任務單獨分配占空間(RAM中的一片區域,一般是全局數組即可)。當我們切換任務時,我們需要把CPU未保存的寄存器保存到該任務對應的棧中,然后保存當前任務對應的棧指針。恢復下一個任務時,則需要首先拿到該任務對應的棧指針,然后恢復我們之前手動保存的寄存器信息。然后將PSP的值更改為當前的棧指針。在CPU退出異常后,會自動將之前保存的寄存器信息進行恢復(Cortex-M3可以設置退出異常后是使用MSP還是PSP,CPU會根據這個值進行寄存器信息的恢復,對于任務切換來說,就是將PSP對應的棧中的信息恢復到相對應的寄存器中)。
3 任務切換的實現
3.1 設計目標
設計目標:
- 構建一個最小的兩個任務切換運行的最小系統。
涉及要點:
- 怎樣跑起來第一個任務?
- 怎樣在兩個任務間切換運行?
切換至初始任務,步驟如下:
任務之間的切換過程(從任務1切換到任務2):
暫停運行任務1。
需要保存的狀態保存到當前棧(任務1的棧)。
切換當前棧為任務2的棧。
從當前棧(任務2的棧)中恢復狀態。
繼續運行任務2的代碼。
3.2 設計實現
任務的初始化:
任務的初始化:初始化任務為等待狀態。
切換至初始任務:
從前一任務切換至后一任務:
在PendSV中切換任務:
3.3 代碼實現
項目組織結構如下:
tinyOS.h:
switch.c:
/*************************************** Copyright (c)****************************************************** ** File name : switch.c ** Latest modified Date : 2016-06-01 ** Latest Version : 0.1 ** Descriptions : tinyOS任務切換中與CPU相關的函數。 ** **-------------------------------------------------------------------------------------------------------- ** Created by : 01課堂 lishutong ** Created date : 2016-06-01 ** Version : 1.0 ** Descriptions : The original version ** **-------------------------------------------------------------------------------------------------------- ** Copyright : 版權所有,禁止用于商業用途 ** Author Blog : http://ilishutong.com **********************************************************************************************************/ #include "tinyOS.h" #include "ARMCM3.h"// 在任務切換中,主要依賴了PendSV進行切換。PendSV其中的一個很重要的作用便是用于支持RTOS的任務切換。 // 實現方法為: // 1、首先將PendSV的中斷優先配置為最低。這樣只有在其它所有中斷完成后,才會觸發該中斷; // 實現方法為:向NVIC_SYSPRI2寫NVIC_PENDSV_PRI // 2、在需要中斷切換時,設置掛起位為1,手動觸發。這樣,當沒有其它中斷發生時,將會引發PendSV中斷。 // 實現方法為:向NVIC_INT_CTRL寫NVIC_PENDSVSET // 3、在PendSV中,執行任務切換操作。 #define NVIC_INT_CTRL 0xE000ED04 // 中斷控制及狀態寄存器 #define NVIC_PENDSVSET 0x10000000 // 觸發軟件中斷的值 #define NVIC_SYSPRI2 0xE000ED22 // 系統優先級寄存器 #define NVIC_PENDSV_PRI 0x000000FF // 配置優先級#define MEM32(addr) *(volatile unsigned long *)(addr) #define MEM8(addr) *(volatile unsigned char *)(addr)// 下面的代碼中,用到了C文件嵌入ARM匯編 // 基本語法為:__asm 返回值 函數名(參數聲明) {....}, 更具體的用法見Keil編譯器手冊,此處不再詳注。/********************************************************************************************************** ** Function name : PendSV_Handler ** Descriptions : PendSV異常處理函數。很有些會奇怪,看不到這個函數有在哪里調用。實際上,只要保持函數頭不變 ** void PendSV_Handler (), 在PendSV發生時,該函數會被自動調用 ** parameters : 無 ** Returned value : 無 ***********************************************************************************************************/ __asm void PendSV_Handler () { IMPORT currentTask // 使用import導入C文件中聲明的全局變量IMPORT nextTask // 類似于在C文文件中使用extern int variable// MRS Rx, PSP --- 將PSP堆棧寄存器的值傳送給Rx寄存器MRS R0, PSP // 獲取當前任務的堆棧指針// CBZ Rx, label -- 判斷Rx的值是否為0,如果為0則跳轉到指定標號處運行。否則繼續往下運行CBZ R0, PendSVHandler_nosave // if 這是由tTaskSwitch觸發的(此時,PSP肯定不會是0了,0的話必定是tTaskRunFirst)觸發// 不清楚的話,可以先看tTaskRunFirst和tTaskSwitch的實現// STMDB Rx!, {Rm-Rn} -- 將Rm-Rn之間的一堆寄存器寫到Rx中地址對應的內存處。每寫一個單元前,地址先自減4再寫,先寫Rm,最后寫Rn。寫完后將最后的地址保存到Rx寄存器中。STMDB R0!, {R4-R11} // 那么,我們需要將除異常自動保存的寄存器這外的其它寄存器自動保存起來{R4, R11}// 保存的地址是當前任務的PSP堆棧中,這樣就完整的保存了必要的CPU寄存器,便于下次恢復// 取currentTask這個變量符號的地址寫到R1!注意,不是取currentTask的值LDR R1, =currentTask // 保存好后,將最后的堆棧頂位置,保存到currentTask->stack處 // LDR R1, [R1] -- 從R1中的地址處,取32位,再寫到R1。也就是從currentTask的地址處,取32位值。由于currenTask是指針,這個操作也就是取currentTask的值到R1。由于currentTask指向了某個tTask結構,也就是說此時R1的值是某個tTask結構變量的起始地址。而由于stack位于tTask的起始處,所以tTask.stack的地址與tTask相同。此時R1就是currentTask中stack的地址LDR R1, [R1] // 由于stack處在結構體stack處的開始位置處,顯然currentTask和stack在內存中的起始// STR R0, [R1] -- 將R0的值寫到R1中地址處。也就是將STMDB最后的地址,寫到currentTask->stack處STR R0, [R1] // 地址是一樣的,這么做不會有任何問題PendSVHandler_nosave // 無論是tTaskSwitch和tTaskSwitch觸發的,最后都要從下一個要運行的任務的堆棧中恢復// CPU寄存器,然后切換至該任務中運行// 取currentTask的地址到R0LDR R0, =currentTask // 好了,準備切換了// 取nextTask的地址到R1 LDR R1, =nextTask // 從nextTask的地址處取32位值,也就是R2 <= nextTask的值。LDR R2, [R1] // 向currentTask的地址處寫nextTask的值,也就是實現currentTask <= nextTaskSTR R2, [R0] // 先將currentTask設置為nextTask,也就是下一任務變成了當前任務// 從currentTask指向的結構起始地址中取32位,由于stack成員位于結構體開始處,也就是R0 <= currentTask.stackLDR R0, [R2] // 然后,從currentTask中加載stack,這樣好知道從哪個位置取出CPU寄存器恢復運行// 前面取了堆棧地址currentTask.stack。下面就是從該地址(R0中的值)連續取若干個32位單元,恢復到R4~R11。這個順序和前面的STMDB恰好相反。LDMIA R0!, {R4-R11} // 恢復{R4, R11}。為什么只恢復了這么點,因為其余在退出PendSV時,硬件自動恢復// 恢復R4~R11后,我們需要切換到這個堆棧。所以將最后的R0地址,寫到PSP堆棧寄存器中 MSR PSP, R0 // 最后,恢復真正的堆棧指針到PSP // 下面的代碼,如果不懂,請忽略。只要知道是切換到PSP堆棧中。ORR LR, LR, #0x04 // 標記下返回標記,指明在退出LR時,切換到PSP堆棧中(PendSV使用的是MSP) BX LR // 最后返回,此時任務就會從堆棧中取出LR值,恢復到上次運行的位置 } /********************************************************************************************************** ** Function name : tTaskRunFirst ** Descriptions : 在啟動tinyOS時,調用該函數,將切換至第一個任務運行 ** parameters : 無 ** Returned value : 無 ***********************************************************************************************************/ void tTaskRunFirst() {// 這里設置了一個標記,PSP = 0, 用于與tTaskSwitch()區分,用于在PEND_SV// 中判斷當前切換是tinyOS啟動時切換至第1個任務,還是多任務已經跑起來后執行的切換__set_PSP(0);MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI; // 向NVIC_SYSPRI2寫NVIC_PENDSV_PRI,設置其為最低優先級MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL寫NVIC_PENDSVSET,用于PendSV// 可以看到,這個函數是沒有返回// 這是因為,一旦觸發PendSV后,將會在PendSV后立即進行任務切換,切換至第1個任務運行// 此后,tinyOS將負責管理所有任務的運行,永遠不會返回到該函數運行 }/********************************************************************************************************** ** Function name : tTaskSwitch ** Descriptions : 進行一次任務切換,tinyOS會預先配置好currentTask和nextTask, 然后調用該函數,切換至 ** nextTask運行 ** parameters : 無 ** Returned value : 無 ***********************************************************************************************************/ void tTaskSwitch() {// 和tTaskRunFirst, 這個函數會在某個任務中調用,然后觸發PendSV切換至其它任務// 之后的某個時候,將會再次切換到該任務運行,此時,開始運行該行代碼, 返回到// tTaskSwitch調用處繼續往下運行MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL寫NVIC_PENDSVSET,用于PendSV }main.cpp:
/*************************************** Copyright (c)****************************************************** ** File name : main.c ** Latest modified Date : 2016-06-01 ** Latest Version : 0.1 ** Descriptions : 主文件,包含應用代碼 ** **-------------------------------------------------------------------------------------------------------- ** Created by : 01課堂 lishutong ** Created date : 2016-06-01 ** Version : 1.0 ** Descriptions : The original version ** **-------------------------------------------------------------------------------------------------------- ** Copyright : 版權所有,禁止用于商業用途 ** Author Blog : http://ilishutong.com **********************************************************************************************************/ #include "tinyOS.h"// 當前任務:記錄當前是哪個任務正在運行 tTask * currentTask;// 下一個將即運行的任務:在進行任務切換前,先設置好該值,然后任務切換過程中會從中讀取下一任務信息 tTask * nextTask;// 所有任務的指針數組:簡單起見,只使用兩個任務 tTask * taskTable[2];/********************************************************************************************************** ** Function name : tTaskInit ** Descriptions : 初始化任務結構 ** parameters : task 要初始化的任務結構 ** parameters : entry 任務的入口函數 ** parameters : param 傳遞給任務的運行參數 ** Returned value : 無 ***********************************************************************************************************/ void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t * stack) {// 為了簡化代碼,tinyOS無論是在啟動時切換至第一個任務,還是在運行過程中在不同間任務切換// 所執行的操作都是先保存當前任務的運行環境參數(CPU寄存器值)的堆棧中(如果已經運行運行起來的話),然后再// 取出從下一個任務的堆棧中取出之前的運行環境參數,然后恢復到CPU寄存器// 對于切換至之前從沒有運行過的任務,我們為它配置一個“虛假的”保存現場,然后使用該現場恢復。// 注意以下兩點:// 1、不需要用到的寄存器,直接填了寄存器號,方便在IDE調試時查看效果;// 2、順序不能變,要結合PendSV_Handler以及CPU對異常的處理流程來理解*(--stack) = (unsigned long)(1<<24); // XPSR, 設置了Thumb模式,恢復到Thumb狀態而非ARM狀態運行*(--stack) = (unsigned long)entry; // 程序的入口地址*(--stack) = (unsigned long)0x14; // R14(LR), 任務不會通過return xxx結束自己,所以未用*(--stack) = (unsigned long)0x12; // R12, 未用*(--stack) = (unsigned long)0x3; // R3, 未用*(--stack) = (unsigned long)0x2; // R2, 未用*(--stack) = (unsigned long)0x1; // R1, 未用*(--stack) = (unsigned long)param; // R0 = param, 傳給任務的入口函數*(--stack) = (unsigned long)0x11; // R11, 未用*(--stack) = (unsigned long)0x10; // R10, 未用*(--stack) = (unsigned long)0x9; // R9, 未用*(--stack) = (unsigned long)0x8; // R8, 未用*(--stack) = (unsigned long)0x7; // R7, 未用*(--stack) = (unsigned long)0x6; // R6, 未用*(--stack) = (unsigned long)0x5; // R5, 未用*(--stack) = (unsigned long)0x4; // R4, 未用task->stack = stack; // 保存最終的值 }/********************************************************************************************************** ** Function name : tTaskSched ** Descriptions : 任務調度接口。tinyOS通過它來選擇下一個具體的任務,然后切換至該任務運行。 ** parameters : 無 ** Returned value : 無 ***********************************************************************************************************/ void tTaskSched () { // 這里的算法很簡單。// 一共有兩個任務。選擇另一個任務,然后切換過去if (currentTask == taskTable[0]) {nextTask = taskTable[1];}else {nextTask = taskTable[0];}tTaskSwitch(); }/********************************************************************************************************** ** 應用示例 ** 有兩個任務,分別執行task1Entry和task2Entry。功能是分別對相應的變量進行周期性置0置1,每次完成從1->0的切換后, ** 自動切換至另一個任務中運行。這樣便實現了兩個任務交替性的使用一段時間CPU,對相應變量值修改。 **********************************************************************************************************/ void delay (int count) {while (--count > 0); }int task1Flag; void task1Entry (void * param) {for (;;) {task1Flag = 1;delay(100);task1Flag = 0;delay(100);tTaskSched();} }int task2Flag; void task2Entry (void * param) {for (;;) {task2Flag = 1;delay(100);task2Flag = 0;delay(100);tTaskSched();} }// 任務1和任務2的任務結構,以及用于堆棧空間 tTask tTask1; tTask tTask2; tTaskStack task1Env[1024]; tTaskStack task2Env[1024];int main () {// 初始化任務1和任務2結構,傳遞運行的起始地址,想要給任意參數,以及運行堆棧空間tTaskInit(&tTask1, task1Entry, (void *)0x11111111, &task1Env[1024]);tTaskInit(&tTask2, task2Entry, (void *)0x22222222, &task2Env[1024]);// 接著,將任務加入到任務表中taskTable[0] = &tTask1;taskTable[1] = &tTask2;// 我們期望先運行tTask1, 也就是void task1Entry (void * param) nextTask = taskTable[0];// 切換到nextTask, 這個函數永遠不會返回tTaskRunFirst();return 0; }3.4 PendSV_Handler的另一種實現
不再將PSP=0:
- 芯片在上電啟動后,默認使用MSP作為堆棧的指針, 在這里我們直接將PSP = MSP。
不再判斷PSP=0?
上面提到過,在芯片啟動時,默認使用MSP堆棧。然后在tTaskRunFirst()中,我們設置了PSP=MSP。
第一次進入PendSV_Handler()時,已經設置了PSP=MSP,所以硬件自動將R0-R3等壓入MSP堆棧。同時STMDB R0!, {R4-R11}會將R4-R11壓入到MSP。系統跑起來之后,進入PendSV_Handler()前一直用的是PSP堆棧,所以硬件自動保存及STMDB保存的也是在PSP堆棧中。這樣一來就不用再判斷PSP = 0 ?的問題。
__asm void PendSV_Handler (void) { IMPORT saveAndLoadStackAddr// 切換第一個任務時,由于設置了PSP=MSP,所以下面的STMDB保存會將R4~R11// 保存到系統啟動時默認的堆棧中,而不是某個任務MRS R0, PSP STMDB R0!, {R4-R11} // 將R4~R11保存到當前任務棧,也就是PSP指向的堆棧BL saveAndLoadStackAddr // 調用函數:參數通過R0傳遞,返回值也通過R0傳遞 LDMIA R0!, {R4-R11} // 從下一任務的堆棧中,恢復R4~R11MSR PSP, R0MOV LR, #0xFFFFFFFD // 指明返回異常時使用PSP。注意,這時LR不是程序返回地址BX LR }下面的C代碼,用于替換原來的匯編代碼,更容易理解。
uint32_t saveAndLoadStackAddr (uint32_t stackAddr) {if (currentTask != (tTask *)0) { // 第一次切換時,當前任務為0currentTask->stack = (uint32_t *)stackAddr; // 所以不會保存}currentTask = nextTask; return (uint32_t)currentTask->stack; // 取下一任務堆棧地址 }參考資料:
總結
以上是生活随笔為你收集整理的任务的定义、任务切换的原理及实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鼠标不可移动怎么办 鼠标无法移动怎么处理
- 下一篇: vi常用命令汇总