FreeRTOS Study == ###### tags: `FreeRTOS` `RTOS` # Task and Co-routines ## Task Introduction > Note that an application can be designed using just tasks, just co-routines, or a mixture of both. > Co-routines are really only intended for use on very small processors that have severe RAM constraints. ### Characteristics of a ‘Task’ - Scheduler 負責決定現在該由哪個 task 進行動作 - Task 不知道 scheduler 的運作方式,也就不知道自己什麼時候被點名 - 每個 Task 都有自己用來儲存狀態的 stack,被 scheduler 要求停止動作時,會把狀態儲存以備下次使用 ### Characteristics of a ‘Coroutine’ - Coroutine 被用在非常小的裝置上,時至今日已很少被使用 - Coroutine 的概念跟 task 很像,但有以下幾點不同 1. **Stack usage** 所有 co-routines 共用一個 stack,這麼做可以大幅減少 RAM 的使用 2. **Scheduling and priorities** Co-routines 採用了以優先權為依據的任務排程方式,但是它可以被包在採用搶占排程的 task 裡面 3. **Macro implementation** co-routine 的實作是由一組 macro 組成 4. **Restrictions on use** 由於減少了 RAM 的使用,伴隨而來的缺點是對 co-routines 結構的嚴格限制 ## Task States ### Running 正在執行的 task,如果處理器只有單核心,那就只會有一個 task 在 running state ### Ready 當有優先權相同或更高的 task 正在執行時,其他沒有被 blocked 或是 suspended 的 task 就會在 ready state 等待被 scheduler 點名出來執行 ### Blocked 當 task 要等待某個指令才會繼續進行時會進入 block state,例如在 task 內呼叫 *vTaskDelay()*,除此之外,等待 queue、event、semaphore、notification 等事件也會使得 task 進入 block state。 所有在 block state 的 task 都會有一個計時器,計時器 timeout 時,即使 task 沒有等到要繼續執行的 event,也會被強制 unblock ### Suspended 只有明確呼叫 *vTaskSuspend()* 的 task 會進入 suspend state。而呼叫 *vTaskResume()* 可以使在 suspend state 的 task 回到 ready state ![Valid task state transitions](https://www.freertos.org/fr-content-src/uploads/2018/07/tskstate.gif) https://www.freertos.org/RTOS-task-states.html ## Task Priorities - task 的優先權由低至高為 0 ~ (configMAX_PRIORITIES - 1) - configMAX_PRIORITIES 被定義在 FreeRTOSConfig.h - configMAX_PRIORITIES 可以為任意值,但為了 RAM 的使用效率著想,盡量不要設定為過高的數值 > If the port in use implements a port optimised task selection mechanism that uses a ‘count leading zeros’ type instruction (for task selection in a single instruction) and **configUSE_PORT_OPTIMISED_TASK_SELECTION** is set to **1** in FreeRTOSConfig.h, then **configMAX_PRIORITIES** cannot be higher than **32**. > // TODO: study what is PORT_OPTIMISED_TASK_SELECTION - [idle task](https://www.freertos.org/RTOS-idle-task.html) 優先權為 0 (tskIDLE_PRIORITY) - Running state 的 task 必定為優先權最高或與 ready state 等高的 task - configUSE_TIME_SLICING 被設為 1 的話,相同優先權的 task 可以共享處理器的時間(分時多工) https://www.freertos.org/RTOS-task-priority.html ## Implementing a Task ### Basic Task 長得像下列範例 ```cpp= void vATaskFunction(void *pvParameters) { for (;;) { // Task implementation } vTaskDelete(NULL); } ``` - *pvParameters 是唯一傳入的參數 - 不要在 task 裡面使用 return - 使用 **xTaskCreate()** 或 **xTaskCreateStatic()** 來創建 task,用 **vTaskDelete()** 來移除 task ### Task Creation Macro **portTASK_FUNCTION** 和 **portTASK_FUNCTION_PROTO** 是用來給編譯器識別為特殊的句法,以轉換成相對應的程式碼。除非文件內有特別註明,否則不要隨便使用。(目前只有 PIC18 fedC 使用) https://www.freertos.org/implementing-a-FreeRTOS-task.html ## The Idle Task and Idle Hook Function ### The Idle Task - 當 scheduler 啟動時就會自動產生一個優先權為最低(0, a.k.a. tskIDLE_PRIORITY)的 task - 這個優先權最低的 task 是用來回收呼叫 vTaskDelete() 的 task 所遺留的記憶體空間 - application task 也能使用這個最低的優先權(0, tskIDLE_PRIORITY),詳細請參考[configIDLE_SHOULD_YIELD configuration parameter](https://www.freertos.org/a00110.html) ### The Idle Task Hook - Idle task hook 是在每個 idle task 的 cycle 中會執行的函式 - 通常用來使 MCU 進入省電模式 - 有兩種實現方式 1. 在 idle task hook 裡面實作你要的功能 - 把 FreeRTOSConfig.h 裡面的 **configUSE_IDLE_HOOK** 設為 **1** - 定義 **void vApplicationIdleHook(void);** 函式 2. 創建一個優先權唯 tskIDLE_PRIORITY 的 task - 請參考 [Embedded software application design](https://www.freertos.org/tutorial/index.html) 頁面來了解更多 - 不要去 block idle task > 講簡單一點就是 idle task 進行時,idle task 會呼叫的 callback https://www.freertos.org/RTOS-idle-task.html # Queues, Mutexes, Semaphores ## Queues ![](https://i.imgur.com/rade7Tl.gif) - 用來做 tasks 之間或者是 interrupt 與 task 之間的溝通 - FIFO - 資料小時用 pass by copy & 資料大時用 pass by copied pointer (類 reference) - C 語言的基本型態變數或是小的 structure 可以直接複製到 queue 裡面 - 資料大小可能會改變的變數可透過自定義的 queue 來傳送,而負責傳送的 message 的 structure 會含有一個成員指標指向資料位置,另一個則負責記錄資料大小,may be something like following ```cpp= struct message { data_t *ptrToData, double dataSize } ``` - 單一 queue 可以接收不同來源的訊息 - tasks 之間透過 queue 傳遞資料的整個過程是 thread-safe 的 - task 和 interrupt 要收送 queue 內的資料時,呼叫的是不同的 API,不過都很簡單好使用:) ### Blocking on Queues - 當一個 task 要讀取空的 queue 時,該 task 會被 block - 當一個 task 要寫入滿的 queue 時,該 task 會被 block - 當很多 task 因為同一個 queue 被 block 時,優先權高的 task 會先被 unblock - interrupts 只能呼叫結尾是 "FromISR" 的 function https://www.freertos.org/Embedded-RTOS-Queues.html ## Binary Semaphores > TIP: ‘Task Notifications’ can provide a light weight alternative to binary semaphores in many situations > > 文件一開頭就寫了上面這句,因此想到要用 semaphores 時應該先去考慮使用 task notifications - Binary semaphores 被應用在同步以及互斥鎖 - Binary semaphores 和 mutexes 很類似,但是 mutexes 有優先權繼承機制,binary semaphores 沒有 - Binary semaphores 可視為一個大小只有 1 個 message 的 queue,因此 queue 的狀態只會有 "滿的" 和 "空的" 兩種狀態(binary) - 等待 semaphore 的 task 只能讀取 queue (解鎖),釋放 semaphore 的 task/interrupt 只能寫入 queue (上鎖) - Binary semaphores 的使用者並不關心 queue 裡面存了什麼資料,只在乎它是什麼狀態 ![](https://i.imgur.com/ecz1IVe.gif) https://www.freertos.org/Embedded-RTOS-Binary-Semaphores.html ## Counting Semaphores > TIP: ‘Task Notifications’ can provide a light weight alternative to counting semaphores in many situations > > 文件一開頭就寫了上面這句,因此想到要用 semaphores 時應該先去考慮使用 task notifications - 和 Binary semaphores 一樣, Counting Semaphores 的使用者並不關心 queue 裡面存了什麼資料,只在乎它是什麼狀態 - Counting semaphores 通常用在下列兩種狀況 1. Counting events 每當有事件發生時,event handler 會給予一個 semaphore(增加 semaphore 的數量);而 handler task 每次處理事件時,會消耗一個 semaphore(減少semaphore的數量),因此在這種情境下,counting semaphores 的數量表示還沒有被處理的事件個數。 2. Resource managment 這種應用情境中,counting semaphores 表示可被使用的資源個數。某個 task 使用資源時,須取走一個 semaphore,當 semaphores 個數為 0 時表示已經沒有資源可被使用,需等待 task 使用完該資源並歸還 semaphores 時,才能讓下一個 task 使用。 https://www.freertos.org/Real-time-embedded-RTOS-Counting-Semaphores.html ## Mutexes - Mutexes 和 binary semaphores 類似,差別在於 mutexes 有優先權繼承機制 - Binary semaphores 適合用在同步(tasks 之間或 task 與 interrupt 之間) - Mutexes 適合用在簡易的互斥鎖 - Mutexes 可以想像成是資源的使用權,要使用資源的人需要去取得 mutex,使用完之後須歸還 - 在使用取得 mutex 的函式時,要傳入一個參數用來指定取得 mutex 前**可等待的最大 tick 數**,超過這個 tick 數之後,欲獲取 mutex 的 task 會被 block - 當有一高優先權的 task 要領取 mutex,而一低優先權的 task 正在使用 mutex,則低優先權 task 會暫時被拉到與要領取 mutex 的 task 一樣高的優先權,這就是**優先權繼承機制**。這是被設計來解決優先權倒置的問題 - 優先權繼承機制並不能完全解決優先權倒置問題,但能最小化它 - Mutexes 不應該被使用在 interrupt 上,因為: 1. interrupt 沒有優先權 2. interrupt 不能為了等資源而被 block 住 ![](https://i.imgur.com/5huXaaD.gif) https://www.freertos.org/Real-time-embedded-RTOS-mutexes.html ## Recursive Mutexes - Mutex 可以透過 *xSemaphoreTakeRecursive()* 被同一個 task 取走多次 - 由於 mutex 有優先權繼承機制,因此只有取走 mutex 的 task 可以歸還 mutex > 不然還有誰可以還? - mutex 被取走幾次就要歸還幾次後才能再次被其他人使用 - 同上,Mutexes 不應該被使用在 interrupt 上,因為: 1. interrupt 沒有優先權 2. interrupt 不能為了等資源而被 block 住 https://www.freertos.org/RTOS-Recursive-Mutexes.html # Source Code Organization [Link](https://www.freertos.org/a00017.html) ``` FreeRTOS | +-Demo Contains the demo application projects. | | | +-Common The demo application files that are used by all the demos. | +-Dir x The demo application build files for port x | +-Dir y The demo application build files for port y | +-Source Contains the real time kernel source code. | +-include The core FreeRTOS kernel header files | +-Portable Processor specific code. | +-Compiler x All the ports supported for compiler x +-Compiler y All the ports supported for compiler y +-MemMang The sample heap implementations ``` ## FreeRTOS Kernel Files - 三個最基本的程式碼 - task.c (5000多行) - queue.c (3000行) - list.c (200多行) - 其他 Kernel程式碼 (選用) - timer.c (1000多行) - coroutine.c (350多行) ## Memory Management Files 在選用 portable files (/Source/Portable/Compiler/Architecture)時,也要一併把 **/MemMang** 留著。 更多有關 MemMang 的介紹,請參考[連結](https://www.freertos.org/a00111.html) # Static VS Dynamic Memory ## Introduction - FreeRTOS 提供下列物件靜態 / 動態記憶體分配方式 - Tasks - Software Timers - Queues - Event Groups - Binary Semaphores - Counting Semaphores - Recursive Semaphores - Mutexes - 靜態 & 動態記憶體分配可同時使用在同一應用程式內 ## Creating an RTOS Object Using Dynamically Allocated RAM - 創建物件時所需要的函式參數少 - 記憶體分配由 RTOS 的 api 管理 - 物件被刪除時能自動會收記憶體空間,減少記憶體的使用率 - RTOS 的 api 會回傳記憶體堆疊的資訊,使得堆疊大小能夠做最佳化 - 記憶體分配策略可以自己選擇,例如 - heap_1.c 最簡單,且通常用於要求安全性的應用上 - heap_4.c 有做記憶體空間碎片的保護 - heap_5.c 可以將堆疊分散到記憶體的不同區域 - 將 **configSUPPORT_DYNAMIC_ALLOCATION** 設為 **1** 或**不定義**,可以使用下列 api 來建立動態記憶體物件 - xTaskCreate() - xQueueCreate() - xTimerCreate() - xEventGroupCreate() - xSemaphoreCreateBinary() - xSemaphoreCreateCounting() - xSemaphoreCreateMutex() - xSemaphoreCreateRecursiveMutex() ## Creating an RTOS Object Using Statically Allocated RAM - 物件可以被分配到指定的記憶體空間 - 所需最大記憶體空間在 link time 就知道,不需要等到 run time - 開發者不用去擔心記憶體分配失敗的問題 - 簡化 RTOS 應用程式的開發 - 將 **configSUPPORT_STATIC_ALLOCATION** 設為 **1**,則可以使用靜態記憶體分配的相關 api,如下 - xTaskCreateStatic() - xQueueCreateStatic() - xTimerCreateStatic() - xEventGroupCreateStatic() - xSemaphoreCreateBinaryStatic() - xSemaphoreCreateCountingStatic() - xSemaphoreCreateMutexStatic() - xSemaphoreCreateRecursiveMutexStatic() https://www.freertos.org/Static_Vs_Dynamic_Memory_Allocation.html # Heap Memory Managment - FreeRTOS 有五種預設的記憶體堆疊的管理機制,被放在 /FreeRTOS/Source/portable/MemMang 裡面 - heap_1: 最簡易的一種,不允許記憶體被釋放 - heap_2: 允許記憶體被釋放,但不會去合併相鄰的閒置記憶體 - heap_3: 將標準的 malloc() 和 free() 重新包裹為 thread-safe 的函式 - heap_4: 會將相鄰的閒置記憶體合併。Includes absolute address placement option. - heap_5: 除了可以做到 heap_4 能做到的事以外,還可以將一個堆疊分散到不同的記憶體碎片做存取 > heap_1 is less useful since FreeRTOS added [support for static allocation](https://www.freertos.org/Static_Vs_Dynamic_Memory_Allocation.html). heap_2 is now considered legacy as the newer heap_4 implementation is preferred. ## heap_1.c - 儘管無法釋放用不到的記憶體堆疊,但在不需要移除 task, queue, semaphore, mutex 的應用上,仍被大量使用 - 記憶體堆疊的總大小被定義在 **FreeRTOSConfig.h** 裡面的 **configTOTAL_HEAP_SIZE** - **xPortGetFreeHeapSize()** 回傳尚未被使用的記憶體堆疊大小,可被用來最佳化 configTOTAL_HEAP_SIZE 的數值 ### 有關 heap_1.c 的實現 - 如果應用程式不會去刪除 task, queue, semaphore, mutex 則可能適合使用 heap_1 - 記憶體的分配一開始就被確立,因此不會有記憶體碎片化的問題 - 把記憶體分配為靜態的陣列,因此適用於那些不是真的要使用動態記憶體分配的應用程式 ## heap_2.c - heap_4.c 出來以後 heap_2.c 就過時了,請以 heap_4 替代 heap_2 - 可以釋放記憶體,但不會去統合分散的記憶體堆疊 - 記憶體堆疊的總大小被定義在 **FreeRTOSConfig.h** 裡面的 **configTOTAL_HEAP_SIZE** - **xPortGetFreeHeapSize()** 回傳尚未被使用的記憶體堆疊大小,可被用來最佳化 configTOTAL_HEAP_SIZE 的數值,但不會提供記憶體分散的資訊 ### 有關 heap_2.c 的實現 - 不要在每次生成時大小都不一樣的 task, queue, semaphore, mutex 上使用 heap_2,這會造成記憶體碎片化,最終導致記憶體分配失敗 - 此方式為動態分配記憶體,比原生的 malloc() 等函式效能更好 ## heap_3.c - 僅僅是將原生的 malloc() 和 free() 函式重新包裹為 thread-safe 函式 ### 有關 heap_3.c 的實現 - 需要一個 linker 來設置記憶體堆疊,以及有提供 malloc()、free() 函式的 library - 記憶體空間和位置並非固定 - 會造成 RTOS kernel 的大小增加 - **configTOTAL_HEAP_SIZE** 不影響 heap_3 ## heap_4.c - 和 heap_2 類似,不過可以合併分散的記憶體空間 - 記憶體堆疊的總大小被定義在 **FreeRTOSConfig.h** 裡面的 **configTOTAL_HEAP_SIZE** - **xPortGetFreeHeapSize()** 回傳尚未被使用的記憶體堆疊大小 - **xPortGetMinimumEverFreeHeapSize()** 回傳系統開機到當下最小的可用記憶體空間大小 - 上述兩個函式皆不會回傳分散的記憶體資訊 - vPortGetHeapStats() 能提供額外資訊,如下 ```cpp= /* Prototype of the vPortGetHeapStats() function. */ void vPortGetHeapStats( HeapStats_t *xHeapStats ); /* Definition of the Heap_stats_t structure. */ typedef struct xHeapStats { size_t xAvailableHeapSpaceInBytes; /* The total heap size currently available – this is the sum of all the free blocks, not the largest block that can be allocated. */ size_t xSizeOfLargestFreeBlockInBytes; /* The maximum size, in bytes, of all the free blocks within the heap at the time vPortGetHeapStats() is called. */ size_t xSizeOfSmallestFreeBlockInBytes; /* The minimum size, in bytes, of all the free blocks within the heap at the time vPortGetHeapStats() is called. */ size_t xNumberOfFreeBlocks; /* The number of free memory blocks within the heap at the time vPortGetHeapStats() is called. */ size_t xMinimumEverFreeBytesRemaining; /* The minimum amount of total free memory (sum of all free blocks) there has been in the heap since the system booted. */ size_t xNumberOfSuccessfulAllocations; /* The number of calls to pvPortMalloc() that have returned a valid memory block. */ size_t xNumberOfSuccessfulFrees; /* The number of calls to vPortFree() that has successfully freed a block of memory. */ } HeapStats_t; ``` ### 有關 heap_4.c 的實現 - 可以用在會反覆刪除 task, queue, semaphore, mutex 的應用程式上 - 記憶體的分配位置不固定,但效能比標準 c library 還要好 heap_4 對於想要植入記憶體動態分配的應用程式來說非常有用 ## heap_5.c - 與 heap_4 類似,可以整合相鄰且被釋放的記憶體空間,除此之外,還能**將記憶體堆疊擴展到不相鄰的記憶體片段上** - 透過 **vPortDefineHeapRegions()** 初始化,且任何要使用 heap_5 管理方式的人都必須等待該函式被呼叫,其中建立任何 RTOS 物件都會時都會在背後呼叫 pvPortMalloc(),因此在建立 RTOS 物件前要確定有呼叫到 vPortDefineHeapRegions() - vPortDefineHeapRegions() 帶了一個參數,這個參數是 HeapRegion_t 結構的矩陣,定義如下 ```cpp typedef struct HeapRegion { /* Start address of a block of memory that will be part of the heap.*/ uint8_t *pucStartAddress; /* Size of the block of memory. */ size_t xSizeInBytes; } HeapRegion_t; ``` - 該矩陣結尾時須加上 {NULL, 0} 的 HeapRegion_t,請參考下列例子 ```cpp /* Allocate two blocks of RAM for use by the heap. The first is a block of 0x10000 bytes starting from address 0x80000000, and the second a block of 0xa0000 bytes starting from address 0x90000000. The block starting at 0x80000000 has the lower start address so appears in the array fist. */ const HeapRegion_t xHeapRegions[] = { { ( uint8_t * ) 0x80000000UL, 0x10000 }, { ( uint8_t * ) 0x90000000UL, 0xa0000 }, { NULL, 0 } /* Terminates the array. */ }; /* Pass the array into vPortDefineHeapRegions(). */ vPortDefineHeapRegions( xHeapRegions ); ``` - **xPortGetFreeHeapSize()** 回傳尚未被使用的記憶體堆疊大小 - **xPortGetMinimumEverFreeHeapSize()** 回傳系統開機到當下最小的可用記憶體空間大小 - 上述兩個函式皆不會回傳分散的記憶體資訊 - vPortGetHeapStats() 能提供額外資訊 # RTOS Fundamentals ## Multitasking Basics - Concurrency 是真正的多工,multitasking 是分時多工 ![](https://i.imgur.com/nGcJSJY.gif) https://www.freertos.org/implementation/a00004.html ## Scheduling Basics - Scheduler 是 kernel 中負責管控哪個 task 可以運行的角色 - 排程策略 (Scheduling Policy) 是決定哪個 task 可以運作的演算法,通常會讓每個 task 公平的分到執行的權利 - 除了 scheduler 可以暫停某項 task 以外,task 也可以 suspend 自己 https://www.freertos.org/implementation/a00005.html ## Context Switching - 每個 task 都會有自己的 context (哪個記憶體的變數為多少、執行到哪行...) - OS 會在 task 進入 suspend 前把 context 記錄下來,task 回復並繼續執行前,會將 context 載入 ![](https://i.imgur.com/oHBHxC3.gif) https://www.freertos.org/implementation/a00006.html ## Real Time Applications - RTOS 和 none-RTOS 的差異反映在他們的排程策略上 - RTOS 中任務完成的時間必須反應在現實中的時間 https://www.freertos.org/implementation/a00007.html ## Real Time Scheduling ![](https://i.imgur.com/Zv3RfN3.gif) - 一開始,vControlTask 在等待 timer event,vKeyHandlerTask 在等待 key pressed event,因此是由 idle task 取得處理器的工作時間 - 在 t1,key pressed event 發生了,且 vKeyHandlerTask 的優先權比 idle task 高,所以輪到 vKeyHandlerTask 工作 - 在 t2,vKeyHandlerTask 已經處理完自己的工作,且沒有其他優先權更高的 task 要運作,因此又輪到 idle task 工作 - 在 t3,timer event 發生了,這次輪到 vControlTask 工作 - t3 和 t4 之間,key pressed event 也發生了,但是 vKeyHandlerTask 的優先權比 vControlTask 低,因此沒辦法馬上開始工作 - 在 t4,vControlTask 完成了自己的工作,並將自己暫停以等待下一個 timer event,此時 vKeyHandlerTask 就可以出來處理自己的工作了 - 在 t5,key pressed event 被處理完了,因此 vKeyHandlerTask 將自己暫停以等待下一個 key pressed event,此時沒有其他 task 要執行,因此統再度把 idle task 叫起來工作 - t5 和 t6 中間處理了一個 timer event - 在 t6,vKeyHandlerTask 被叫起來工作,但在工作中 timer event 也被觸發,且 vControlTask 的優先權比 vKeyHandlerTask 高,因此 vKeyHandlerTask 會被打斷,並開始執行 vControlTask 的工作 - 在 t8,vControlTask 的工作結束了,所以剛被打斷工作的 vKeyHandlerTask 可以回來繼續執行他的任務 https://www.freertos.org/implementation/a00008.html # v means return void x means return signed number