任務函數原型:
void ATaskFunction(void * pvParameters);
任務不允許從實現函數中返回.如果一個任務不再需要,可以用vTaskDelete()刪除;
一個任務函數可以用來創建多個任務,各任務均是獨立的執行實例,擁有屬於自己的棧空間.
典型的任務函數結構:
void ATaskFunction( void *pvParameters ) { /* 可以像普通函數一樣定義變量。用這個函數創建的每個任務實例都有一個屬於自己的iVarialbleExample變 量。但如果iVariableExample被定義為static,這一點則不成立 – 這種情況下只存在一個變量,所有的任務實 例將會共享這個變量。 */ int iVariableExample = 0; /* 任務通常實現在一個死循環中。 */ for( ;; ) { /* 完成任務功能的代碼將放在這里。 */ } /* 如果任務的具體實現會跳出上面的死循環,則此任務必須在函數運行完之前刪除。傳入NULL參數表示刪除 的是當前任務 */ vTaskDelete( NULL ); }
在最簡單的情況下,一個任務可以有兩個狀態:1.運行狀態 2.非運行狀態.
當某個任務處於運行狀態時,處理器就正在執行它的代碼;當一個任務處於非運行狀態時,該任務就進行休眠,它的所有狀態都被妥善保存,以便在它再次進入運行狀態時可以繼續執行之前的代碼.
當任務恢復執行時,其將精確地從離開運行狀態時正在准備執行的那一條指令開始執行.
任務從非運行狀態轉移到運行狀態被稱為”切入或切換入”,反之則稱為”切出或切換出”
創建任務:vTaskCreate
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode, const signed portCHAR * const pcName, unsigned portSHORT usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask );
參數:
pvTaskCode:指向一個實現函數的指針.
pcName:描述性的任務名,這個參數不會被FreeRTOS使用,只是單純用於輔助調試.
usStackDepth:分配的棧空間大小,注意不是Byte,而是word.比如32位寬的棧空間,usStackDepth為100則會分配400字節的棧空間(4bytes*100).
pvParameters:傳入的參數
uxPriority:優先級(0為最低優先級,configMAX_PRIORITIES – 1)為最大優先級)
pxCreatedTask :用於傳出任務的句柄。這個句柄將在 API 調用中對該創建出來的任務進行引用,比如改變任務優先級,或者刪除任務。
返回值:
1. pdTRUE 表明任務創建成功
2.errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 由於內存堆空間不足,FreeRTOS 無法分配足夠的空間來保存任務結構數據和任務棧,因此無法創建任務。
例:
void vTask1( void *pvParameters ) { const char *pcTaskName = "Task 1 is running\r\n"; volatile unsigned long ul; /* 和大多數任務一樣,該任務處於一個死循環中。 */ for( ;; ) { /* Print out the name of this task. */ vPrintString( pcTaskName ); /* 延遲,以產生一個周期 */ for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ ) { /* 這個空循環是最原始的延遲實現方式。在循環中不做任何事情。后面的示例程序將采用 delay/sleep函數代替這個原始空循環。 */ } } }
int main( void ) { /* 創建第一個任務。需要說明的是一個實用的應用程序中應當檢測函數xTaskCreate()的返回值,以確保任 務創建成功。 */ xTaskCreate( vTask1, /* 指向任務函數的指針 */ "Task 1", /* 任務的文本名字,只會在調試中用到 */ 1000, /* 棧深度 – 大多數小型微控制器會使用的值會比此值小得多 */ NULL, /* 沒有任務參數 */ 1, /* 此任務運行在優先級1上. */ NULL ); /* 不會用到任務句柄 */ /* 啟動調度器,任務開始執行 */ vTaskStartScheduler(); /* 如果一切正常,main()函數不應該會執行到這里。但如果執行到這里,很可能是內存堆空間不足導致空閑 任務無法創建。第五章有講述更多關於內存管理方面的信息 */ for( ;; ); }
在tick的周期性中斷里,運行調度器.
configTICK_RATE_HZ:tick的中斷頻率,比如設為1000(即1KHZ),這是每1ms中斷一次.
vTaskPrioritySet():在調度器啟動后設置任務的優先級.
portTICK_RATE_MS:用於將以心跳為單位的時間值轉化為以毫秒為單位的時間值.比如我要延時500毫秒,於是就可以這樣 vTaskDelay( 500/ portTICK_RATE_MS )
一個事件驅動任務只會在事件發生后觸發工作(處理),而在事件沒有發生時是不能進入運行態的。調度器總是選擇所有能夠進入運行態的任務中具有最高優先級的任務。一個高優先級但不能夠運行的任務意味着不會被調度器選中,而代之以另一個優先級雖然更低但能夠運行的任務。因此,采用事件驅動任務的意義就在於任務可以被創建在許多不同的優先級.
如果一個任務正在等待某個事件,則稱這個任務處於”阻塞態(blocked)”。阻塞態是非運行態的一個子狀態。
任務可以進入阻塞態以等待以下兩種不同類型的事件:
1. 定時(時間相關)事件——這類事件可以是延遲到期或是絕對時間到點
2. 同步事件——源於其它任務或中斷的事件。
FreeRTOS 的隊列,二值信號量,計數信號量,互斥信號量(recursive semaphore,遞歸信號量,本文一律稱為互斥信號量,因為其主要用於實現互斥訪問)和互斥量都可以用來實現同步事件
vTaskSuspend() :讓一個任務進入掛起狀態
vTaskResume() 或vTaskResumeFromISR(): 把 一 個 掛 起 狀 態 的 任 務 喚 醒
大多數應用程序中都不會用到掛起狀態。
空閑任務是在調度器啟動時自動創建的,以保證至少有一個任務可運行
void vTaskDelay( portTickType xTicksToDelay );
參數:
xTicksToDelay :延遲多少個心跳周期.
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
vTaskDelayUntil() :類似vTaskDelay. 可以用於實現一個固定執行周期的需求.由於調用此函數的任務解除阻塞的時間是絕對時刻,比起相對於調用時刻的相對時間更精確(即比調用 vTaskDelay()可以實現更精確的周期性)。
參數:
pxPreviousWakeTime:此參數命名時假定 vTaskDelayUntil()用於實現某個任務以固定頻率周期性執行。這種情況下 pxPreviousWakeTime保存了任務上一次離開阻塞態(被喚醒)的時刻。這個時刻被用作一個參考點來計算該任務下一次離開阻塞態的時刻。
xTimeIncrement:此參數命名時同樣是假定 vTaskDelayUntil()用於實現某個任 務 以 固 定 頻 率 周 期 性 執 行 —— 這 個 頻 率 就 是 由xTimeIncrement 指定的。 xTimeIncrement 的 單 位 是 心 跳 周 期 , 可 以 使 用 常 量portTICK_RATE_MS 將毫秒轉換為心跳周期。
void vTaskFunction( void *pvParameters ) { char *pcTaskName; portTickType xLastWakeTime; /* The string to print out is passed in via the parameter. Cast this to a character pointer. */ pcTaskName = ( char * ) pvParameters; /* 變量xLastWakeTime需要被初始化為當前心跳計數值。說明一下,這是該變量唯一一次被顯式賦值。之后, xLastWakeTime將在函數vTaskDelayUntil()中自動更新。 */ xLastWakeTime = xTaskGetTickCount(); /* As per most tasks, this task is implemented in an infinite loop. */ for( ;; ) { /* Print out the name of this task. */ vPrintString( pcTaskName ); /* 本任務將精確的以250毫秒為周期執行。同vTaskDelay()函數一樣,時間值是以心跳周期為單位的, 可以使用常量portTICK_RATE_MS將毫秒轉換為心跳周期。變量xLastWakeTime會在 vTaskDelayUntil()中自動更新,因此不需要應用程序進行顯示更新。 */ vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) ); } }
vTaskStartScheduler():調度器會自動創建一個空閑任務。空閑任務擁有最低優先級(優先級 0)以保證其不會妨礙具有更高優先級的應用任務進入運行態.
空閑任務鈎子函數 :通過空閑任務鈎子函數(或稱回調,hook, or call-back),可以直接在空閑任務中添加應用程序相關的功能。空閑任務鈎子函數會被空閑任務每循環一次就自動調用一次。
通常空閑任務鈎子函數被用於:
1.執行低優先級,后台或需要不停處理的功能代碼。
2. 測試處系統處理裕量(空閑任務只會在所有其它任務都不運行時才有機會執行,所以測量出空閑任務占用的處理時間就可以清楚的知道系統有多少富余的處理時間)。
3.將處理器配置到低功耗模式——提供一種自動省電方法,使得在沒有任何應用功能需要處理的時候,系統自動進入省電模式。
空閑任務鈎子函數必須遵從以下規則
1. 絕不能阻或掛起。空閑任務只會在其它任務都不運行時才會被執行(除非有應用任務共享空閑任務優先級)。以任何方式阻塞空閑任務都可能導致沒有任務能夠進入運行態!
2. 如果應用程序用到了 vTaskDelete() AP 函數,則空閑鈎子函數必須能夠盡快返回。因為在任務被刪除后,空閑任務負責回收內核資源。如果空閑任務一直運行在鈎子函數中,則無法進行回收工作。
空閑任務鈎子函數必須具有如下所示的函數名和函數原型:
void vApplicationIdleHook( void );
如果要使用空閑任務鈎子, configUSE_IDLE_HOOK 必須定義為 1.
void vTaskDelete( xTaskHandle pxTaskToDelete );
參數:
pxTaskToDelete:
被刪除任務的句柄(目標任務) —— 參考 xTaskCreate() API 函數的參數 pxCreatedTask 以了解如何得到任務句柄方面的信息。 任務可以通過傳入 NULL 值來刪除自己。
任務可以使用 API 函數 vTaskDelete()刪除自己或其它任務。
任務被刪除后就不復存在,也不會再進入運行態。
空閑任務的責任是要將分配給已刪除任務的內存釋放掉。因此有一點很重要,那就是使用 vTaskDelete() API 函數的任務千萬不能把空閑任務的執行時間餓死。
需要說明一點,只有內核為任務分配的內存空間才會在任務被刪除后自動回收。任務自己占用的內存或資源需要由應用程序自己顯式地釋放。
作為一種通用規則,完成硬實時功能的任務優先級會高於完成軟件時功能任務的優先級。但其它一些因素,比如執行時間和處理器利用率,都必須納入考慮范圍,以保證應用程序不會超過硬實時的需求限制。
單調速率調度(Rate Monotonic Scheduling, RMS)是一種常用的優先級分配技術。其根據任務周期性執行的速率來分配一個唯一的優先級。具有最高周期執行頻率的任務賦予高最優先級;具有最低周期執行頻率的任務賦予最低優先級。這種優先級分配方式被證明了可以最大化整個應用程序的可調度性(schedulability),但是運行時間不定以及並非所有任務都具有周期性,會使得對這種方式的全面計算變得相當復雜。
隊列Queue:
往隊列寫入數據是通過字節拷貝把數據復制存儲到隊列中;從隊列讀出數據使得把隊列中的數據拷貝刪除。
由於隊列可以被多個任務讀取,所以對單個隊列而言,也可能有多個任務處於阻塞狀態以等待隊列數據有效。這種情況下,一旦隊列數據有效,只會有一個任務會被解除阻塞,這個任務就是所有等待任務中優先級最高的任務。而如果所有等待任務的優先級相同,那么被解除阻塞的任務將是等待最久的任務。而寫隊列也類似.
同讀隊列一樣,任務也可以在寫隊列時指定一個阻塞超時時間。
創建一個隊列:
xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength,
unsigned portBASE_TYPE uxItemSize );
參數:
uxQueueLength:隊列能夠存儲的最大單元數目,即隊列深度.
uxItemSize:隊列中數據單元的長度,以字節為單位。
返回值:
NULL 表示沒有足夠的堆空間分配給隊列而導致創建失敗。
非 NULL 值表示隊列創建成功。此返回值應當保存下來,以作為操作此隊列的句柄。
portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );
portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );
功能:如同函數名字字面意思,xQueueSendToBack()用於將數據發送到隊列尾,xQueueSendToFront()用於將數據發送到隊列首.
xQueueSend()等同xQueueSendToBack().
但 切 記 不 要 在 中 斷 服 務 例 程 中 調 用 xQueueSendToFront() 或xQueueSendToBack()。系統提供中斷安全版本的 xQueueSendToFrontFromISR()與xQueueSendToBackFromISR()用於在中斷服務中實現相同的功能。
參數:
xQueue:目標隊列的句柄。這個句柄即是調用 xQueueCreate()創建該隊列時的返回值。
pvItemToQueue:發送數據的指針。其指向將要復制到目標隊列中的數據單元。由於在創建隊列時設置了隊列中數據單元的長度,所以會從該指針指向的空間復制對應長度的數據到隊列的存儲區域。
xTicksToWait:阻塞超時時間。如果在發送時隊列已滿,這個時間即是任務處於阻塞態等待隊列空間有效的最長等待時間。
xQueueReceive()與xQueuePeek():相同點:用於從隊列中接收數據.不同點:xQueuePeek不會從隊列中改變(刪除,存儲順序)數據,而xQueueReceive則會從隊列中刪除接收到的數據.
uxQueueMessagesWaiting():用於查詢隊列中當前有效數據單元個數。
taskYIELD():通知調度器現在就切換到其它任務,而不必等到本任務的時間片耗盡.
調用 taskYIELD()的任務轉移到就緒態,
只有以”FromISR”或”FROM_ISR”結束的 API 函數或宏才可以在中斷服務例程中。
在中斷服務例程中使用隊列:
信號量用於事件通信。而隊列不僅可以用於事件通信,還可以用來傳遞數據。
xQueueSendToFrontFromISR(),xQueueSendToBackFromISR()與 xQueueReceiveFromISR()分別是
xQueueSendToFront(),xQueueSendToBack()與 xQueueReceive()的中斷安全版本,專門用於中斷服務例程中。
xQueueSendFromISR()完全等同於 xQueueSendToBackFromISR().
ISR越短越好.
ISR:interrupt service routine
創建一個二值信號量:
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );
參數:
xSemaphore :
創建的信號量
獲取一個信號量.除互斥信號量外,所有類型的信號量都可以調用函數 xSemaphoreTake()來獲取:
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );
參數:
xSemaphore:信號量
xTicksToWait:阻塞超時時間
返回值:
pdPASS:成功獲得信號量
pdFALSE:未能獲得信號量
給出一個信號量,除互斥信號量外,所有類型的信號量都可以調用函數 xSemaphoreTake()來獲取:
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken );
參數:
xSemaphore:信號量
pxHigherPriorityTaskWoken:
如果調用 xSemaphoreGiveFromISR()使得一個任務解除阻塞,並且這個任務的優先級高於當前任務(也就是被中斷的任務),那么 xSemaphoreGiveFromISR()會在 函 數 內 部 將 pxHigherPriorityTaskWoken 設 為pdTRUE。 如 果 xSemaphoreGiveFromISR() 將 此 值 設 為pdTRUE,則在中斷退出前應當進行一次上下文切換。這樣才能保證中斷直接返回到就緒態任務中優先級最高的任務中。
計數信號量:相對於二值信號量,可防止信號丟失.
延遲處理任務正在在處理二值信號量時,如果此時又有中斷發生,並在中斷里給出信號,那么這個信號將會丟失,得不到處理.計數信號量可解決這個問題.
計數信號量的兩種典型用法:
1.事件計數
中斷服務例程給出信號量(計數值加1).延遲處理任務獲取信號量(計數值減1).創建時計數值初始化為0
2.資源管理
一個任務要獲取資源的控制權,其必須先獲得(take)信號量(計數值減1),當計數值減至0,則表示沒有可用資源.當任務利用資源完成后,將給出(giving)信號量(計數值加1).創建時計數值初始化為可用總和.
xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount, unsigned portBASE_TYPE uxInitialCount );
參數:
uxMaxCount:最大計數值.
uxInitialCount :信號量的初始值.
返回:非NULL表示成功;NULL表示堆上空間不足.
中斷優先級是硬件控制的優先級,中斷服務例程的執行會與之關聯。任務並非運行在中斷服務中,所以賦予任務的軟件優先級與賦予中斷源的硬件優先級之間沒有任何關系。
互斥:
1.基本臨界區
基本臨界區是指宏 taskENTER_CRITICAL()與 taskEXIT_CRITICAL()之間的代碼區間.
臨界區的工作僅僅是簡單地把中斷全部關掉,或是關掉優先級在 configMAX_SYSCAL_INTERRUPT_PRIORITY 及以下的中斷.
臨界區必須只具有很短的時間,否則會反過來影響中斷響應時間.
2.掛起(鎖定)調度器
通過調用 vTaskSuspendAll()來掛起調度器.
通過調用 xTaskResumeAll()來喚醒調度器.
由掛起調度器實現的臨界區只可以保護一段代碼區間不被其它任務打斷,因為這種方式下,中斷是使能的.
但是喚醒(resuming, or un-suspending)調度器卻是一個相對較長的操作.
掛起調度器可以停止上下文切換而不用關中斷。如果某個中斷在調度器掛起過程中要求進行上下文切換,則個這請求也會被掛起,直到調度器被喚醒后才會得到執行。
在調度器處於掛起狀態時,不能調用 FreeRTOS API 函數.
3.互斥量(即二值信號量)
互斥量是一種特殊的二值信號量,用於控制在兩個或多個任務間訪問共享資源.
互斥量的缺點:優先級反轉
創建一個互斥量:
xSemaphoreHandle xSemaphoreCreateMutex( void );
互斥量使用(take)后必須歸還(giving)
例:
xSemaphoreTake( xMutex, portMAX_DELAY ); { /* 程序執行到這里表示已經成功持有互斥量。現在可以自由訪問標准輸出,因為任意時刻只會有一個任 務能持有互斥量。 */ printf( "%s", pcString ); fflush( stdout ); /* 互斥量必須歸還! */ } xSemaphoreGive( xMutex );
FreeRTOS 將內存分配作為可移植層面.
當內核請求內存時,其調用 pvPortMalloc()而不是直接調用 malloc();當釋放內存時,調用 vPortFree()而不是直接調用 free()。pvPortMalloc()具有與 malloc()相同的函數原型;vPortFree()也具有與 free()相同的函數原型。
Heap_1.c 實現了一個非常基本的 pvPortMalloc()版本,而且沒有實現 vPortFree()。如果應用程序不需要刪除任務,隊列或者信號量,則具有使用 heap_1 的潛質。Heap_1總是具有確定性。
Heap_2.c 也是使用了一個由 configTOTAL_HEAP_SIZE 定義大小的簡單數組。不同於 heap_1 的是,heap_2 采用了一個最佳匹配算法來分配內存,並且支持內存釋放。
Heap_3.c 簡單地調用了標准庫函數 malloc()和 free(),但是通過暫時掛起調度器使得函數調用備線程安全特性。
unsigned portBASE_TYPE uxTaskGetStackHighWaterMark( xTaskHandle xTask );
uxTaskGetStackHighWaterMark()主要用來查詢指定任務的運行歷史中,其棧空間還差多少就要溢出.
返回值:
任務棧空間的實際使用量會隨着任務執行和中斷處理過程上下浮動。uxTaskGetStackHighWaterMark()返回從任務啟動執行開始的運行歷史中,棧空間具有的最小剩余量。這個值即是棧空間使用達到最深
時的剩下的未使用的棧空間。這個值越是接近 0,則這個任務就越是離棧溢出不遠了。
把局部變量改變為靜態變量后是改變了它的存儲方式即改變了它的生存期。把全局變量改變為靜態變量 后是改變了它的作用域, 限制了它的使用范圍。因此static 這個說明符在不同的地方所起的作用是不同的。