详解基于 Cortex-M3 的任务调度(下)
文章目錄
- 工程說明
- 實驗結果
- 代碼講解
- 時鐘節拍
- 任務切換 task_switch()
- PendSV_Handler
- 任務的代碼
- 重要的全局變量
- main() 函數
- 代碼下載
在 詳解基于 Cortex-M3 的任務調度(上)_車子(chezi)-CSDN博客 這篇文章中,我們已經有了理論基礎,這篇文章,我們寫代碼實踐一下。
代碼基于網友提供的工程和書中的參考代碼修改而成,不求面面俱到,只求講清原理。
工程說明
我用的是 STM32F103 這款芯片,工程結構如圖:
User 下面是串口的裸板驅動,調用官方的庫函數,非常套路化;
Cortex-M3 下面是廠家提供的文件,一般不用修改;
OS 下面是本文的重點,任務調度的精髓就在里面;
Compiler 下面是我添加的組件,由 ARM 提供,也不用修改。方便沒有板子的時候也可以用 PC 模擬運行。
實驗結果
結果就是 4 個任務輪流執行。雖然每個任務代碼中都有 while(1),但是它不會一直占用 CPU,當它的時間片到了,OS 就會剝奪它對 CPU 的使用權,讓下一個任務運行。
如果你有板子,那么就用串口輸出。需要在 RTE_Components.h 文件中注釋掉這兩行
//#define RTE_Compiler_IO_STDOUT /* Compiler I/O: STDOUT */ //#define RTE_Compiler_IO_STDOUT_ITM /* Compiler I/O: STDOUT ITM */如果沒有板子,在仿真的時候,打開 Debug(printf) Viewer 窗口就可以了。
代碼講解
時鐘節拍
上一篇博文說過,系統滴答定時器(SYSTICK)中斷是要有的,在這個中斷里面觸發任務切換。
void OSSysTickInit(void) { //Systick定時器初使化char *Systick_priority = (char *)0xe000ed23; //Systick中斷優先級寄存器SysTick->LOAD = (SystemCoreClock/8/1000000)* 1000; //Systick定時器重裝載 計數9000次=1ms *Systick_priority = 0x00; //Systick定時器中斷優先級SysTick->VAL = 0; //Systick定時器計數器清0SysTick->CTRL = 0x3;//Systick打開并使能中斷,且使用外部晶振時鐘,8分頻 72MHz/8=9MHz 計數9000次=1ms 計數9次=1us }配置 SYSTICK 的計數頻率,然后使能 SYSTICK 和中斷。
如果 SystemCoreClock 是 72MHz,就是 1ms 一次中斷。
void SysTick_Handler(void) // 1KHz { System.TimeMS++; //系統時鐘節拍累加if((--System.TaskTimeSlice) == 0) { System.TaskTimeSlice = TASK_TIME_SLICE;//重置時間片初值task_switch();} }以上就是 SYSTICK 中斷處理函數。System.TaskTimeSlice 的初始值是 10;
在 main() 中有初始化的語句:
System.TaskTimeSlice = TASK_TIME_SLICE; // #define TASK_TIME_SLICE 10 System.OSRunning = OS_TRUE;System.TimeMS = 0;也就是說 10ms 切換一次任務。
注意,這是基于時間片的任務調度,而不是基于優先級。
任務切換 task_switch()
void task_switch(void) {if(System.OSLockNesting != 0) return;switch(curr_task) {case(0): next_task=1; break;case(1): next_task=2; break;case(2): next_task=3; break;case(3): next_task=0; break;default: next_task=0;stop_cpu;break; // Should not be here}if (curr_task != next_task){ // Context switching neededSCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; // Set PendSV to pending}}第 3 行:判斷是否給調度器加鎖了,如果是,就禁止任務切換,直接返回。
切換的邏輯很簡單,只有 0-3 個任務, 0 切到 1,1 切到 2,…。next_task 是個全局變量,記錄下一個任務編號。
第 26 行很重要,設置 PendSV 中斷懸起。當 SYSTICK 中斷服務退出后,馬上就會進入 PendSV 中斷服務。
這個函數雖然叫 task_switch,但是真正的切換是在 PendSV 中斷服務里面。
PendSV_Handler
__asm void PendSV_Handler(void) { // Simple version - assume No floating point support// Save current contextMRS R0, PSP // Get current process stack pointer valueSTMDB R0!,{R4-R11} // Save R4 to R11 in task stack (8 regs)LDR R1,=__cpp(&curr_task)LDR R2,[R1] // Get current task IDLDR R3,=__cpp(&PSP_array)STR R0,[R3, R2, LSL #2] // Save PSP value into PSP_array// Load next contextLDR R4,=__cpp(&next_task)LDR R4,[R4] // Get next task IDSTR R4,[R1] // Set curr_task = next_taskLDR R0,[R3, R4, LSL #2] // Load PSP value from PSP_arrayLDMIA R0!,{R4-R11} // Load R4 to R11 from taskstack (8 regs)MSR PSP, R0 // Set PSP to next taskBX LR // ReturnALIGN 4 }這段代碼雖然短,但它是任務切換的精髓,簡而言之就是保存當前任務的上下文,加載下一個任務的上下文。
在 PendSV_Handler 發生后,會有 8 個寄存器被自動壓棧,7-8 行用來手動壓棧另外 8 個寄存器。
我們一句一句說。
第 7 行:MRS R0, PSP
加載棧指針到 R0,也就是找到當前任務的棧
第 8 行:STMDB R0!,{R4-R11}
R0 = R0-4, 把 R11 的值存入 R0 指向的內存;然后 R0 = R0-4,把 R10 的值存入 R0 指向的內存;…
為了更加直觀,我弄了一幅圖:
這張圖是剛壓棧后的情況,可以看到,R4 是最后被壓進去的。右邊的方框展示的是某個任務的棧。
第 9 行:LDR R1,=__cpp(&curr_task)
這句話的意思是把變量 curr_task 的地址賦值給 R1
第 10 行:LDR R2,[R1] // Get current task ID
取 R1 指向的內容給 R2,也就是獲得當前任務的編號
第 11 行:LDR R3,=__cpp(&PSP_array)
取數組 PSP_array[] 的地址給 R3
第 12 行:STR R0,[R3, R2, LSL #2]
相當于偽碼 STR R0,[R3, R2<<2],也就是 STR R0,[R3 + R2*4]
因為 R2 里面是當前任務的編號,所以 [R3 + R2*4] 是根據任務編號索引 PSP_array 數組(每個元素占 4 個字節),意思是把 R0 的值保存到 PSP_array[R2] ,結合 R0 指向當前任務的棧,就是要把當前任務的棧指針保存到數組中。
上面這一番操作,其實是保存當前任務的上下文。
我們繼續看代碼:
LDR R4,=__cpp(&next_task)LDR R4,[R4] // Get next task IDSTR R4,[R1] // Set curr_task = next_taskLDR R0,[R3, R4, LSL #2] // Load PSP value from PSP_arrayLDMIA R0!,{R4-R11} // Load R4 to R11 from taskstack (8 regs)MSR PSP, R0 // Set PSP to next taskBX LR // Return1:取得變量 next_task 的地址給 R4
2:把 R4 指向的內容給 R4,也就是得到下一個任務的編號
3:存儲 R4 的值到 R1 指向的內存,R1 是 curr_task 的指針,所以就是把下一個任務的編號賦值給變量 curr_task ,用 C 語言表示就是 curr_task = next_task;
4:相當于 LDR R0,[R3, R4*4],即以 R4 的值為下標索引 PSP_array 數組,把里面的值給 R0,連起來就是獲得下一個任務的 PSP 到 R0
5:手動出棧,把下一個任務的棧上面的 8 個值恢復到對應的寄存器。剩下 8 個怎么辦?會在中斷返回的時候自動出棧。IA 表示每次傳送后地址增加 4,出棧順序是先 R4, 再 R5,…,最后 R11
6:用 R0 調整棧指針 PSP,為后面的自動出棧做準備
7:啟動異常返回流程
以上語句執行后,就會切換到下一個任務。
任務的代碼
void task0(void) //任務0 {while(1) {OSprintf("Task0 is running\r\n"); OS_delayMs(500); //任務延時 } }void task1(void) //任務1 { while(1) { OSprintf("Task1 is running\r\n"); OS_delayMs(1000); //任務延時 } } void task2(void) //任務2 {while(1) {OSprintf("Task2 is running\r\n");OS_delayMs(2000); //任務延時 } }void task3(void) //任務3 {while(1) {OSprintf("Task3 is running\r\n"); OS_delayMs(4000); //任務延時 } }很傻瓜地搞了四個任務,每個任務都向串口輸出一句話。
OSprintf 中有一個給調度器上鎖和解鎖的操作,防止每個任務的打印混淆在一起。有關的代碼是:
#define OS_CORE_ENTER __disable_irq #define OS_CORE_EXIT __enable_irq#define OSprintf(fmt, ...) \ { OSSchedLock(); printf( fmt, ##__VA_ARGS__); OSSchedUnlock();}//系統布爾值 #define OS_FALSE 0 #define OS_TRUE 1 //系統變量類型定義 typedef struct {INT8U OSRunning; //運行標志變量INT8U OSLockNesting; //任務切換鎖定層數統計變量 volatile INT32U TimeMS; //系統時鐘節拍累計變量INT32U TaskTimeSlice; //任務時間片 }SYSTEM;//系統變量 SYSTEM System;void OSSchedLock(void) //任務切換鎖定 {OS_CORE_ENTER(); // 關中斷if(System.OSRunning == OS_TRUE) { if (System.OSLockNesting < 255u) // 任務鎖定可以最大嵌套 255 層System.OSLockNesting++; }OS_CORE_EXIT(); // 開中斷 } void OSSchedUnlock(void) //任務切換解鎖 {OS_CORE_ENTER(); if(System.OSRunning == OS_TRUE){ if (System.OSLockNesting > 0) System.OSLockNesting--; }OS_CORE_EXIT(); } INT32U GetTime(void) {return System.TimeMS; }void OS_delayMs(INT32U ms) {INT32U counter;counter = GetTime() + ms;while(1){if(counter < GetTime()) break;} }OS_delayMs 這個函數有點問題,沒有考慮到定時器的溢出。另外,OS_delayMs 這個函數不會掛起當前任務。比較好的做法是當任務調用這個函數的時候,主動放棄 CPU,這時候 CPU 可以選擇其他任務執行。當延時時間到了,被掛起任務再恢復到就緒態。
重要的全局變量
// Stack for each task (4K bytes each) unsigned int task0_stack[1024], task1_stack[1024],task2_stack[1024], task3_stack[1024];// Data use by OS uint32_t curr_task = 0; // Current task uint32_t next_task = 1; // Next task uint32_t PSP_array[4]; // Process Stack Pointer for each task2-5:定義了 4 個數組,分別對應 4 個任務的棧
10-11:curr_task 記錄當前任務的編號,next_task 記錄下一個任務的編號
12:數組 PSP_array 用來保存每個任務的棧指針。
其實管理任務應該有個 TCB(任務控制塊),但是我們的代碼比較簡陋(防止喧賓奪主),所以就用這些全局變量對付了。
main() 函數
鋪墊了那么多,終于來到主函數。
#define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS)))int main(void) {USART1_Config(115200); //串口1初使化System.TaskTimeSlice = TASK_TIME_SLICE; // 設置時間片為 10msSystem.OSRunning = OS_TRUE;System.TimeMS = 0; SCB->CCR |= SCB_CCR_STKALIGN_Msk; // Enable double word stack alignment//(recommended in Cortex-M3 r1p1, default in Cortex-M3 r2px and Cortex-M4)// Create stack frame for task0PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;HW32_REG((PSP_array[0] + (14<<2))) = (unsigned long) task0;// initial Program CounterHW32_REG((PSP_array[0] + (15<<2))) = 0x01000000; // initial xPSR// Create stack frame for task1PSP_array[1] = ((unsigned int) task1_stack) + (sizeof task1_stack) - 16*4;HW32_REG((PSP_array[1] + (14<<2))) = (unsigned long) task1;// initial Program CounterHW32_REG((PSP_array[1] + (15<<2))) = 0x01000000; // initial xPSR// Create stack frame for task2PSP_array[2] = ((unsigned int) task2_stack) + (sizeof task2_stack) - 16*4;HW32_REG((PSP_array[2] + (14<<2))) = (unsigned long) task2;// initial Program CounterHW32_REG((PSP_array[2] + (15<<2))) = 0x01000000; // initial xPSR// Create stack frame for task3PSP_array[3] = ((unsigned int) task3_stack) + (sizeof task3_stack) - 16*4;HW32_REG((PSP_array[3] + (14<<2))) = (unsigned long) task3;// initial Program CounterHW32_REG((PSP_array[3] + (15<<2))) = 0x01000000; // initial xPSRcurr_task = 0; // Switch to task #0 (Current task)__set_PSP((PSP_array[curr_task] + 16*4)); // Set PSP to top of task 0 stackNVIC_SetPriority(PendSV_IRQn, 0xFF); // Set PendSV to lowest possible priorityOSSysTickInit(); __set_CONTROL(0x3); // Switch to use Process Stack, unprivileged state__ISB(); // Execute ISB after changing CONTROL (architectural recommendation)task0(); // Start task 0while(1){stop_cpu;// Should not be here};}比較重要的是創建每個任務的棧幀,比如
// Create stack frame for task0PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;HW32_REG((PSP_array[0] + (14<<2))) = (unsigned long) task0;// initial Program CounterHW32_REG((PSP_array[0] + (15<<2))) = 0x01000000; // initial xPSR當在 PendSV_Handler 中進行切換的時候,要手動出棧 8 個寄存器(藍色),另外 8 個寄存器(紅色)會自動出棧,對于要切換的任務(將要運行的任務),它的棧指針應該指向 R4
所以第 2 行:PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;
后面減去 16*4 表示要預留出這 16 個寄存器的位置,把 PSP 指向 R4
這 16 個寄存器中有 2 個要給初始值,一個是 PC,要指向任務的入口函數;還有一個是 xPSR
xPSR 的 bit[24] 必須是 1,表示 Thumb state,所以會有代碼
HW32_REG((PSP_array[0] + (15<<2))) = 0x01000000; // initial xPSR
繼續看代碼
curr_task = 0; // Switch to task #0 (Current task)__set_PSP((PSP_array[curr_task] + 16*4)); // Set PSP to top of task 0 stackNVIC_SetPriority(PendSV_IRQn, 0xFF); // Set PendSV to lowest possible priorityOSSysTickInit(); // SysTick 初始化和使能__set_CONTROL(0x3); // Switch to use Process Stack, unprivileged state__ISB(); // Execute ISB after changing CONTROL (architectural recommendation)task0(); // Start task 0第 2 行:因為前面設置好了棧幀,PSP_array[0] 其實是指向 task0 的棧(上面圖中 R4 的位置),但是 task0 先運行,它不是在 PendSV_Handler 中切換過去的,而是通過調函數 task0() 來開始執行的,所以它的棧應該是空的,也就是要把它的 PSP 調整到最高處,所以要加上 16*4
第 3 行:把 PendSV_Handler 設置成最低的優先級,為什么這樣,可以看我的前一篇博文:詳解基于 Cortex-M3 的任務調度(上)_車子(chezi)-CSDN博客
第 6 行:使用 PSP,且運行在非特權級
第 7 行:指令同步屏障。用來清空流水線,確保在執行新的指令前,前面的指令都已執行完畢。
第 8 行:執行 task0。其實執行第一個任務還有別的方法,比如觸發 PendSV_Handler,在中斷里面“切換”到 task0
以上就是本文全部內容,歡迎讀者批評指正。
代碼下載
鏈接:https://pan.baidu.com/s/1dnl7Cld97hujA3OoxGfd3Q
提取碼:chez
參考資料
【1】《Cortex-M3 權威指南 》
【2】《The De?nitive Guide to ARM Cortex-M3 and Cortex-M4 Processors(Third Edition)》
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的详解基于 Cortex-M3 的任务调度(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 详解基于 Cortex-M3 的任务调度
- 下一篇: 是什么缩写_网友:啊啊啊啊这是什么该死的