详解基于 Cortex-M3 的任务调度(下)
文章目錄
- 工程說明
- 實(shí)驗(yàn)結(jié)果
- 代碼講解
- 時鐘節(jié)拍
- 任務(wù)切換 task_switch()
- PendSV_Handler
- 任務(wù)的代碼
- 重要的全局變量
- main() 函數(shù)
- 代碼下載
在 詳解基于 Cortex-M3 的任務(wù)調(diào)度(上)_車子(chezi)-CSDN博客 這篇文章中,我們已經(jīng)有了理論基礎(chǔ),這篇文章,我們寫代碼實(shí)踐一下。
代碼基于網(wǎng)友提供的工程和書中的參考代碼修改而成,不求面面俱到,只求講清原理。
工程說明
我用的是 STM32F103 這款芯片,工程結(jié)構(gòu)如圖:
User 下面是串口的裸板驅(qū)動,調(diào)用官方的庫函數(shù),非常套路化;
Cortex-M3 下面是廠家提供的文件,一般不用修改;
OS 下面是本文的重點(diǎn),任務(wù)調(diào)度的精髓就在里面;
Compiler 下面是我添加的組件,由 ARM 提供,也不用修改。方便沒有板子的時候也可以用 PC 模擬運(yùn)行。
實(shí)驗(yàn)結(jié)果
結(jié)果就是 4 個任務(wù)輪流執(zhí)行。雖然每個任務(wù)代碼中都有 while(1),但是它不會一直占用 CPU,當(dāng)它的時間片到了,OS 就會剝奪它對 CPU 的使用權(quán),讓下一個任務(wù)運(yùn)行。
如果你有板子,那么就用串口輸出。需要在 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 窗口就可以了。
代碼講解
時鐘節(jié)拍
上一篇博文說過,系統(tǒng)滴答定時器(SYSTICK)中斷是要有的,在這個中斷里面觸發(fā)任務(wù)切換。
void OSSysTickInit(void) { //Systick定時器初使化char *Systick_priority = (char *)0xe000ed23; //Systick中斷優(yōu)先級寄存器SysTick->LOAD = (SystemCoreClock/8/1000000)* 1000; //Systick定時器重裝載 計(jì)數(shù)9000次=1ms *Systick_priority = 0x00; //Systick定時器中斷優(yōu)先級SysTick->VAL = 0; //Systick定時器計(jì)數(shù)器清0SysTick->CTRL = 0x3;//Systick打開并使能中斷,且使用外部晶振時鐘,8分頻 72MHz/8=9MHz 計(jì)數(shù)9000次=1ms 計(jì)數(shù)9次=1us }配置 SYSTICK 的計(jì)數(shù)頻率,然后使能 SYSTICK 和中斷。
如果 SystemCoreClock 是 72MHz,就是 1ms 一次中斷。
void SysTick_Handler(void) // 1KHz { System.TimeMS++; //系統(tǒng)時鐘節(jié)拍累加if((--System.TaskTimeSlice) == 0) { System.TaskTimeSlice = TASK_TIME_SLICE;//重置時間片初值task_switch();} }以上就是 SYSTICK 中斷處理函數(shù)。System.TaskTimeSlice 的初始值是 10;
在 main() 中有初始化的語句:
System.TaskTimeSlice = TASK_TIME_SLICE; // #define TASK_TIME_SLICE 10 System.OSRunning = OS_TRUE;System.TimeMS = 0;也就是說 10ms 切換一次任務(wù)。
注意,這是基于時間片的任務(wù)調(diào)度,而不是基于優(yōu)先級。
任務(wù)切換 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 行:判斷是否給調(diào)度器加鎖了,如果是,就禁止任務(wù)切換,直接返回。
切換的邏輯很簡單,只有 0-3 個任務(wù), 0 切到 1,1 切到 2,…。next_task 是個全局變量,記錄下一個任務(wù)編號。
第 26 行很重要,設(shè)置 PendSV 中斷懸起。當(dāng) SYSTICK 中斷服務(wù)退出后,馬上就會進(jìn)入 PendSV 中斷服務(wù)。
這個函數(shù)雖然叫 task_switch,但是真正的切換是在 PendSV 中斷服務(wù)里面。
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 }這段代碼雖然短,但它是任務(wù)切換的精髓,簡而言之就是保存當(dāng)前任務(wù)的上下文,加載下一個任務(wù)的上下文。
在 PendSV_Handler 發(fā)生后,會有 8 個寄存器被自動壓棧,7-8 行用來手動壓棧另外 8 個寄存器。
我們一句一句說。
第 7 行:MRS R0, PSP
加載棧指針到 R0,也就是找到當(dāng)前任務(wù)的棧
第 8 行:STMDB R0!,{R4-R11}
R0 = R0-4, 把 R11 的值存入 R0 指向的內(nèi)存;然后 R0 = R0-4,把 R10 的值存入 R0 指向的內(nèi)存;…
為了更加直觀,我弄了一幅圖:
這張圖是剛壓棧后的情況,可以看到,R4 是最后被壓進(jìn)去的。右邊的方框展示的是某個任務(wù)的棧。
第 9 行:LDR R1,=__cpp(&curr_task)
這句話的意思是把變量 curr_task 的地址賦值給 R1
第 10 行:LDR R2,[R1] // Get current task ID
取 R1 指向的內(nèi)容給 R2,也就是獲得當(dāng)前任務(wù)的編號
第 11 行:LDR R3,=__cpp(&PSP_array)
取數(shù)組 PSP_array[] 的地址給 R3
第 12 行:STR R0,[R3, R2, LSL #2]
相當(dāng)于偽碼 STR R0,[R3, R2<<2],也就是 STR R0,[R3 + R2*4]
因?yàn)?R2 里面是當(dāng)前任務(wù)的編號,所以 [R3 + R2*4] 是根據(jù)任務(wù)編號索引 PSP_array 數(shù)組(每個元素占 4 個字節(jié)),意思是把 R0 的值保存到 PSP_array[R2] ,結(jié)合 R0 指向當(dāng)前任務(wù)的棧,就是要把當(dāng)前任務(wù)的棧指針保存到數(shù)組中。
上面這一番操作,其實(shí)是保存當(dāng)前任務(wù)的上下文。
我們繼續(xù)看代碼:
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 指向的內(nèi)容給 R4,也就是得到下一個任務(wù)的編號
3:存儲 R4 的值到 R1 指向的內(nèi)存,R1 是 curr_task 的指針,所以就是把下一個任務(wù)的編號賦值給變量 curr_task ,用 C 語言表示就是 curr_task = next_task;
4:相當(dāng)于 LDR R0,[R3, R4*4],即以 R4 的值為下標(biāo)索引 PSP_array 數(shù)組,把里面的值給 R0,連起來就是獲得下一個任務(wù)的 PSP 到 R0
5:手動出棧,把下一個任務(wù)的棧上面的 8 個值恢復(fù)到對應(yīng)的寄存器。剩下 8 個怎么辦?會在中斷返回的時候自動出棧。IA 表示每次傳送后地址增加 4,出棧順序是先 R4, 再 R5,…,最后 R11
6:用 R0 調(diào)整棧指針 PSP,為后面的自動出棧做準(zhǔn)備
7:啟動異常返回流程
以上語句執(zhí)行后,就會切換到下一個任務(wù)。
任務(wù)的代碼
void task0(void) //任務(wù)0 {while(1) {OSprintf("Task0 is running\r\n"); OS_delayMs(500); //任務(wù)延時 } }void task1(void) //任務(wù)1 { while(1) { OSprintf("Task1 is running\r\n"); OS_delayMs(1000); //任務(wù)延時 } } void task2(void) //任務(wù)2 {while(1) {OSprintf("Task2 is running\r\n");OS_delayMs(2000); //任務(wù)延時 } }void task3(void) //任務(wù)3 {while(1) {OSprintf("Task3 is running\r\n"); OS_delayMs(4000); //任務(wù)延時 } }很傻瓜地搞了四個任務(wù),每個任務(wù)都向串口輸出一句話。
OSprintf 中有一個給調(diào)度器上鎖和解鎖的操作,防止每個任務(wù)的打印混淆在一起。有關(guān)的代碼是:
#define OS_CORE_ENTER __disable_irq #define OS_CORE_EXIT __enable_irq#define OSprintf(fmt, ...) \ { OSSchedLock(); printf( fmt, ##__VA_ARGS__); OSSchedUnlock();}//系統(tǒng)布爾值 #define OS_FALSE 0 #define OS_TRUE 1 //系統(tǒng)變量類型定義 typedef struct {INT8U OSRunning; //運(yùn)行標(biāo)志變量INT8U OSLockNesting; //任務(wù)切換鎖定層數(shù)統(tǒng)計(jì)變量 volatile INT32U TimeMS; //系統(tǒng)時鐘節(jié)拍累計(jì)變量INT32U TaskTimeSlice; //任務(wù)時間片 }SYSTEM;//系統(tǒng)變量 SYSTEM System;void OSSchedLock(void) //任務(wù)切換鎖定 {OS_CORE_ENTER(); // 關(guān)中斷if(System.OSRunning == OS_TRUE) { if (System.OSLockNesting < 255u) // 任務(wù)鎖定可以最大嵌套 255 層System.OSLockNesting++; }OS_CORE_EXIT(); // 開中斷 } void OSSchedUnlock(void) //任務(wù)切換解鎖 {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 這個函數(shù)有點(diǎn)問題,沒有考慮到定時器的溢出。另外,OS_delayMs 這個函數(shù)不會掛起當(dāng)前任務(wù)。比較好的做法是當(dāng)任務(wù)調(diào)用這個函數(shù)的時候,主動放棄 CPU,這時候 CPU 可以選擇其他任務(wù)執(zhí)行。當(dāng)延時時間到了,被掛起任務(wù)再恢復(fù)到就緒態(tài)。
重要的全局變量
// 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 個數(shù)組,分別對應(yīng) 4 個任務(wù)的棧
10-11:curr_task 記錄當(dāng)前任務(wù)的編號,next_task 記錄下一個任務(wù)的編號
12:數(shù)組 PSP_array 用來保存每個任務(wù)的棧指針。
其實(shí)管理任務(wù)應(yīng)該有個 TCB(任務(wù)控制塊),但是我們的代碼比較簡陋(防止喧賓奪主),所以就用這些全局變量對付了。
main() 函數(shù)
鋪墊了那么多,終于來到主函數(shù)。
#define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS)))int main(void) {USART1_Config(115200); //串口1初使化System.TaskTimeSlice = TASK_TIME_SLICE; // 設(shè)置時間片為 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};}比較重要的是創(chuàng)建每個任務(wù)的棧幀,比如
// 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當(dāng)在 PendSV_Handler 中進(jìn)行切換的時候,要手動出棧 8 個寄存器(藍(lán)色),另外 8 個寄存器(紅色)會自動出棧,對于要切換的任務(wù)(將要運(yùn)行的任務(wù)),它的棧指針應(yīng)該指向 R4
所以第 2 行:PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;
后面減去 16*4 表示要預(yù)留出這 16 個寄存器的位置,把 PSP 指向 R4
這 16 個寄存器中有 2 個要給初始值,一個是 PC,要指向任務(wù)的入口函數(shù);還有一個是 xPSR
xPSR 的 bit[24] 必須是 1,表示 Thumb state,所以會有代碼
HW32_REG((PSP_array[0] + (15<<2))) = 0x01000000; // initial xPSR
繼續(xù)看代碼
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 行:因?yàn)榍懊嬖O(shè)置好了棧幀,PSP_array[0] 其實(shí)是指向 task0 的棧(上面圖中 R4 的位置),但是 task0 先運(yùn)行,它不是在 PendSV_Handler 中切換過去的,而是通過調(diào)函數(shù) task0() 來開始執(zhí)行的,所以它的棧應(yīng)該是空的,也就是要把它的 PSP 調(diào)整到最高處,所以要加上 16*4
第 3 行:把 PendSV_Handler 設(shè)置成最低的優(yōu)先級,為什么這樣,可以看我的前一篇博文:詳解基于 Cortex-M3 的任務(wù)調(diào)度(上)_車子(chezi)-CSDN博客
第 6 行:使用 PSP,且運(yùn)行在非特權(quán)級
第 7 行:指令同步屏障。用來清空流水線,確保在執(zhí)行新的指令前,前面的指令都已執(zhí)行完畢。
第 8 行:執(zhí)行 task0。其實(shí)執(zhí)行第一個任務(wù)還有別的方法,比如觸發(fā) PendSV_Handler,在中斷里面“切換”到 task0
以上就是本文全部內(nèi)容,歡迎讀者批評指正。
代碼下載
鏈接:https://pan.baidu.com/s/1dnl7Cld97hujA3OoxGfd3Q
提取碼:chez
參考資料
【1】《Cortex-M3 權(quán)威指南 》
【2】《The De?nitive Guide to ARM Cortex-M3 and Cortex-M4 Processors(Third Edition)》
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的详解基于 Cortex-M3 的任务调度(下)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 详解基于 Cortex-M3 的任务调度
- 下一篇: 是什么缩写_网友:啊啊啊啊这是什么该死的