RTOS內功修煉記(一)—— 任務到底應該怎麼寫?

內容導讀:

本篇文章講述了任務的三大元素:任務控制塊、任務棧、任務入口函數,並講述了編寫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 running\r\n");
        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函數來延時。

這樣做有非常多的優點,一方面是防止系統發生堵塞,導致其它任務得不到運行;另一方面是使系統中的空閒任務可以在空閒的時候回收系統內存資源,進入低功耗模式等騷操作。


本節內容就講到這裏,希望對你有所幫助,我是Mculover666,一個喜歡玩板子的小碼農,下期文章再見~

接收更多精彩文章及資源推送,歡迎訂閱我的微信公衆號:『mculover666』。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章