本篇文章講述了任務(wù)的三大元素:任務(wù)控制塊、任務(wù)棧、任務(wù)入口函數(shù),并講述了編寫RTOS任務(wù)入口函數(shù)時(shí)三個(gè)重要的注意點(diǎn)。
1. 知識(shí)點(diǎn)回顧
在正式開(kāi)始講解內(nèi)容之前,我會(huì)先回顧一下基礎(chǔ)知識(shí)點(diǎn),請(qǐng)確保你已經(jīng)了解并掌握。
1.1. 任務(wù)的創(chuàng)建方法
在用戶層調(diào)用API創(chuàng)建一個(gè)任務(wù),通常的流程如下:
① 創(chuàng)建一個(gè)數(shù)組作為任務(wù)棧:
#define TASK1_STACK_SIZE 512
k_stack_t task1_stack[TASK1_STACK_SIZE];
② 創(chuàng)建一個(gè)任務(wù)控制塊:
k_task_t task1;
③ 編寫任務(wù)入口函數(shù):
void task1_entry(void *arg)
{
while(1)
{
printf("task1 is runningrn");
tos_task_delay(1000);
}
}
④ 調(diào)用系統(tǒng)API創(chuàng)建任務(wù):
ret = tos_task_create(&task1,
"task1",
task1_entry,
NULL,
TASK1_PRO,
task1_stack,
TASK1_STACK_SIZE,
10);
創(chuàng)建之后任務(wù)為就緒態(tài)(處于系統(tǒng)就緒隊(duì)列中),等待系統(tǒng)調(diào)度器調(diào)度執(zhí)行。
1.2. STM32內(nèi)存分布
閱讀之后,你應(yīng)該要知道,STM32(Cortex-M3)中Flash和SRAM的內(nèi)存空間如下:
其中Flash存儲(chǔ)空間中又分為文本段、只讀數(shù)據(jù)段、復(fù)制數(shù)據(jù)段:
其中SRAM存儲(chǔ)空間中又分為data數(shù)據(jù)段、bss數(shù)據(jù)段、堆空間、??臻g:
并且還要知道不同的變量類型,它對(duì)應(yīng)的存儲(chǔ)位置在哪里,如果沒(méi)有,一定要閱讀上文之后再回來(lái)看,這是理解之后內(nèi)容的基礎(chǔ)。
1.3. Cortex-M3/4系列內(nèi)核
CrortexM3/4系列內(nèi)核中的寄存器組都有16個(gè)寄存器,如圖所示,寄存器組通常都是CPU用于數(shù)據(jù)處理和運(yùn)行控制的,希望你可以大概知道每個(gè)寄存器的作用:
① R0-R12:通用寄存器,用于數(shù)據(jù)操作;
② R13:棧頂指針,有兩個(gè)互斥的指針MSP和PSP,在任一時(shí)刻只能使用其中一個(gè);
③ R14:連接寄存器,調(diào)用子程序時(shí)存放返回地址;
④ R15:程序計(jì)數(shù)器,PC指針指向哪里,CPU就執(zhí)行哪里的代碼;
在RTOS內(nèi)核中,這16個(gè)寄存器組的值稱之為 「上下文環(huán)境」 ,即當(dāng)前任務(wù)運(yùn)行時(shí)這16個(gè)寄存器中的值稱為上文環(huán)境,下一個(gè)任務(wù)運(yùn)行時(shí)這16個(gè)寄存器的值稱為下文環(huán)境, 「上下文切換」 就是指將這16寄存器組的值修改為下一個(gè)任務(wù)的值。
1.4. 棧
棧是一種 「只能在一端插入或者刪除元素」 的數(shù)據(jù)結(jié)構(gòu),規(guī)則為: 「先入后出」 (FILO)。
在C語(yǔ)言程序運(yùn)行的時(shí)候,棧是非常非常非常重要的,在裸機(jī)程序中,棧頂指針由寄存器R13給出。
棧的作用,一方面是局部變量的存儲(chǔ),局部變量的定義會(huì)被匯編為PUSH 指令,將局部變量中的內(nèi)容壓入棧中,在函數(shù)執(zhí)行完畢之后出棧,該局部變量被銷毀;另一方面是函數(shù)調(diào)用時(shí)的參數(shù)傳遞,也會(huì)被壓入棧中,在函數(shù)執(zhí)行完畢后出棧。
2. 任務(wù)控制塊長(zhǎng)啥樣
任務(wù)控制塊是一個(gè)任務(wù)的核心,廣義的講: 「內(nèi)核所有對(duì)任務(wù)的操作,其實(shí)都是在操作任務(wù)控制塊」 。
任務(wù)控制塊類型k_task_t是一個(gè)結(jié)構(gòu)體類型:
typedef struct k_task_st k_task_t;
當(dāng)定義了一個(gè)任務(wù)控制塊時(shí),該結(jié)構(gòu)體變量沒(méi)有初始值,所以 「存儲(chǔ)位置在STM32內(nèi)部SRAM中的bss段內(nèi)」 。
任務(wù)控制塊的結(jié)構(gòu)體類型定義如下:
/**
* 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 */
};
此處引用的源碼 「不完整」 ,方便閱讀起見(jiàn),所有使用宏開(kāi)關(guān)配置的定義全部省略。
任務(wù)控制塊中的內(nèi)容主要分為三部分:
① 任務(wù)棧棧頂指針sp:接下來(lái)會(huì)重點(diǎn)講解;
② 任務(wù)的全部信息:任務(wù)名稱、任務(wù)狀態(tài)、任務(wù)優(yōu)先級(jí)、任務(wù)入口函數(shù)及參數(shù)、任務(wù)棧地址和大??;
③ 任務(wù)的鏈表:后續(xù)文章中重點(diǎn)講解。
3. 任務(wù)棧
3.1. 任務(wù)棧是什么
任務(wù)棧類型 k_stack_t 是一個(gè) uint8_t 類型:
typedef uint8_t k_stack_t;
當(dāng)定義了一個(gè)任務(wù)棧數(shù)組時(shí):
#define TASK1_STACK_SIZE 512
k_stack_t task1_stack[TASK1_STACK_SIZE];
本質(zhì)上還是一個(gè)uint8_t類型的全局變量數(shù)組,該全局變量數(shù)組沒(méi)有初始值,所以 「存儲(chǔ)位置仍在STM32內(nèi)部SRAM中的bss段內(nèi)」 。
在使用該數(shù)組的時(shí)候,只通過(guò)指針sp訪問(wèn),假裝它是一個(gè)棧,在使用上和棧的使用方式一模一樣,所以稱之為任務(wù)棧。
3.2. 任務(wù)棧中有什么(作用)
在創(chuàng)建任務(wù)的API中,有這樣一句代碼來(lái)初始化任務(wù)棧,并且返回任務(wù)棧的棧頂指針sp:
task- >sp = cpu_task_stk_init((void *)entry, arg, (void *)task_exit, stk_base, stk_size);
查看cpu_task_stk_init
函數(shù)的定義,會(huì)發(fā)現(xiàn) 「不同的CPU結(jié)構(gòu),該函數(shù)的實(shí)現(xiàn)不同」 。
為什么不同的CPU結(jié)構(gòu),會(huì)導(dǎo)致任務(wù)棧的初始化代碼實(shí)現(xiàn)不同呢?
不急,讓我們先來(lái)看看如何來(lái)初始化任務(wù)棧, 「Cortex-M系列芯片的內(nèi)核對(duì)應(yīng)的都是ARM v7m架構(gòu)」 ,選取此架構(gòu)中的 cpu_task_stk_init 函數(shù)實(shí)現(xiàn)來(lái)探索問(wèn)題的答案。
① 獲取任務(wù)棧棧頂指針的地址并對(duì)齊:
cpu_data_t *sp;
sp = (cpu_data_t *)&stk_base[stk_size];
sp = (cpu_data_t *)((cpu_addr_t)sp & 0xFFFFFFF8);
② PendSV異常發(fā)生時(shí)自動(dòng)保存的寄存器:
/* 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 */
③ 手動(dòng)保存/加載的寄存器:
*--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 */
④ 返回當(dāng)前棧頂指針:
return (k_stack_t *)sp;
初始化后任務(wù)棧中的內(nèi)容如下:
任務(wù)切換的大致流程是觸發(fā)PendSV異常,在異常處理函數(shù)中使用匯編語(yǔ)言實(shí)現(xiàn)任務(wù)切換,也就是 「上下文切換」 ,在接下來(lái)的文章中會(huì)專門講述任務(wù)切換。
當(dāng)該任務(wù)被調(diào)度執(zhí)行時(shí),CPU會(huì)自動(dòng)將任務(wù)棧中最前面的8個(gè)寄存器值加載到CPU寄存器中,完成 「下文環(huán)境切換」 ,此時(shí):
- 棧頂指針寄存器R13中的值是該任務(wù)的任務(wù)棧的sp指針;
- 程序計(jì)數(shù)器指針PC指向的是該任務(wù)的入口函數(shù)entry;
接下來(lái)CPU中的環(huán)境就是該任務(wù)的環(huán)境,該任務(wù)開(kāi)始運(yùn)行。
因?yàn)闂m斨羔樦赶虻氖窃撊蝿?wù)的任務(wù)棧,所以此時(shí)若在任務(wù)的入口函數(shù)中傳遞參數(shù),調(diào)用函數(shù),創(chuàng)建局部變量, 「所有數(shù)據(jù)都被壓入到該任務(wù)的任務(wù)棧中」 ,與STM32內(nèi)部的棧空間毫無(wú)關(guān)系。
同理,當(dāng)任務(wù)執(zhí)行完畢時(shí)(不一定是程序結(jié)束,而是調(diào)度器需要去調(diào)度執(zhí)行別的任務(wù)了),因?yàn)闂>哂?「后入先出」 的規(guī)則,CPU再將當(dāng)前寄存器組的值壓入到棧中,完成 「上文環(huán)境保存」 ,下次再需要被加載時(shí),這些寄存器組的值將首先出棧。
最后揭曉問(wèn)題答案,因?yàn)?「不同的CPU架構(gòu),CPU寄存器組的數(shù)量、功能都不同」 ,所以需要針對(duì)每種CPU架構(gòu)都要有一個(gè)實(shí)現(xiàn)。
4. 任務(wù)到底應(yīng)該怎么寫
在學(xué)習(xí)RTOS的時(shí)候,我們的關(guān)注點(diǎn)都是“如何創(chuàng)建任務(wù)”,將重點(diǎn)放在了創(chuàng)建任務(wù)的API上,而忽略了一些最重要的問(wèn)題。
重點(diǎn)①: 「任務(wù)入口函數(shù),并不是一個(gè)普通的函數(shù)」 。
任務(wù)入口函數(shù),通常它都偽裝成了一個(gè)普通函數(shù),不像main函數(shù)那樣鶴立雞群,所以很多時(shí)候我們覺(jué)得它就是一個(gè)普通函數(shù)調(diào)用,實(shí)則不然。
「每一個(gè)任務(wù)的entry,首先應(yīng)該是一個(gè)獨(dú)立的裸機(jī)程序。」
為什么這么說(shuō)?因?yàn)槎嗳蝿?wù)操作系統(tǒng)的機(jī)制是搶占式調(diào)度和時(shí)間片輪轉(zhuǎn),無(wú)論再怎么牛逼,也無(wú)法改變CPU中只有一個(gè)CPU的事實(shí),所以無(wú)論在任何一個(gè)時(shí)刻,系統(tǒng)中都只有唯一一個(gè)任務(wù)在運(yùn)行。
重點(diǎn)②: 「每寫一行代碼,都要思考任務(wù)棧是否足夠」 。
在任務(wù)入口函數(shù)中創(chuàng)建的局部變量,函數(shù)調(diào)用,函數(shù)傳參,都使用的是該任務(wù)的任務(wù)棧,和STM32內(nèi)部棧空間沒(méi)有任何關(guān)系,所以在編寫的時(shí)候一定要時(shí)刻思考自己指定的任務(wù)棧大小是否足夠,特別是在開(kāi)辟局部變量數(shù)組的時(shí)候,調(diào)用一些庫(kù)的API的時(shí)候。
而在任務(wù)入口函數(shù)中,如果定義的是static變量,則不會(huì)存放到任務(wù)棧中,存放位置在STM32內(nèi)部SRAM中的bss區(qū)域內(nèi)。
除此之外,其余代碼都屬于可執(zhí)行代碼,存放在Flash中Text區(qū)域中的Executable Code段,大可不必太在意。
重點(diǎn)③: 「盡量盡量要主動(dòng)釋放CPU,切忌浪費(fèi)CPU」 。
在裸機(jī)程序中,如果你動(dòng)不動(dòng)喜歡寫個(gè)死循環(huán)延時(shí),尚可原諒,但是在RTOS系統(tǒng)中,如果一個(gè)任務(wù)在死循環(huán)做無(wú)用功,而導(dǎo)致其它任務(wù)得不到調(diào)度執(zhí)行,將是不可饒恕的。
在編寫任務(wù)入口函數(shù)的時(shí)候,一定要遵循“不使用,就讓出”的原則,做一個(gè)高素質(zhì)的任務(wù),最普遍的做法是使用系統(tǒng)提供的delay函數(shù)來(lái)延時(shí)。
這樣做有非常多的優(yōu)點(diǎn),一方面是防止系統(tǒng)發(fā)生堵塞,導(dǎo)致其它任務(wù)得不到運(yùn)行;另一方面是使系統(tǒng)中的空閑任務(wù)可以在空閑的時(shí)候回收系統(tǒng)內(nèi)存資源,進(jìn)入低功耗模式等騷操作。
-
寄存器
+關(guān)注
關(guān)注
31文章
5357瀏覽量
120720 -
STM32
+關(guān)注
關(guān)注
2270文章
10910瀏覽量
356651 -
RTOS
+關(guān)注
關(guān)注
22文章
817瀏覽量
119735 -
Cortex-M3
+關(guān)注
關(guān)注
9文章
270瀏覽量
59513 -
SRAM控制器
+關(guān)注
關(guān)注
0文章
11瀏覽量
5908
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論