從零開始構建實時操作系統—任務切換
1、前言
隨著計算機技術和微電子技術的迅速發展,嵌入式系統應用領域越來越廣泛,尤其是其具備低功耗技術的特點得到人們的重視。隨著工信部提出NB-IoT基站建設具體目標、三大運營商加速建設,即將迎來萬物互聯的新時代,這是信息產業繼移動互聯網之后的下一個萬億級市場,這些為實時操作系統的應用提供了廣闊的前景。
嵌入式實時操作系統將會部署到越來越多的設備中,這就要求工程師深入地了解嵌入式實時操作系統。本系列文章將和大家一起從零開始構建一個嵌入式實時操作系統,我將用最簡單直白的方式一步一步搭建,我將用一篇文章的方式來總結搭建中的每個節點階段,并開源軟件工程和源代碼。
2、嵌入式實時操作系統
嵌入式實時操作系統是一個特殊的程序,是一個支持多任務的運行環境。嵌入式實時操作系統最大的特點就是“實時性”,如果有一個任務需要執行,實時操作系統會立即執行該任務,不會有較長的延時。典型的實時操作系統有uCOS ,RT-Thread,FreeRTOS ,VxWorks,WinCE等。
嵌入式實時操作系統是一個特殊的程序(通常稱為內核),它可以創建和控制所有任務。嵌入式實時操作系統除了包含一個內核以外,還提供其他服務,如文件系統,協議棧,圖形用戶界面等。本文的重點在于了解嵌入式實時操作系統內核的工作原理和結構,因此文中提到的實時操作系統通常指的是操作系統內核。實時操作系統內核通常要占用5%左右的CPU運行時間,另外內核是一個軟件代碼,需要額外占用ROM空間和RAM空間。
嵌入式實時操作系主要由以下3個子系統組成:
- 任務調度子系統
- 任務通信子系統
- 內存管理子系統
3、實現目標
本文講解構建嵌入式實時操作系統的第一個節點階段:實現簡單的任務切換功能。
代碼區的數據是不變的,處理器寄存器的值和棧空間的值決定程序運行狀態。讓每個任務“獨享”一個棧空間,當我們將任務運行時的處理器寄存器的值保存起來時,這樣就實現保存任務的運行狀態。同樣的當我們把保存的任務運行時的處理器寄存器的值裝載到處理的寄存器中時,這樣就恢復了任務的運行狀態,任務繼續運行起來。
切換任務的原理是:每個任務有一個“獨享”棧空間,通過保存和裝載任務運行時的處理器寄存器的值,實現任務的暫停和恢復運行。暫停一個任務后再恢復另外一個任務就完成了一次任務切換。
任務代碼,任務棧空間和處理器狀態如下圖:
4、實驗環境
硬件是基于意法半導體的STM32F401(ARM公司的Cortex-M4內核),軟件開發使用的是KEIL V5.2 開發工具。
軟件工程如下:
軟件工程中包含:main.c ,startup_stm32f401xc.s 和 readme三個文件。startup_stm32f401xc.s文件為STM32F401的啟動文件,main.c文件實現任務切換功能,readme文件用于記錄版本修改日志。
5、代碼實現
切換任務的原理是讓每個任務都有一個“獨享”棧空間,通過保存和裝載任務運行時的處理器寄存器的值,實現任務的暫停和恢復運行。暫停一個任務后再恢復另外一個任務就完成了一次任務切換。
因此需要實現:
- 每個任務的獨立棧空間。
- 實現任務的暫停和恢復。
- 實現任務的調度。
(1)實現獨立棧空間
棧空間代碼如下:
為每個任務定義一個靜態數組,當任務運行時將處理器的棧指針指向任務“自己的”靜態數組,從而實現獨立棧空間。棧空間用來存放局部變量,中斷調用和函數調用時的處理器寄存器的值。任務切換時需要將處理器寄存器的值保存到任務的獨立棧空間。
在保存任務運行狀態時需要保存處理器寄存器值到棧空間,因此需要深入了解處理器寄存器的用途和出入棧順序,Cortex-M4內核的寄存器和寄存器中斷自動入棧的順序圖如下:
初始化棧空間的代碼如下:
棧空間初始化后的狀態如下:
棧是一中先入后出的數據結構,Cortex-M4內核的棧操作方式倍設置成了向下生長。psp_array用于保存任務棧指針,psp_array[0]任務0棧指針指向task0_stack[112],其中task0_stack[116]保存PC程序指針值,task0_stack[117]保存狀態寄存器(符合Cortex-M4內核寄存器出棧順序:手動出棧8個寄存器,硬件自動出棧8個寄存器)。
(2)實現任務的暫停和恢復
代碼如下:
cortex-M4內核有一個PendSV(可掛起的系統調用)異常,其異常編號為14并且具有可編程的優先級。當軟件將PendSV設置成掛起時,程序將進入PendSV異常(中斷)。
將PendSV異常優先級設置為最低,其它中斷函數都可以得到正常響應,不會受到PendSV異常影響,在PendSV異常中執行任務切換,時序框圖如下:
PendSV_Handler為Cortex-M4內核中斷服務函數,進入中斷函數時處理器自動保存了R0,R1,R2,R3, R12,LR,PC,XPSR,在PendSV_Handler中斷程序中完成R4~R11入棧保存工作,從而實現任務保存工作。
/* 讀取當前進程棧指針數值 */
MRS R0,PSP
/* 保存R4-R11八個寄存器的值到當前任務棧中 同時將回寫的地址寫入R0 */
STMDB R0!,{R4-R11}
psp_array[0]為任務0的棧指針, psp_array[1]為任務1的棧指針。以下代碼實現任務棧指針切換。
/* 讀取psp_array 地址 */
LDR R3, =__cpp(&psp_array)
/* 將當前進程PSP指針值 寫入 相應的 PSP_array 位置 */
STR R0,[R3,R2,LSL #2]
/* 獲取下個進程序號 */
LDR R4,=__cpp(&next_task)
LDR R4,[R4]
/* R1為&curr_task 將下個進程序號寫入curr_task中 */
STR R4,[R1]
/* psp_array讀取更新后的curr_task的PSP指針數值 */
LDR R0,[R3,R4,LSL #2]
在PendSV_Handler中斷程序中完成R4~R11寄存器出棧,PendSV_Handler中斷程序返回時處理器自動出棧R0,R1,R2,R3, R12,LR,PC,XPSR,從而實現任務恢復工作。
/* 出棧 R4-R11八個寄存器 */
LDMIA R0!,{R4-R11}
/* 設置PSP指針 */
MSR PSP,R0
/* 中斷返回 */
BX LR
(3)實現任務的調度
任務調度的代碼如下:
SysTick_Handler為定時器中斷程序,實現時間片輪流改變目標任務,并掛起PendSV_Handle中斷,退出SysTick_Handler中斷程序時進入PendSV_Handle中斷程序。
6、運行結果
代碼仿真運行如下:
運行代碼后task_num0和task_num1這兩個變量依次自加,代碼實現任務輪流切換功能。