? ? ?上一篇博客,我們講解了FreeRTOS中,我們講解了創建任務和刪除任務的API函數,那么這一講,我們從實戰出發,規范我們在FreeRTOS下的編碼風格,掌握動態創建任務的編碼風格,達到實戰應用!
目錄
一、任務函數
二、動態創建任務的基本步驟
2.1 使能FreeRTOS的API函數
2.2? 定義動態創建任務函數的入口參數
2.3 編寫任務函數
2.4 主函數進行調用
2.5 補充??
2.6 任務執行順序
四、動態創建任務的API函數解析(選學)
五、任務優先級
六、總結
一、任務函數
? ? ? ? ?不論是動態創建任務還是靜態創建任務,我們FreeRTOS都是在任務之間切換執行,那么任務函數就是我們單獨要實現的功能,根據功能的不同,把裸機系統分割為?個個獨立的無限循環且無法返回的函數。我們把這種函數稱之為任務。即:任務函數是沒有返回值,并且是死循環的!任務的形式:如下:
void task1(void *arg)
{//初始化代碼while(1) //?限循環且不能返回{具體實現的功能}//延時函數
}
1、為什么FreeRTOS的任務函數沒有返回值?(可以將任務理解為線程)
1. 持續運行的任務
? ? ? ?FreeRTOS 任務設計為長期運行,不像普通函數那樣有明確的結束點。在嵌入式系統中,任務(或者稱為線程)通常負責特定的功能,這些功能需要一直運行。例如,處理傳感器數據、管理通信協議或維護系統健康狀態等。這些功能需要持續監控和響應外部事件或內部條件,因此任務函數通常設計為死循環。
2. 任務調度
? ? ? ?FreeRTOS 是一個實時操作系統,負責在多個任務之間進行調度。任務函數進入死循環后,會周期性地調用 FreeRTOS 提供的 API 函數(如
vTaskDelay
或xQueueReceive
),這些 API 會將任務置于阻塞狀態,直到特定條件滿足(延時時間到或者信號量接收到)。這種設計允許 RTOS 進行有效的任務切換,確保系統的實時性和多任務處理能力。3. 沒有返回值
? ? ? 由于任務函數設計為長期運行,因此它們不需要返回值。任務的結束通常不是通過函數返回來實現的,而是通過其他機制,如任務刪除 (
vTaskDelete
)。任務函數的主要目的是在系統運行過程中持續執行特定操作,而不是像傳統函數那樣在執行完特定操作后返回。4. 系統穩定性和資源管理
? ? ? ?任務函數設計為死循環還有助于系統的穩定性和資源管理。在 RTOS 中,任務的生命周期由系統管理,任務函數一旦啟動,便由調度器根據優先級和調度策略進行管理。死循環的設計簡化了任務的生命周期管理,避免了頻繁創建和銷毀任務帶來的資源開銷和復雜性。
2、為什么FreeRTOS任務函數的主體是一個死循環?
1、實時性:
? ? ? ?通過使用死循環,任務可以及時檢查事件狀態并作出相應的處理,以滿足實時性。
2、持續性:
? ? ? ?將任務放在一個循環中,可以持續執行。如果任務函數沒有死循環,而是在任務完成后直接返回,那么任務將會自動退出。這可能導致任務被刪除并釋放資源,而無法再次調度執行
3、提高資源的利用率:
? ? ?只要任務不退出,就不需要重新獲取資源,提高效率。
二、動態創建任務的基本步驟
2.1 使能FreeRTOS的API函數
? ? ? 在使用FreeRTOS任務創建函數之前,我們需要在配置文件里(FreertosConfig.h)將宏configSUPPORT_DYNAMIC_ALLOCATION 配置為 1,此時便支持動態創建。利用Ctrl+F搜索即可。
2.2? 定義動態創建任務函數的入口參數
? ? ? ? 通過上一講我們知道動態創建任務的API函數如下:
其實,我們需要定義的入口參數就是這個API函數的參數,提前定義好,然后傳入參數,他就會自動的為我們創建好對應的任務,并且處于一種就緒態。? ?從上面我們可以看到:
1、任務函數指針:
? ? ? ?其實就是函數名,我們知道函數名就是函數的入口地址,就是一個函數指針
2、任務名字:
? ? ? ? 其實也就是函數名對應的字符串,要用雙引號括起來
3、任務堆棧大小:
? ? ? ? 動態創建任務,任務的任務控制塊以及任務的棧空間所需的內存,均由 FreeRTOS 自動從 FreeRTOS 管理的堆中分配,但是我們需要定義好任務棧的大小,使用宏:
#define START_TASK_STACK_SIZE 128 //定義任務堆棧大小為128字(1字等于4字節)
4、傳遞給任務的參數:
? ? ? ?不需要傳參,我們直接給NULL即可;
5、任務優先級:
? ? ? ? 我們使用的是硬件的方式,因此,它要在0-31之間,使用宏定義即可:
#define START_TASK_PRIO 1 //定義任務優先級,0-31根據任務需求
6、任務句柄:
? ? ? ? 這個參數是指向任務控制塊的指針,任務控制塊TCB其實就是描述任務屬性的一個結構體,一次他就是一個結構體指針,我們后續對任務的刪除等操作,都是通過該任務句柄進行操作,因此,我們需要提前定義好,然后傳入即可,使用宏即可:
TaskHandle_t start_task_handler; //定義任務句柄(結構體指針)
? ? ? 從上面我們可以知道:其實我們只需要提前利用宏定義好三個參數即可,其他的參數只要任務函數編寫好,便可以確定。示例如下:
/**********************START_TASK任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define START_TASK_STACK_SIZE 128 //定義堆棧大小為128字(1字等于4字節)
#define START_TASK_PRIO 1 //定義任務優先級,0-31根據任務需求
TaskHandle_t start_task_handler; //定義任務句柄(結構體指針)
void start_task(void* args);
注意:
- 為了編碼規范,我們使用的宏都是大寫,雖然較長,但是通俗易懂;
- 使用API函數進行任務創建,里面的參數需要進行強制轉換,以免報錯。
- 為了任務執行的順序是按照我們設定好的優先級執行的,我們可以在創建任務的任務中,使用臨界段保護,那么在這個任務體中,可以屏蔽中斷(中斷優先級在5-15之內)比如切換任務的PendSV,此時,我們創建任務的過程中,不會進行任務的調度,然后我們創建任務結束后,在打開臨界段保護,此時不會對所有中斷進行屏蔽,也就是任務切換PendSV(中斷)才會進行任務調度。如下代碼所示,在創建任務開始之前和創建任務之后加入,后面詳細講解。
- 動態創建任務函數,有返回值,我們可以在編程時,對返回值進行判斷,由此可以知道任務是否創建成功!
#include "stm32f4xx.h" // Device header
#include "stdio.h"
#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"/**********************START_TASK任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/#define START_TASK_STACK_SIZE 128 //定義堆棧大小為128字(1字等于4字節)
#define START_TASK_PRIO 1 //定義任務優先級,0-31根據任務需求
TaskHandle_t start_task_handler; //定義任務句柄(結構體指針)
void start_task(void* args);/**********************TASK1任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define TASK1_STACK_SIZE 128 //定義堆棧大小為128字(1字等于4字節)
#define TASK1_PRIO 2 //定義任務優先級,0-31根據任務需求
TaskHandle_t task1_handler; //定義任務句柄(結構體指針)
void task1(void* args);/**********************TASK2任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define TASK2_STACK_SIZE 128 //定義堆棧大小為128字(1字等于4字節)
#define TASK2_PRIO 3 //定義任務優先級,0-31根據任務需求
TaskHandle_t task2_handler; //定義任務句柄(結構體指針)
void task2(void* args);/**********************TASK3任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define TASK3_STACK_SIZE 128 //定義堆棧大小為128字(1字等于4字節)
#define TASK3_PRIO 4 //定義任務優先級,0-31根據任務需求
TaskHandle_t task3_handler; //定義任務句柄(結構體指針)
void task3(void* args);
開始任務用來創建其他三個任務,只創建一次,不能是死循環,同時創建完3個任務后刪除開始任務本身
void start_task(void* args)
{taskENTER_CRITICAL(); /*進入臨界區*/BaseType_t xReturn; //定義接收函數返回值的變量xTaskCreate( (TaskFunction_t) task1,(char *) "task1", ( configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,(void *) NULL,(UBaseType_t) TASK1_PRIO ,(TaskHandle_t *) &task1_handler );//任務1創建結果的判斷if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}xTaskCreate( (TaskFunction_t) task2,(char *) "task2", ( configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,(void *) NULL,(UBaseType_t) TASK2_PRIO ,(TaskHandle_t *) &task2_handler ); //任務2創建結果的判斷if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}xTaskCreate( (TaskFunction_t) task3,(char *) "task3", ( configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE,(void *) NULL,(UBaseType_t) TASK3_PRIO ,(TaskHandle_t *) &task3_handler ); //任務3創建結果的判斷if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}vTaskDelete(NULL); //刪除開始任務自身,傳參NULLtaskEXIT_CRITICAL(); /*退出臨界區*///臨界區內不會進行任務的調度切換,出了臨界區才會進行任務調度,搶占式
}
2.3 編寫任務函數
? ? 對每個任務具體實現的功能進行函數的實現:需要注意,任務函數沒有返回值并且是死循環的!
/********其余三個任務的任務函數,無返回值且是死循環***********//***任務1:實現LED0每500ms翻轉一次*******/
void task1(void* args)
{while(1){printf("任務1正在運行!\n");GPIO_ToggleBits(GPIOF,GPIO_Pin_9 );vTaskDelay(500); //FreeRTOS自帶的延時函數,會進行任務切換調度}}/***任務2:實現LED1每500ms翻轉一次*******/
void task2(void* args)
{while(1){printf("任務2正在運行!\n");GPIO_ToggleBits(GPIOF,GPIO_Pin_10 );vTaskDelay(500); //FreeRTOS自帶的延時函數,會進行任務切換調度}}/***任務3:判斷按鍵KEY0,按下KEY0,任務1刪除*******/
void task3(void* args)
{while(1){printf("任務3正在運行!\n");if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)==0) //表示按鍵按下{if(task1_handler!=NULL) //防止重復刪除{printf("刪除任務1!\n");vTaskDelete(task1_handler); //刪除任務1,傳任務1的句柄task1_handler=NULL;}} vTaskDelay(10); //FreeRTOS自帶的延時函數,會進行任務切換調度}}
? ? ? 此外,我們再自定義一個入口函數,用來創建開始任務,然后將要創建的任務全部放在這個開始任務中,主函數只需調用這個入口函數,即可在這個開始任務中 , 創建其他的任務,這樣做,規范代碼,梳理代碼邏輯,清晰易懂任務的運行順序!如下所示:
//FreeRTO入口例程函數,無參數,無返回值,用來創建開始任務
void freertos_demo(void)
{xTaskCreate( (TaskFunction_t) start_task,(char *) "start_task", ( configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,(void *) NULL,(UBaseType_t) START_TASK_PRIO ,(TaskHandle_t *) &start_task_handler );vTaskStartScheduler(); //開啟任務調度器}
2.4 主函數進行調用
? ? ? ? 在完成上述的編寫后,主函數內部只需要引入對應的頭文件,然后在函數內部調用相應的函數對使用到的外設進行初始化,然后調用入口函數即可進行按照我們設定的優先級進行任務的調度,如下所示:
#include "stm32f4xx.h" // Device header
#include "stdio.h"
#include "myled.h"
#include "mykey.h"
#include "myusart.h"#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h" //可以用來單獨存放任務函數的聲明以及配置相關的宏定義,然后直接引入頭文件使用extern TaskHandle_t Start_Handle;
/*使用任務句柄可以對任務操作,如果沒有添加上面的單獨頭文件存放,
那么使用其他文件的全局變量利用extern關鍵字引入即可。*/int main(void)
{//1、外設初始化My_UsartInit();LED_Init();KEY_Init();//2、調用入口函數freertos_demo();}
2.5 補充??
? ? ? ?為進行模塊化的編程,我們可以將創建相應的頭文件可以用來單獨存放任務函數的聲明以及任務配置相關的宏定義,然后在主函數直接引入頭文件使用即可,這樣工程結構清晰易懂!
2.6 任務執行順序
? ? ? ? 編寫完程序后,一定要進行驗證,驗證程序是否按照我們設定的順序及進行執行,類似于操作系統的線程同步問題!
? ? ? ?首先主函數調用入口函數,在入口函數內部創建開始任務函數,該開始任務進入就緒狀態,啟用任務調度器,調度器啟動后,FreeRTOS 將接管系統控制,開始調度任務。此時CPU就會去執行開始任務,然后,在開始任務中創建三個任務,注意:由于使用了臨界保護:taskENTER_CRITICAL(); ? ? ? ?/*進入臨界區*/? 它會對5-15優先級的中斷進行屏蔽,即不會發生作用,其中PendSV是用來任務切換的內核中斷,它的優先級是13,因此,會被屏蔽,也就是說,我在創建三個任務的過程中,不會進行其他任務的切換,保證我的開始任務創建其他的三個任務不會被打斷!!!創建完三個任務后,它們都進入了就緒態,然后,再刪除這個開始任務(因為每個任務只需要創建一次,多次創建占用堆棧內存,造成棧溢出!)此時,我在關閉臨界區保護,taskEXIT_CRITICAL(); ? /*退出臨界區*/,也就是打開所有中斷,此時PendSV中斷就會被打開,按照任務的優先級進行搶占式調度,分別執行任務3、任務2、任務1,在三個任務執行的過程中,加入適當的延時,他就會進行任務的切換,去就緒列表尋找優先級最高的任務去運行!
四、動態創建任務的API函數解析(選學)
五、任務優先級
? ? ?在 FreeRTOS 中,任務的優先級決定了任務在系統中的調度順序和執行時機。設定任務優先級是 FreeRTOS 任務創建過程中一個重要的步驟。
1、優先級的范圍
FreeRTOS 任務優先級的范圍由
configMAX_PRIORITIES
宏定義。該宏在FreeRTOSConfig.h
文件中定義。通常,優先級的范圍是從 0 到configMAX_PRIORITIES - 1
,優先級數值越大,優先級越高。2、注意事項
優先級的相對性:任務的優先級是相對的,系統中最高優先級的任務將獲得最多的 CPU 時間。如果多個任務具有相同的優先級,調度器會按照時間片輪轉或其他調度策略在它們之間切換。
優先級反轉:在某些情況下,低優先級的任務可能會持有高優先級任務所需的資源,導致優先級反轉問題。FreeRTOS 提供了優先級繼承機制來解決這個問題。
優先級設定的策略:設定優先級時,需要考慮任務的重要性和時間敏感性。實時性要求高的任務應設定較高的優先級,而非實時任務可以設定較低的優先級。
避免過高優先級:設定任務優先級時要避免將所有任務都設為過高的優先級,這樣會導致系統缺乏靈活性,可能導致低優先級任務得不到執行。
六、總結
? ? ? ? ?通過以上的介紹,是不是覺得相比裸機開發確實提升了不少的難度,這就是實時性帶來的,萬事有利必有弊,多看幾遍,相信你對動態創建任務的過程會有清晰的認識,其實步驟也是非常簡單的,接下來去實踐吧!熟練后就不難了,萬事開頭難!
溫馨提示:?
? ? ? ?對于某個需要知道具體函數的實現的,我們可以雙擊函數然后直接跳轉到定義處,或者Ctrl+F 搜索,也可以去官網查看對應的使用實例:https://www.freertos.org/。
? ? ? 至此,動態創建任務就已經講解完畢!初次學習,循序漸進,一步步掌握即可!以上就是全部內容!請務必掌握,創作不易,歡迎大家點贊加關注評論,您的支持是我前進最大的動力!下期再見!