在裸機系統中, 系統的主體就是 main 函數里面順序執行的無限循環,這個無限循環里面 CPU 按照順序完成各種事情。在多任務系統中,我們根據功能的不同,把整個系統分割成一個個獨立的且無法返回的函數,這個函數我們稱為任務。
STM32在執行配置初始化函數的時候, 操作系統完全都還沒有涉及到, 和裸機工程里面的硬件初始化工作是一模一樣的。硬件初始化完后才慢慢啟動操作系統, 最后運行創建好的任務。
1、創建任務
1.1、創建靜態內存的任務
(1)、定義任務函數
任務實際上就是一個無限循環且不帶返回值的 C 函數。
任務必須是一個死循環,否則任務將通過 LR 返回,如果 LR 指向了非法的內存就會產生 HardFault_Handler,而 FreeRTOS 指向一個死循環,那么任務返回之后就在死循環中執行,這樣子的任務是不安全的,所以避免這種情況,任務一般都是死循環並且無返回值的。
任務里面的延時函數必須使用 FreeRTOS 里面提供的延時函數,並不能使用我們裸機編程中的那種延時。這兩種的延時的區別是 FreeRTOS 里面的延時是阻塞延時,即調用 vTaskDelay()函數的時候,當前任務會被掛起,調度器會切換到其它就緒的任務,從而實現多任務。如果還是使用裸機編程中的那種延時,那么整個任務就成為了一個死循環,如果恰好該任務的優先級是最高的,那么系統永遠都是在這個任務中運行,比它優先級更低的任務無法運行,根本無法實現多任務 。
static void CreateAppTask(void) { while(1) { vTaskDelay(); } }
(2)、 定義任務棧
static StackType_t CreateAppTask_Stack[128]; //定義任務棧
在裸機系統中,全局變量統統放在一個叫棧的地方,棧是單片機 RAM 里面一段連續的內存空間,棧的大小一般在啟動文件或者鏈接腳本里面指定, 最后由 C 庫函數_main 進行初始化。但是, 在多任務系統中,每個任務都是獨立的,互不干擾的,所以要為每個任務都分配獨立的棧空間,這個棧空間通常是一個預先定義好的全局數組, 也可以是動態分配的一段內存空間,但它們都存在於 RAM 中。
在 FreeRTOS 系統中,每一個任務都是獨立的,他們的運行環境都單獨的保存在他們的棧空間當中。那么在定義好任務函數之后,我們還要為任務定義一個棧,目前我們使用的是靜態內存,所以任務棧是一個獨立的全局變量。任務的棧占用的是 MCU 內部的 RAM,當任務越多的時候,需要使用的棧空間就越大,即需要使用的RAM 空間就越多。一個 MCU 能夠支持多少任務,就得看你的 RAM 空間有多少。
在大多數系統中需要做棧空間地址對齊,在 FreeRTOS 中是以 8 字節大小對齊,並且會檢查堆棧是否已經對齊,其中 portBYTE_ALIGNMENT 是在 portmacro.h 里面定義的一個宏,其值為 8,就是配置為按 8 字節對齊,當然用戶可以選擇按 1、 2、 4、 8、 16、 32 等字節對齊,目前默認為 8,具體見代碼如下:
#if portBYTE_ALIGNMENT == 8 #define portBYTE_ALIGNMENT_MASK ( 0x0007 ) #endif pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 ); pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) &( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); /* 檢查計算出的堆棧頂部的對齊方式是否正確。 */ configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack &( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
(3)、定義任務控制塊
定義好任務函數和任務棧之后,我們還需要為任務定義一個任務控制塊,通常我們稱這個任務控制塊為任務的身份證。在 C 代碼上,任務控制塊就是一個結構體,里面有非常多的成員,這些成員共同描述了任務的全部信息。
static StaticTask_t CreateAppTask_TCB; //CreateAppTask任務控制塊
(4)、 靜態創建任務
一個任務的三要素是任務主體函數,任務棧,任務控制塊,那么怎么樣把這三個要素聯合在一起? FreeRTOS 里面有一個叫靜態任務創建函數 xTaskCreateStatic(),它就是干這個活的。 它將任務主體函數, 任務棧(靜態的)和任務控制塊(靜態的)這三者聯系在一起,讓任務可以隨時被系統啟動,具體見下面代碼清單:
//創建 AppTaskCreate 任務 AppTaskCreate_Handle = xTaskCreateStatic( (TaskFunction_t)CreateAppTask, //任務函數(1) (const char* )"CreateAppTask", //任務名稱(2) (uint32_t )128, //任務堆棧大小 (3) (void* )NULL, //傳遞給任務函數的參數(4) (UBaseType_t )3, //任務優先級 (5) (StackType_t* )CreateAppTask_Stack, //任務堆棧(6) (StaticTask_t* )&CreateAppTask_TCB); //任務控制塊(7) if (NULL != CreateAppTask_Handle) //創建成功 vTaskStartScheduler(); //啟動任務,開啟調度
(1): 任務入口函數,即任務函數的名稱,需要我們自己定義並且實現。
(2): 任務名字,字符串形式, 最大長度由FreeRTOSConfig.h 中定義的configMAX_TASK_NAME_LEN 宏指定,多余部分會被自動截掉,這里任務名字最好要與任務函數入口名字一致,方便進行調試。 (3): 任務堆棧大小,單位為字,在 32 位的處理器下(STM32),一個字等於4個字節,那么任務大小就為 128*4 字節。 (4): 任務入口函數形參,不用的時候配置為0或者NULL即可。 (5): 任務的優先級。優先級范圍根據 FreeRTOSConfig.h 中 的 宏configMAX_PRIORITIES 決定, 如果使能 configUSE_PORT_OPTIMISED_TASK_SELECTION,這個宏定義,則最多支持 32 個優先級;如果不用特殊方法查找下一個運行的任務,那么則不強制要求限制最大可用優先級數目。在 FreeRTOS 中, 數值越大優先級越高, 0 代表最低優先級。 (6): 任務棧起始地址, 只有在使用靜態內存的時候才需要提供, 在使用動態內存的時候會根據提供的任務棧大小自動創建。 (7): 任務控制塊指針,在使用靜態內存的時候,需要給任務初始化函數 xTaskCreateStatic()傳遞預先定義好的任務控制塊的指針。在使用動態內存的時候,任務創建函數 xTaskCreate()會返回一個指針指向任務控制塊,該任務控制塊是 xTaskCreate()函數里面動態分配的一塊內存。
(5)、空閑任務與定時器任務堆棧函數實現
當我們使用了靜態創建任務的時候, configSUPPORT_STATIC_ALLOCATION 這個宏定 義 必 須 為 1 ( 在 FreeRTOSConfig.h 文 件 中 ) , 並 且 我 們 需 要 實 現 兩 個 函 數 :vApplicationGetIdleTaskMemory()與 vApplicationGetTimerTaskMemory(),這兩個函數是用戶設定的空閑(Idle)任務與定時器(Timer)任務的堆棧大小,必須由用戶自己分配,而不能是動態分配,具體見下面代碼清單。
#define configSUPPORT_STATIC_ALLOCATION 1
/* 空閑任務任務堆棧 */ static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE]; /* 定時器任務堆棧 */ static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH]; /* 空閑任務控制塊 */ static StaticTask_t Idle_Task_TCB; /* 定時器任務控制塊 */ static StaticTask_t Timer_Task_TCB; /** ******************************************************************* * @brief 獲取空閑任務的任務堆棧和任務控制塊內存 * ppxTimerTaskTCBBuffer : 任務控制塊內存 * ppxTimerTaskStackBuffer : 任務堆棧內存 * pulTimerTaskStackSize : 任務堆棧大小 * @author fire * @version V1.0 * @date 2018-xx-xx ********************************************************************** */ void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,StackType_t **ppxIdleTaskStackBuffer,uint32_t *pulIdleTaskStackSize) { *ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任務控制塊內存 */ *ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任務堆棧內存 */ *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任務堆棧大小 */ } /** ********************************************************************* * @brief 獲取定時器任務的任務堆棧和任務控制塊內存 * ppxTimerTaskTCBBuffer : 任務控制塊內存 * ppxTimerTaskStackBuffer : 任務堆棧內存 * pulTimerTaskStackSize : 任務堆棧大小 * @author fire * @version V1.0 * @date 2018-xx-xx ********************************************************************** */ void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,StackType_t **ppxTimerTaskStackBuffer,uint32_t *pulTimerTaskStackSize) { *ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任務控制塊內存 */ *ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任務堆棧內存 */ *pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任務堆棧大小 */ }
(6)、 啟動任務
當任務創建好后,是處於任務就緒 ,在就緒態的任務可以參與操作系統的調度。但是此時任務僅僅是創建了,還未開啟任務調度器,也沒創建空閑任務與定時器任務(如果使能了 configUSE_TIMERS 這個宏定義),那這兩個任務就是在啟動任務調度器中實現,每個操作系統,任務調度器只啟動一次,之后就不會再次執行了, FreeRTOS 中啟動任務調度器的函數是 vTaskStartScheduler(),並且啟動任務調度器的時候就不會返回,從此任務管理都由FreeRTOS 管理,此時才是真正進入實時操作系統中的第一步。
vTaskStartScheduler(); // 啟動任務,開啟調度
1.2、創建動態內存的任務
創建一個動態內存任務,任務使用的棧和任務控制塊是在創建任務的時候FreeRTOS 動態分配的,並不是預先定義好的全局變量。那這些動態的內存堆是從哪里來?
在創建靜態內存任務時,任務控制塊和任務棧的內存空間都是從內部的 SRAM 里面分配的,具體分配到哪個地址由編譯器決定。現在我們開始使用動態內存,即堆,其實堆也是內存,也屬於 SRAM。 FreeRTOS 做法是在 SRAM 里面定義一個大數組,也就是堆內存,供 FreeRTOS 的動態內存分配函數使用,在第一次使用的時候,系統會將定義的堆內存進行初始化,這些代碼在 FreeRTOS 提供的內存管理方案中實現(heap_1.c、heap_2.c、 heap_4.c 等)。
//系統所有總的堆大小 #define configTOTAL_HEAP_SIZE ((size_t)(36*1024)) (1) static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; (2) //如果這是第一次調用 malloc 那么堆將需要初始化, 以設置空閑塊列表 if (pxEnd == NULL ) { prvHeapInit(); (3) } else { mtCOVERAGE_TEST_MARKER(); }
(1):堆內存的大小為configTOTAL_HEAP_SIZE,在FreeRTOSConfig.h 中由我們自己定義,configSUPPORT_DYNAMIC_ALLOCATION 這個宏定義在使用 FreeRTOS 操作系統的時候必須開啟。
(2):從內部 SRAMM 里面定義一個靜態數組 ucHeap,大小由configTOTAL_HEAP_SIZE 這個宏決定, 目前定義為 36KB。定義的堆大小不能超過內部SRAM 的總大小。 (3):如果這是第一次調用 malloc 那么需要將堆進行初始化,以設置空閑塊列表,方便以后分配內存,初始化完成之后會取得堆的結束地址,在 MemMang 中的5 個內存分配 heap_x.c 文件中實現。
(1)、定義任務函數
使用動態內存的時候,任務的主體函數與使用靜態內存時是一樣的,任務實際上就是一個無限循環且不帶返回值的 C 函數。
任務必須是一個死循環,否則任務將通過 LR 返回,如果 LR 指向了非法的內存就會產生 HardFault_Handler,而 FreeRTOS 指向一個死循環,那么任務返回之后就在死循環中執行,這樣子的任務是不安全的,所以避免這種情況,任務一般都是死循環並且無返回值的.
任務里面的延時函數必須使用 FreeRTOS 里面提供的延時函數,並不能使用我們裸機編程中的那種延時。這兩種的延時的區別是 FreeRTOS 里面的延時是阻塞延時,即調用 vTaskDelay()函數的時候,當前任務會被掛起,調度器會切換到其它就緒的任務,從而實現多任務。如果還是使用裸機編程中的那種延時,那么整個任務就成為了一個死循環,如果恰好該任務的優先級是最高的,那么系統永遠都是在這個任務中運行,比它優先級更低的任務無法運行,根本無法實現多任務 。
static void CreateAppTask(void) { while(1) { vTaskDelay(); } }
(2)、定義任務棧
使用動態內存的時候,任務棧在任務創建的時候創建,不用跟使用靜態內存那樣要預先定義好一個全局的靜態的棧空間,動態內存就是按需分配內存,隨用隨取。
(3)、定義任務塊控制指針
使用動態內存時候,不用跟使用靜態內存那樣要預先定義好一個全局的靜態的任務控制塊空間。任務控制塊是在任務創建的時候分配內存空間創建,任務創建函數會返回一個指針,用於指向任務控制塊,所以要預先為任務棧定義一個任務控制塊指針,也是我們常說的任務句柄,具體見下面代碼清單 。
/**************************** 任務句柄 ********************************/ //任務句柄是一個指針,用於指向一個任務,當任務創建好之后,它就具有了一個任務句柄以后我們要想操作這個任務都需要通過這個任務句柄,如果是自身的任務操作自己,那么這個句柄可以為 NULL。 static TaskHandle_t AppTaskCreate_Handle = NULL; //創建任務句柄 static TaskHandle_t LED_Task_Handle = NULL; //LED任務句柄
(4)、 動態創建任務
使用靜態內存時,使用 xTaskCreateStatic()來創建一個任務,而使用動態內存的時,則使用 xTaskCreate()函數來創建一個任務,兩者的函數名不一樣,具體的形參也有區別,具體見下面代碼清單。
//創建 AppTaskCreate 任務 xReturn = xTaskCreate( (TaskFunction_t )AppTaskCreate, //任務入口函數(1) (const char* )"AppTaskCreate", //任務名字(2) (uint16_t )512, //任務棧大小(3) (void* )NULL, //任務入口函數參數(4) (UBaseType_t )1, //任務的優先級(5) (TaskHandle_t* )&AppTaskCreate_Handle); //任務控制塊指針(6) //啟動任務調度 if (pdPASS == xReturn) vTaskStartScheduler(); //啟動任務,開啟調度
(1): 任務入口函數,即任務函數的名稱,需要我們自己定義並且實現。
(2): 任務名字,字符串形式, 最大長度由 FreeRTOSConfig.h 中定義的configMAX_TASK_NAME_LEN 宏指定,多余部分會被自動截掉,這里任務名字最好要與任務函數入口名字一致,方便進行調試。 (3): 任務堆棧大小,單位為字,在 32 位的處理器下(STM32),一個字等於 4 個字節,那么任務大小就為 128 * 4 字節。 (4): 任務入口函數形參,不用的時候配置為 0 或者 NULL 即可。 (5): 任務的優先級。優先級范圍根據 FreeRTOSConfig.h 中的宏configMAX_PRIORITIES 決定, 如果使能 configUSE_PORT_OPTIMISED_TASK_SELECTION,這個宏定義,則最多支持 32 個優先級;如果不用特殊方法查找下一個運行的任務,那么則不強制要求限制最大可用優先級數目。在 FreeRTOS 中, 數值越大優先級越高, 0 代表最低優先級。 (6): 任務控制塊指針,在使用內存的時候,需要給任務初始化函數xTaskCreateStatic()傳遞預先定義好的任務控制塊的指針。在使用動態內存的時候,任務創建函數 xTaskCreate()會返回一個指針指向任務控制塊,該任務控制塊是 xTaskCreate()函數里面動態分配的一塊內存。
(5)、 啟動任務
當任務創建好后,是處於任務就緒(Ready) ,在就緒態的任務可以參與操作系統的調度。但是此時任務僅僅是創建了,還未開啟任務調度器,也沒創建空閑任務與定時器任務(如果使能了 configUSE_TIMERS 這個宏定義),那這兩個任務就是在啟動任務調度器中實現,每個操作系統,任務調度器只啟動一次,之后就不會再次執行了, FreeRTOS 中啟動任務調度器的函數是 vTaskStartScheduler(),並且啟動任務調度器的時候就不會返回,從此任務管理都由FreeRTOS 管理,此時才是真正進入實時操作系統中的第一步。
//啟動任務調度 if (pdPASS == xReturn) vTaskStartScheduler(); //啟動任務,開啟調度 else return -1;
2、FreeRTOS的啟動流程
2.1、FreeRTOS啟動方式
在目前的 RTOS 中,主要有兩種比較流行的啟動方式,如下:
(1)、萬事俱備, 只欠東風
第一種我稱之為萬事俱備, 只欠東風法。這種方法是在 main 函數中將硬件初始化,RTOS 系統初始化,所有任務的創建這些都弄好,這個我稱之為萬事都已經准備好。最后只欠一道東風,即啟動 RTOS 的調度器,開始多任務的調度,具體的偽代碼實現見下面代碼清單。
int main (void) { HardWare_Init(); //硬件初始化(1) RTOS_Init(); //RTOS 系統初始化(2) RTOS_TaskCreate(Task1); //創建任務 1,但任務 1 不會執行,因為調度器還沒有開啟(3) RTOS_TaskCreate(Task2); //創建任務 2,但任務 2 不會執行,因為調度器還沒有開啟 //......繼續創建各種任務 RTOS_Start(); //啟動 RTOS,開始調度(4) } void Task1( void *arg ) (5) { while (1) { //任務實體,必須有阻塞的情況出現 } } void Task1( void *arg ) (6) { while (1) { //任務實體,必須有阻塞的情況出現 } }
(1):硬件初始化。硬件初始化這一步還屬於裸機的范疇,我們可以把需要使用到的硬件都初始化好而且測試好,確保無誤。
(2): RTOS 系統初始化。比如 RTOS 里面的全局變量的初始化,空閑任務的創建等。不同的 RTOS,它們的初始化有細微的差別 (3):創建各種任務。這里把所有要用到的任務都創建好,但還不會進入調度,因為這個時候 RTOS 的調度器還沒有開啟。 (4):啟動 RTOS 調度器,開始任務調度。這個時候調度器就從剛剛創建好的任務中選擇一個優先級最高的任務開始運行。 (5) (6):任務實體通常是一個不帶返回值的無限循環的 C 函數,函數體必須有阻塞的情況出現,不然任務(如果優先權恰好是最高)會一直在 while 循環里面執行,導致其它任務沒有執行的機會。
(2)、小心翼翼, 十分謹慎
第二種我稱之為小心翼翼, 十分謹慎法。這種方法是在 main 函數中將硬件和 RTOS 系統先初始化好,然后創建一個啟動任務后就啟動調度器,然后在啟動任務里面創建各種應用任務,當所有任務都創建成功后,啟動任務把自己刪除,具體的偽代碼實現見下面代碼清單。
int main (void) { HardWare_Init(); //硬件初始化(1) RTOS_Init(); //RTOS 系統初始化(2) RTOS_TaskCreate(AppTaskCreate); //創建一個任務(3) RTOS_Start(); //啟動 RTOS,開始調度(4) } //起始任務,在里面創建任務 void AppTaskCreate( void *arg ) (5) { RTOS_TaskCreate(Task1); //創建任務 1,然后執行(6) RTOS_TaskCreate(Task2); //當任務 1 阻塞時,繼續創建任務 2,然后執行 //......繼續創建各種任務 RTOS_TaskDelete(AppTaskCreate); //當任務創建完成, 刪除起始任務(7) } void Task1( void *arg ) (8) { while (1) { //任務實體,必須有阻塞的情況出現 } } void Task2( void *arg ) (9) { while (1) { //任務實體,必須有阻塞的情況出現 } }
(1):硬件初始化。來到硬件初始化這一步還屬於裸機的范疇,我們可以把需要使用到的硬件都初始化好而且測試好,確保無誤。
(2): RTOS 系統初始化。比如 RTOS 里面的全局變量的初始化,空閑任務的創建等。不同的 RTOS,它們的初始化有細微的差別。 (3):創建一個開始任務。然后在這個初始任務里面創建各種應用任務。 (4):啟動 RTOS 調度器,開始任務調度。這個時候調度器就去執行剛剛創建好的初始任務。 (5):我們通常說任務是一個不帶返回值的無限循環的 C 函數,但是因為初始任務的特殊性,它不能是無限循環的,只執行一次后就關閉。在初始任務里面我們創建我們需要的各種任務。 (6):創建任務。每創建一個任務后它都將進入就緒態,系統會進行一次調度,如果新創建的任務的優先級比初始任務的優先級高的話,那將去執行新創建的任務,當新的任務阻塞時再回到初始任務被打斷的地方繼續執行。反之,則繼續往下創建新的任務,直到所有任務創建完成。 (7):各種應用任務創建完成后,初始任務自己關閉自己,使命完成。 (8) (9):任務實體通常是一個不帶返回值的無限循環的 C 函數,函數體必須有阻塞的情況出現,不然任務(如果優先權恰好是最高)會一直在 while 循環里面執行,其它任務沒有執行的機會。
LiteOS 和 ucos 第一種和第二種都可以使用,由用戶選擇, RT-Thread 和 FreeRTOS 則默認使用第二種。接下來我們詳細講解下 FreeRTOS 的啟動流程
2.2、FreeRTOS 的啟動流程
在系統上電的時候第一個執行的是啟動文件里面由匯編編寫的復位函數Reset_Handler。復位函數的最后會調用 C 庫函數__main。 __main 函數的主要工作是初始化系統的堆和棧,最后調用 C 中的 main 函數,從而去到 C 的世界。
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP
(1)、創建任務xTaskCreate()函數
在 main()函數中,我們直接可以對 FreeRTOS 進行創建任務操作,因為 FreeRTOS 會自動幫我們做初始化的事情,比如初始化堆內存。 FreeRTOS 的簡單方便是在別的實時操作系統上都沒有的,像 RT-Tharead、 LiteOS 需要我們用戶進行初始化內核。
這種簡單的特點使得 FreeRTOS 在初學的時候變得很簡單,我們自己在 main()函數中直接初始化我們的板級外設——BSP_Init(),然后進行任務的創建即可——xTaskCreate(),在任務創建中, FreeRTOS 會幫我們進行一系列的系統初始化,在創建任務的時候,會幫我們初始化堆內存。
(2)、vTaskStartScheduler()函數
在創建完任務的時候,我們需要開啟調度器,因為創建僅僅是把任務添加到系統中,還沒真正調度,並且空閑任務也沒實現,定時器任務也沒實現,這些都是在開啟調度函數vTaskStartScheduler()中實現的。為什么要空閑任務?因為 FreeRTOS 一旦啟動,就必須要保證系統中每時每刻都有一個任務處於運行態(Runing),並且空閑任務不可以被掛起與刪除, 空閑任務的優先級是最低的,以便系統中其他任務能隨時搶占空閑任務的 CPU 使用權。這些都是系統必要的東西,也無需用戶自己實現, FreeRTOS 全部幫我們搞定了。 處理完這些必要的東西之后,系統才真正開始啟動。