本篇文章講述了任務的三大元素:任務控制塊、任務棧、任務入口函數,并講述了編寫RTOS任務入口函數時三個重要的注意點。
1. 知識點回顧
在正式開始講解內容之前,我會先回顧一下基礎知識點,請確保你已經了解并掌握。
1.1. 任務的創建方法
在用戶層調用API創建一個任務,通常的流程如下:
① 創建一個數組作為任務棧:
#define TASK1_STACK_SIZE 512
k_stack_t task1_stack[TASK1_STACK_SIZE];
② 創建一個任務控制塊:
k_task_t task1;
③ 編寫任務入口函數:
void task1_entry(void *arg)
{
while(1)
{
printf("task1 is runningrn");
tos_task_delay(1000);
}
}
④ 調用系統API創建任務:
ret = tos_task_create(&task1,
"task1",
task1_entry,
NULL,
TASK1_PRO,
task1_stack,
TASK1_STACK_SIZE,
10);
創建之后任務為就緒態(處于系統就緒隊列中),等待系統調度器調度執行。
1.2. STM32內存分布
閱讀之后,你應該要知道,STM32(Cortex-M3)中Flash和SRAM的內存空間如下:
其中Flash存儲空間中又分為文本段、只讀數據段、復制數據段:
其中SRAM存儲空間中又分為data數據段、bss數據段、堆空間、棧空間:
并且還要知道不同的變量類型,它對應的存儲位置在哪里,如果沒有,一定要閱讀上文之后再回來看,這是理解之后內容的基礎。
1.3. Cortex-M3/4系列內核
CrortexM3/4系列內核中的寄存器組都有16個寄存器,如圖所示,寄存器組通常都是CPU用于數據處理和運行控制的,希望你可以大概知道每個寄存器的作用:
① R0-R12:通用寄存器,用于數據操作;
② R13:棧頂指針,有兩個互斥的指針MSP和PSP,在任一時刻只能使用其中一個;
③ R14:連接寄存器,調用子程序時存放返回地址;
④ R15:程序計數器,PC指針指向哪里,CPU就執行哪里的代碼;
在RTOS內核中,這16個寄存器組的值稱之為 「上下文環境」 ,即當前任務運行時這16個寄存器中的值稱為上文環境,下一個任務運行時這16個寄存器的值稱為下文環境, 「上下文切換」 就是指將這16寄存器組的值修改為下一個任務的值。
1.4. 棧
棧是一種 「只能在一端插入或者刪除元素」 的數據結構,規則為: 「先入后出」 (FILO)。
在C語言程序運行的時候,棧是非常非常非常重要的,在裸機程序中,棧頂指針由寄存器R13給出。
棧的作用,一方面是局部變量的存儲,局部變量的定義會被匯編為PUSH 指令,將局部變量中的內容壓入棧中,在函數執行完畢之后出棧,該局部變量被銷毀;另一方面是函數調用時的參數傳遞,也會被壓入棧中,在函數執行完畢后出棧。
2. 任務控制塊長啥樣
任務控制塊是一個任務的核心,廣義的講: 「內核所有對任務的操作,其實都是在操作任務控制塊」 。
任務控制塊類型k_task_t是一個結構體類型:
typedef struct k_task_st k_task_t;
當定義了一個任務控制塊時,該結構體變量沒有初始值,所以 「存儲位置在STM32內部SRAM中的bss段內」 。
任務控制塊的結構體類型定義如下:
/**
* task control block
*/
struct k_task_st {
k_stack_t *sp; /**< task stack pointer. This lady always comes first, we count on her in port_s.S for context switch. */
knl_obj_t knl_obj; /**< just for verification, test whether current object is really a task. */
char name[K_TASK_NAME_MAX]; /**< task name */
k_task_entry_t entry; /**< task entry */
void *arg; /**< argument for task entry */
k_task_state_t state; /**< just state */
k_prio_t prio; /**< just priority */
k_stack_t *stk_base; /**< task stack base address */
size_t stk_size; /**< stack size of the task */
k_list_t stat_list; /**< list for hooking us to the k_stat_list */
k_tick_t tick_expires; /**< if we are in k_tick_list, how much time will we wait for? */
k_list_t tick_list; /**< list for hooking us to the k_tick_list */
k_list_t pend_list; /**< when we are ready, our pend_list is in readyqueue; when pend, in a certain pend object's list. */
pend_obj_t *pending_obj; /**< if we are pending, which pend object's list we are in? */
pend_state_t pend_state; /**< why we wakeup from a pend */
};
此處引用的源碼 「不完整」 ,方便閱讀起見,所有使用宏開關配置的定義全部省略。
任務控制塊中的內容主要分為三部分:
① 任務棧棧頂指針sp:接下來會重點講解;
② 任務的全部信息:任務名稱、任務狀態、任務優先級、任務入口函數及參數、任務棧地址和大小;
③ 任務的鏈表:后續文章中重點講解。
3. 任務棧
3.1. 任務棧是什么
任務棧類型 k_stack_t 是一個 uint8_t 類型:
typedef uint8_t k_stack_t;
當定義了一個任務棧數組時:
#define TASK1_STACK_SIZE 512
k_stack_t task1_stack[TASK1_STACK_SIZE];
本質上還是一個uint8_t類型的全局變量數組,該全局變量數組沒有初始值,所以 「存儲位置仍在STM32內部SRAM中的bss段內」 。
在使用該數組的時候,只通過指針sp訪問,假裝它是一個棧,在使用上和棧的使用方式一模一樣,所以稱之為任務棧。
3.2. 任務棧中有什么(作用)
在創建任務的API中,有這樣一句代碼來初始化任務棧,并且返回任務棧的棧頂指針sp:
task- >sp = cpu_task_stk_init((void *)entry, arg, (void *)task_exit, stk_base, stk_size);
查看cpu_task_stk_init
函數的定義,會發現 「不同的CPU結構,該函數的實現不同」 。
為什么不同的CPU結構,會導致任務棧的初始化代碼實現不同呢?
不急,讓我們先來看看如何來初始化任務棧, 「Cortex-M系列芯片的內核對應的都是ARM v7m架構」 ,選取此架構中的 cpu_task_stk_init 函數實現來探索問題的答案。
① 獲取任務棧棧頂指針的地址并對齊:
cpu_data_t *sp;
sp = (cpu_data_t *)&stk_base[stk_size];
sp = (cpu_data_t *)((cpu_addr_t)sp & 0xFFFFFFF8);
② PendSV異常發生時自動保存的寄存器:
/* auto-saved on exception(pendSV) by hardware */
*--sp = (cpu_data_t)0x01000000u; /* xPSR */
*--sp = (cpu_data_t)entry; /* entry */
*--sp = (cpu_data_t)exit; /* R14 (LR) */
*--sp = (cpu_data_t)0x12121212u; /* R12 */
*--sp = (cpu_data_t)0x03030303u; /* R3 */
*--sp = (cpu_data_t)0x02020202u; /* R2 */
*--sp = (cpu_data_t)0x01010101u; /* R1 */
*--sp = (cpu_data_t)arg; /* R0: arg */
③ 手動保存/加載的寄存器:
*--sp = (cpu_data_t)0x11111111u; /* R11 */
*--sp = (cpu_data_t)0x10101010u; /* R10 */
*--sp = (cpu_data_t)0x09090909u; /* R9 */
*--sp = (cpu_data_t)0x08080808u; /* R8 */
*--sp = (cpu_data_t)0x07070707u; /* R7 */
*--sp = (cpu_data_t)0x06060606u; /* R6 */
*--sp = (cpu_data_t)0x05050505u; /* R5 */
*--sp = (cpu_data_t)0x04040404u; /* R4 */
④ 返回當前棧頂指針:
return (k_stack_t *)sp;
初始化后任務棧中的內容如下:
任務切換的大致流程是觸發PendSV異常,在異常處理函數中使用匯編語言實現任務切換,也就是 「上下文切換」 ,在接下來的文章中會專門講述任務切換。
當該任務被調度執行時,CPU會自動將任務棧中最前面的8個寄存器值加載到CPU寄存器中,完成 「下文環境切換」 ,此時:
- 棧頂指針寄存器R13中的值是該任務的任務棧的sp指針;
- 程序計數器指針PC指向的是該任務的入口函數entry;
接下來CPU中的環境就是該任務的環境,該任務開始運行。
因為棧頂指針指向的是該任務的任務棧,所以此時若在任務的入口函數中傳遞參數,調用函數,創建局部變量, 「所有數據都被壓入到該任務的任務棧中」 ,與STM32內部的棧空間毫無關系。
同理,當任務執行完畢時(不一定是程序結束,而是調度器需要去調度執行別的任務了),因為棧具有 「后入先出」 的規則,CPU再將當前寄存器組的值壓入到棧中,完成 「上文環境保存」 ,下次再需要被加載時,這些寄存器組的值將首先出棧。
最后揭曉問題答案,因為 「不同的CPU架構,CPU寄存器組的數量、功能都不同」 ,所以需要針對每種CPU架構都要有一個實現。
4. 任務到底應該怎么寫
在學習RTOS的時候,我們的關注點都是“如何創建任務”,將重點放在了創建任務的API上,而忽略了一些最重要的問題。
重點①: 「任務入口函數,并不是一個普通的函數」 。
任務入口函數,通常它都偽裝成了一個普通函數,不像main函數那樣鶴立雞群,所以很多時候我們覺得它就是一個普通函數調用,實則不然。
「每一個任務的entry,首先應該是一個獨立的裸機程序。」
為什么這么說?因為多任務操作系統的機制是搶占式調度和時間片輪轉,無論再怎么牛逼,也無法改變CPU中只有一個CPU的事實,所以無論在任何一個時刻,系統中都只有唯一一個任務在運行。
重點②: 「每寫一行代碼,都要思考任務棧是否足夠」 。
在任務入口函數中創建的局部變量,函數調用,函數傳參,都使用的是該任務的任務棧,和STM32內部棧空間沒有任何關系,所以在編寫的時候一定要時刻思考自己指定的任務棧大小是否足夠,特別是在開辟局部變量數組的時候,調用一些庫的API的時候。
而在任務入口函數中,如果定義的是static變量,則不會存放到任務棧中,存放位置在STM32內部SRAM中的bss區域內。
除此之外,其余代碼都屬于可執行代碼,存放在Flash中Text區域中的Executable Code段,大可不必太在意。
重點③: 「盡量盡量要主動釋放CPU,切忌浪費CPU」 。
在裸機程序中,如果你動不動喜歡寫個死循環延時,尚可原諒,但是在RTOS系統中,如果一個任務在死循環做無用功,而導致其它任務得不到調度執行,將是不可饒恕的。
在編寫任務入口函數的時候,一定要遵循“不使用,就讓出”的原則,做一個高素質的任務,最普遍的做法是使用系統提供的delay函數來延時。
這樣做有非常多的優點,一方面是防止系統發生堵塞,導致其它任務得不到運行;另一方面是使系統中的空閑任務可以在空閑的時候回收系統內存資源,進入低功耗模式等騷操作。
-
寄存器
+關注
關注
31文章
5359瀏覽量
120812 -
STM32
+關注
關注
2270文章
10915瀏覽量
356774 -
RTOS
+關注
關注
22文章
817瀏覽量
119769 -
Cortex-M3
+關注
關注
9文章
270瀏覽量
59524 -
SRAM控制器
+關注
關注
0文章
11瀏覽量
5909
發布評論請先 登錄
相關推薦
評論