# FreeRTOS study note References : [成大資工Wiki](https://wiki.csie.ncku.edu.tw/embedded/freertos), [FreeRTOS移植教學](https://www.makdev.net/2022/10/rtos-op.html), [利用SWO pin移植printf(without UART)](https://blog.csdn.net/cole0809/article/details/134251814), [queue](https://blog.csdn.net/weixin_45499326/article/details/128548948) ## Introduction 撰寫筆記的初衷是為了增進自己對FreeRTOS及MCU實作的熟練度,也希望可以藉由此學習筆記分享給其他同仁參考。 本篇將分為多個部分持續記錄我所認知的FreeRTOS架構以及OS中所學的相關概念實現(thread, interrupt, Synchronization...),以及一些學(ㄊㄨㄥ)習(ㄌㄧㄥˊ)過程。 ## FreeRTOS concept ### Intro 隨著嵌入式產品的盛行,FreeRTOS 這款作業系統相信大家多少有聽過,但它跟同為作業系統的 Windows, Linux 有什麼差別呢? :::info 1. 系統目標和用途 * Windows 和 Linux: 設計用於桌面電腦、伺服器、嵌入式電腦、手持設備等具備高性能硬體資源的環境。 主要目的是提供多用戶、多任務、圖形介面、多重網路協定支援,並支持複雜的應用程序。 * FreeRTOS:針對資源有限的嵌入式系統設計,適合低功耗 MCU (如 STM32、ESP32) 的應用。 主要目的是滿足即時性需求,快速響應事件或中斷,通常在沒有完整作業系統的嵌入式系統中使用。 2. 架構與資源管理 * Windows 和 Linux:包含完整的進程管理、記憶體管理(虛擬記憶體)、網路堆疊和文件系統,允許多進程和多執行緒操作。 提供豐富的 I/O 驅動、設備管理、資料同步機制(如文件鎖、共享記憶體等),通常在記憶體和儲存空間上要求較高。 通常整個系統佔幾十GB。 * FreeRTOS:以輕量、快速為核心,專注於執行緒管理,內存管理簡單,通常使用靜態或簡單的動態分配。 不具備完整的文件系統(除非單獨加裝),沒有複雜的進程管理,僅提供輕量的執行緒管理和簡單的資源同步機制 (如訊號量、隊列)。 由於內存限制,FreeRTOS 的記憶體管理策略相對簡單且固定。 燒錄的.bin檔小於10KB。 3. 即時性 * Windows 和 Linux:大型作業系統通常無法實現嚴格的即時性,因為系統資源競爭大,內部存在許多不可預測的延遲。 適合以任務調度為主的需求,如伺服器或桌面應用,無法滿足強即時性的要求。 * FreeRTOS:即時作業系統 (RTOS) 的核心目標是縮短任務響應時間,保證任務在特定時間內完成。 提供細粒度的任務優先級和搶佔式排程,使其適合需要精確控制的應用。 ::: 所以總結來說,我認為FreeRTOS的優勢在於: 1. 輕量化架構 2. 任務即時性 ### Source code 比較重要,有機會用到的核心程式碼: * tasks.c:主要掌管 task 的檔案 * queue.c:管理 task 間 communication (message queue 的概念) * list.c:提供系統與應用實作會用到的 list 資料結構 * FreeRTOSConfig.h:包含 clock speed, heap size, mutexes 等等都在此定義(需自行建立) 以上程式碼會在[後續的章節](#note-1)深入討論 ### Data structure FreeRTOS 明確的定義變數名稱以及資料型態,不會有 unsigned 以及 signed 搞混使用的情形發生。 :::info 1. 變數 * char 類型:以 c 為字首 * short 類型:以 s 為字首 * long 類型:以 l 為字首 * float 類型:以 f 為字首 * double 類型:以 d 為字首 * Enum 變數:以 e 為字首 * portBASE_TYPE 或其他(如 struct):以 x 為字首 * pointer 有一個額外的字首 p , 例如 short 類型的 pointer 字首為 ps * unsigned 類型的變數有一個額外的字首 u , 例如 unsigned short 類型的變數字首為 us 2. 函式:以回傳值型態與所在檔案名稱為開頭(prefix) * vTaskPriority() 是 task.c 中回傳值型態為 void 的函式 * xQueueReceive() 是 queue.c 中回傳值型態為 portBASE_TYPE 的函式 * 只能在該檔案中使用的 (scope is limited in file) 函式,以 prv 為開頭 (private) 3. 巨集名稱:巨集在FreeRTOS裡皆為大寫字母定義,名稱前小寫字母為巨集定義的地方 * portMAX_DELAY : portable.h * configUSE_PREEMPTION : FreeRTOSConfig.h 一般巨集回傳值定義pdTRUE 及 pdPASS為1 , pdFALSE 及 pdFAIL 為0。 ::: ### <a id="note-1">Task</a> 在 FreeRTOS 中,task 是執行的基本單位,透過```xTaskCreate()```創建任務,每個任務負責不一樣的事。最後再呼叫```vTaskStartScheduler()```對所有任務進行排程(multitasking)。 :::info **multitasking :vs: multithread** * 傳統作業系統中的多執行緒:在 Windows 或 Linux 中,進程可以擁有多個執行緒,這些執行緒能夠共享進程的記憶體空間和資源。每個執行緒都可以獨立運行,並且由操作系統的調度器管理。 * FreeRTOS 中的多任務 (Multitasking):在 FreeRTOS 中,任務 (tasks) 類似於執行緒,並且由 FreeRTOS 的內核進行調度 (time sharing)。每個任務擁有自己的堆疊和執行上下文,但它們共享同一個硬體平台和處理器。FreeRTOS 通常運行在單一的處理器核心上,所以多任務調度類似於多執行緒,但其運行環境和操作方式與多執行緒的系統有所不同。 ::: #### ```xTaskCreate()``` 這個函數有幾個特點,它的返回值必須是 void,其中通常會有一個無限迴圈,所有關於這個 task 的工作都會在迴圈中進行,而且這個函數不會有 return,FreeRTOS 不允許 task 自行結束(使用 return 或執行到函數的最後一行)。 ```c BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //指向task的指標 const char * const pcName, //task的暱稱 const configSTACK_DEPTH_TYPE uxStackDepth, //task的stack大小 void * const pvParameters, //傳入task的參數 UBaseType_t uxPriority, //task的優先級 TaskHandle_t * const pxCreatedTask //task的句柄 ) ``` 這個函式主要分為兩步驟: 1. ```prvCreateTask()```分配內存空間給 task TCB 及 stack。 2. ```prvAddNewTaskToReadyList()```把 task 放進 ready list 排程中。 ##### ```prvCreateTask()``` 在這個函數中,首先會判斷巨集```portSTACK_GROWTH > 0```表示 stack 是由低址往高址長,```portSTACK_GROWTH < 0```表示 stack 是由高址往低址長(大多處理器都是這樣),因為這會攸關到 TCB 跟 stack 的分配順序,避免 stack 的增長溢位覆蓋到 TCB 的部分導致 task 無法正確運作。 ![image](https://hackmd.io/_uploads/ByjIuVkfJg.png) 接著再用```pvPortMalloc()```動態分配。 :::info :book: **TCB中存放的東西** ```c typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; // 指向當前棧頂的指針 ListItem_t xStateListItem; // 任務狀態的鏈表項 StackType_t *pxStack; // 指向任務棧的起始位 char pcTaskName[ configMAX_TASK_NAME_LEN ]; // 任務名稱 UBaseType_t uxPriority; // 優先級 UBaseType_t uxBasePriority; // 任務的基礎優先級 // 其他的控制塊成員... } TCB_t; ``` ::: #### Task status ![image](https://hackmd.io/_uploads/rkaVR4kz1g.png) * Ready:準備好要執行的狀態,每個任務剛創建好都應該要是ready。 * Running:正在由 CPU 執行的狀態。 * Blocked:等待中的狀態(```vTaskDelay```, ```signal```) * Suspended:等待中的狀態(並且必須通過```vTaskResume()```或```vTaskResumeFromISR()```明確地重新啟動任務。掛起狀態的任務不會因時間到達而自動恢復運行。) :::warning :heavy_exclamation_mark: 目前有出現一個情況,如果一次創建多個小空間 Task(ex.創建10個 Task 各自 malloc 128 *4 Bytes)或是創建少數大空間 Task(ex. 創建4個 Task 各自 malloc 1024 *4 Bytes)都會發生 hard fault,一開始覺得可能是 stack overflow,之後經過 trial and error 發現相較於創建少數的大空間 Task,創建多個小空間 Task 可能會發生 hard fault,<font color=red>**儘管 total szie 比前者少**</font>。加上總共可用的 heap 大小有75KB那麼多,所以跟超過可用 heap 上限是無關的,目前還在釐清中... :bulb: [FreeRTOS内存分配技巧](https://blog.csdn.net/xingqingly/article/details/120260398) 依照參考文獻內提及,如果用很粗略的方式來區分的話,flash 主要存程式碼,而 RAM 則是存程式運行時需要的空間(包含動態分配、stack、.bss ...)。所以在 FreeRTOS 中要特別注意除了 task 所需要的空間,最好在預留一些空間給其他部分,以免 total size 超過 RAM 的大小。 ![image](https://hackmd.io/_uploads/ryfHcP6Nye.png) ![image](https://hackmd.io/_uploads/HJf8qva4Jg.png) 再來經過我不斷測試跟驗證,<font color=red>**我認為會出現這個問題不是 RAM 的空間爆了,也不是 heap 分配的不夠大**</font>,而是我當初在手動引入 FreeRTOS 時,我是直接修改`FreeRTOSConfig.h`中的`configTOTAL_HEAP_SIZE`而已,這可能存在設定上的遺漏或是錯誤。我嘗試創建另一個專案,所有跟 FreeRTOS 有關的設定全部在 CubeIDE 的 Middleware 介面設定,結果竟然可以正常運作不再出現 hard fault!並且 heap 可用的空間還剩 2273 Bytes。 ![image](https://hackmd.io/_uploads/rJ17cv641l.png) ::: ### List 上一章節,我們知道 task 有不同的狀態,事實上 FreeRTOS 提供一個 list 架構存放這些 task。 ![image](https://hackmd.io/_uploads/BySar_ezkx.png) FreeRTOS 使用 ready list 去管理準備好要執行的 tasks,而 ready list 的資料儲存方式如上圖。 * ```List_t``` : 存放某個優先級的 task 數量,pointer 及 header。 * ```MiniListItem_t``` : header,指向 ready list 中的第一個 task (double linked-list)。 * ```ListItem_t``` : 存放 task 的 TCB ,排序值```xItemValue```跟所屬的```List_t```。 操作 list 的函式包含```vListInsert()```, ```uxListRemove()```,概念就類似雙向鏈結的插入與刪去 #### ```vListInsert()``` ```c void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) { ListItem_t * pxIterator; const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; . . /* 省略 */ . . for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) { /* There is nothing to do here, just iterating to the wanted * insertion position. */ } } pxNewListItem->pxNext = pxIterator->pxNext; pxNewListItem->pxNext->pxPrevious = pxNewListItem; pxNewListItem->pxPrevious = pxIterator; pxIterator->pxNext = pxNewListItem; /* Remember which list the item is in. This allows fast removal of the * item later. */ pxNewListItem->pxContainer = pxList; ( pxList->uxNumberOfItems ) = ( UBaseType_t ) ( pxList->uxNumberOfItems + 1U ); traceRETURN_vListInsert(); } ``` ### Queue 列隊對於FreeRTOS是一個很重要的概念,常見的同步和互斥鎖都是基於列隊原理實現。 ![image](https://hackmd.io/_uploads/SJallyMfyx.png) ![image](https://hackmd.io/_uploads/rk-fekzGkl.png) ![image](https://hackmd.io/_uploads/rJsSx1MzJe.png) FreeRTOS 創建 queue 是透過一個 MACRO ```c #define xQueueCreate( uxQueueLength, uxItemSize ) \ xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) ) // @param queueQUEUE_TYPE_BASE : 要創建哪種類型的隊列 ``` 其實大致上的原理跟創建 task 很像,都是 malloc 給 queue兩塊空間,分別是```Queue_t```結構跟實際上存放 data 的空間。 ```c typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */ { int8_t * pcHead; //指向隊列存儲區起始的指針 int8_t * pcWriteTo; //指向隊列中下一個可寫位置的指針 volatile UBaseType_t uxMessagesWaiting; //當前隊列中已存在的消息數量 UBaseType_t uxLength; //隊列的長度,即隊列中可以容納的最大元素數 UBaseType_t uxItemSize; //每個隊列元素的大小(以字節為單位) //其他的資訊... } xQUEUE; typedef xQUEUE Queue_t; ``` #### Send & Receive 透過```xQueueSend()```跟```xQueue_Receive()```進行操作,其底層原理都是對分配的那塊 queue 空間依序做```prvCopyDataToQueue()```, ```prvCopyDataFromQueue()```(字串複製),利用結構體中的成員```pcWriteTo```, ```pxReadFrom```存取值。 #### Simple implement 利用 queue 實現一個簡易的 task internal communication ```c void Send_task2(void*) { for(;;) { if ( xQueueSend(hqueue1, &user2, portMAX_DELAY) != pdPASS) /* Send data to queue, set the portMAX_DELAY to make sure * progerss will stuck in here when queue is full */ { _printf("error\n"); } user2.val += 2; vTaskDelay(1000); } } void Take_task(void*) { node buffer; for(;;) { if( xQueueReceive(hqueue1, &buffer, portMAX_DELAY) == pdPASS) /* Receive data from queue, set the portMAX_DELAY to make sure * progress will stuck in here if queue don't have data */ { _printf("%s : %d\n\r", buffer.name, buffer.val); } } } ``` 可以看到 queue 有持續收發 data ![image](https://hackmd.io/_uploads/rJOcvP2z1l.png) :::warning :heavy_exclamation_mark: 較新的FreeRTOS版本中,queue 發送分為```xQueueSendToFront```, ```xQueueSendToBack```。 ::: #### Mutex :vs: Semaphore 我們可以藉由調整```queueQUEUE_TYPE_BASE```參數來決定創建哪種 queue * ```queueQUEUE_TYPE_BASE```:基本隊列類型,用於普通的消息隊列。 * ```queueQUEUE_TYPE_MUTEX```:表示互斥量(Mutex),適合用於創建互斥機制。 * ```queueQUEUE_TYPE_COUNTING_SEMAPHORE```:表示計數信號量(Counting Semaphore),適合用於同步和資源管理。 * ```queueQUEUE_TYPE_BINARY_SEMAPHORE```:表示二進制信號量(Binary Semaphore),常用於簡單的同步操作。 * ```queueQUEUE_TYPE_RECURSIVE_MUTEX```:表示遞歸互斥量(Recursive Mutex),允許同一個任務多次鎖定互斥量。 :::info :book: [來源](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/04-Mutexes) <font color=red>Mutexes are binary semaphores that include a priority inheritance mechanism. Whereas binary semaphores are the better choice for implementing synchronisation (between tasks or between tasks and an interrupt), mutexes are the better choice for implementing simple mutual exclusion (hence 'MUT'ual 'EX'clusion).</font> ::: ## Additional information ### Buttom debounce 當壓下按鈕並隨即放開在我們看來只是按了一下,但是對於電路而言卻是經歷了數次甚至數十次的開開關關,這是由於按鈕的機械結構,接點處於若即若離的狀態造成電位抖動。 ![image](https://hackmd.io/_uploads/Sk-HFvnfke.png) 在實際應用上,由於MCU只會判斷0或1,按鈕抖動就會造成在按壓一次按鈕的動作中其實觸發了很多次電位改變!解決方法百百種,從最陽春的計數器跟延遲,到稍微複雜但完善的偽狀態機,以下一一實現。 #### 1. 計數器 顧名思義,利用計數器紀錄電位處於高(or低)的次數,如果計數超過一定數量後(電位狀態變穩定,有效按鈕動作)才執行對應動作。 ```c void Button_task(void*) { uint8_t count = 0; for(;;) { if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) { count++; if (count == 3) { /* valid action, do the corresponding tasks * ... * ... */ while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) { // do nothing, stuck in here until release the button vTaskDelay(10); } count = 0; // reset counter } } } } ``` #### 2. 延遲等待 手動延遲一段時間(debounce time),等到電位平穩後再次讀取,如果一樣是收到高電位(or低電位)及判斷有效動作。 ```c void Button_task(void*) { for(;;) { if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) { vTaskDelay(50); // debounce time = 50 ms if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) // read again, if HIGH valid active { /* valid action, do the corresponding tasks * ... * ... */ while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) { // do nothing, stuck in here until release the button vTaskDelay(10); } } } } } ``` #### 3. 狀態機 紀錄按鈕處於不同階段的工作。利用最後一次按下按鈕的時間差超過debounce time來判斷是否為有效動作。 ```c bool debounceButton(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { static ButtonState buttonState = BUTTON_RELEASED; // static -> will keep the value static uint32_t lastDebounceTime = 0; bool buttonEvent = false; uint32_t currentTime = xTaskGetTickCount(); GPIO_PinState pinState = HAL_GPIO_ReadPin(GPIOx, GPIO_Pin); switch (buttonState) { case BUTTON_RELEASED: // first time press the button (dosen't release yet) if (pinState == GPIO_PIN_SET) { buttonState = BUTTON_DEBOUNCING; lastDebounceTime = currentTime; // record the first press time } break; case BUTTON_DEBOUNCING: // pressing ... if ((currentTime - lastDebounceTime) > DEBOUNCE_DELAY_MS) // currentTime will keep update if call function { if (pinState == GPIO_PIN_SET) // after debounce time and pinState still HIGH, valid action { buttonState = BUTTON_PRESSED; buttonEvent = true; } else // after debounce time and pinState change to LOW, invalid action { buttonState = BUTTON_RELEASED; //reset the pinState } } break; case BUTTON_PRESSED: // if you keep press, the progress will stuck in here if (pinState == GPIO_PIN_RESET) // if release the button { buttonState = BUTTON_RELEASED; // reset the pinState } break; } return buttonEvent; } ``` ### Middleware package setting STM32CubeIDE 提供了一個現成的 FreeRTOS middleware package 讓使用者可以免去自己把 FreeRTOS kernel 引入專案的過程。但在使用上有一些設定需要注意。 * **SysTick** ![image](https://hackmd.io/_uploads/B1l5KfKEyx.png) 在 generate code 的時候會遇到這個警告建議我們更換 HAL 層的時鐘源,這是由於 FreeRTOS 預設是使用 SysTick 時鐘源,如果我們在設定.ioc檔中(也就是HAL層)的 timebase source 也選擇 SysTick 就會有可能發生衝突。 | Firmware layer | 會用到時鐘源的函式 | |:--------------:|:------------------:| | FreeRTOS | `vTaskDelay()` | | HAL | `HAL_Delay()` | ## Non-OS project porting to the FreeRTOS 這一章節,我會把一個在 non-OS 的專案移植到 FreeRTOS上。 在原先的架構中,由於沒有引入作業系統,基本上就是 CPU 不斷的在執行一個 while loop,這會導致<font color=red>**所有的行為都是有序的**</font>,這樣會使一個系統變得比較沒有那麼有彈性跟即時性。 :::info :book: CPU 一個時間點本來就只能做一件事,**所以就算是 FreeRTOS 本質上也是有序的在執行**。但如果我們有意的不斷切換不同的 task,就會讓使用者有一種多任務並行的錯覺。 ::: 首先,我要先把 while loop 這個龐大的任務依照性質劃分成好幾個 sub-task : * ```StartDefaultTask``` : 負責開機的偵測及流程。 * ```RoutineTask``` : Run time mode 的行為。 * ```PollingTask``` : 負責那些需要定時去檢查的項目。 * ```ProcessTask``` : 負責各種情況下觸發的 LED 變換以及關機的行為。 * ```UARTHandlerTask``` : 負責 UART 相關的收發跟轉換。 interrupt 的部分跟 non-OS 中的設定大同小異,就算在 FreeRTOS 中,<font color=red>**中斷仍然擁有最高的優先執行權,只是要特別注意不要讓太複雜的行為在中斷中實現**</font>,畢竟中斷這種不講理的搶CPU也只是搶一小段的時間就要還回去了(小三偷吃太久會被正宮抓到?)。 ### Task create 有了構想之後,就可以開始創建 task ![image](https://hackmd.io/_uploads/BkTw_4gVJg.png) ![image](https://hackmd.io/_uploads/SJwcdNlE1g.png) 我把除了```RoutineTask```之外的 task 都設高一階的優先級,以確保 mode 的變換、電池電量的檢測、關機的動作等重要的行為可以被優先處理。然後```StartDefaultTask```這個 task 其實是 FreeRTOS firmware 內建的空任務,不用白不用XD。 :::warning :warning: 特別注意,每一個 task 都應該要是無窮迴圈且不 return 值;換句話說,<font color=red>**我們要確保每個(尤其是高優先級的) task 會有一段時間進入 Blocked,這樣 CPU 才有空檔去執行其他低優先級的任務。**</font> ::: #### Power on mechanism 在原先的架構中,我們將開機的程序放在main函式的一開始 ```c int main(void) { /* * Init ... * */ //turn off LED Process_SLED(POWER_ON_MODE); EMS_LED_Ctrl(EMS_DISABLE); /* Start System timer */ if (HAL_TIM_Base_Start_IT(&htim14) != HAL_OK) { /* Starting Error */ Error_Handler(); } Power_On_Cnt = htim14_I_Cnt; if (LL_RCC_IsActiveFlag_IWDGRST()) { DB_printf("Reset by IWDG.!!!"); /* clear IWDG reset flag */ LL_RCC_ClearResetFlags(); Power_Mode = 1; Reset_By_IWDG = 1; } else { // ! LL_RCC_IsActiveFlag_IWDGRST() #if POWER_ON_2S_SUPPORT while ( PWR_SW_GPIO_Port->IDR & PWR_SW_Pin ) { //active high if ( (htim14_I_Cnt - Power_On_Cnt) >= POWER_ONOFF_CNT ) { //2s Bypass_Poweron_Mode_Event = 1; Power_Mode = 1; break; } } #else // !POWER_ON_2S_SUPPORT Bypass_Poweron_Mode_Event = 1; Power_Mode = 1; #endif if ( Power_Mode == 0 ) { P_Dbg_Off = POWER_ON_LPRESS_OFF; Process_Power_off(P_Dbg_Off); } . . . ``` 但在 FreeRTOS 架構中,我們希望所有複雜的操作可以放到 task 中(ex. PWM, GPIO, UART...),於是我把除了```Init```之外的所有行為交給```StartDefaultTask```執行。 ```c void StartDefaultTask(void *argument) { /* USER CODE BEGIN 5 */ /* Infinite loop */ for(;;) { if (HAL_TIM_Base_Start_IT(&htim14) != HAL_OK) { /* Starting Error */ Error_Handler(); } Power_On_Cnt = htim14_I_Cnt; if (LL_RCC_IsActiveFlag_IWDGRST()) { DB_printf("Reset by IWDG.!!!"); /* clear IWDG reset flag */ LL_RCC_ClearResetFlags(); Power_Mode = 1; Reset_By_IWDG = 1; } else { // ! LL_RCC_IsActiveFlag_IWDGRST() #if POWER_ON_2S_SUPPORT while ( PWR_SW_GPIO_Port->IDR & PWR_SW_Pin ) { //active high if ( (htim14_I_Cnt - Power_On_Cnt) >= POWER_ONOFF_CNT ) { //2s Bypass_Poweron_Mode_Event = 1; Power_Mode = 1; break; } } #else // !POWER_ON_2S_SUPPORT Bypass_Poweron_Mode_Event = 1; Power_Mode = 1; #endif . . . ``` 但這邊會有幾個問題, 1. 要怎麼確保開機程序在開機後不會再被 FreeRTOS 調度執行? 2. 在尚未開機的期間如何確保其他 task 不會被執行? 第一個問題可以透過```vTaskDelete(NULL)```把任務從排程還有 heap 中刪除,確保在開機期間不會再被調用。 第二個問題可以透過多種方法解決: 1. Counting Semaphore awake 2. Dynamic create task 3. Event Groups awake 我這邊是以 Counting Semaphore awake 實現 ```c void StartDefaultTask(void *argument) { /* USER CODE BEGIN 5 */ /* Infinite loop */ for(;;) { . . . // if detect the 2s button behavior, wake up all task and delete itself if (Power_Mode == 1) { for (int i=0; i<4; i++) { xSemaphoreGive(xCountingSemaphore); } vTaskDelete(NULL); } vTaskDelay(10); } /* USER CODE END 5 */ } ``` 如此一來,一個完整的開機程序完成 ### Run time tasks 基於 FreeRTOS 的優勢,我把原先在 loop 中攏長的程式分成```RoutineTask```, ```PollingTask```, ```ProcessTask```, ```UARTHandlerTask```幾個 task 同步執行。這邊對於程式碼不再多贅述,主要針對特別的機制或運用做說明。 - [ ] `BD_Mode` 把所有跟模式改變有關的行為全部放在`ProcessTask`, ```c if (Pre_BD_Mode != BD_Mode) { // LED Process_SLED(BD_Mode); if (BD_Mode == HYDRA_MODE || BD_Mode == GLOW_MODE) { Process_EMS(); EMS_LED_Ctrl(EMS_Level); } else EMS_LED_Ctrl(EMS_DISABLE); // Buzz if (Skip_Buzz) Skip_Buzz = 0; else ModeChange_Buzzer(htim17); #if MEMORY_SUPPORT if (EE_flag == 0) write_Dbword_to_flash(); #endif /* synchronous, make sure that Routine task * will not Repeated trigger */ Pre_BD_Mode = BD_Mode; xSemaphoreGive(xTaskSemaphore1); } ``` `BD_Mode`的部分,每次偵測到改變除了 LED、Buzzer、Memory function 的行為之外,在最後還會送出一個 Semaphore 給`RoutineTask`,<font color=red>***Why?***</font> 事實上,一開始會加入這個機制是因為`RoutineTask`中的`Check_Break_Event_In_Mode`判斷機制是 ![image](https://hackmd.io/_uploads/HygmNErVJl.png) 但我把`Pre_BD_Mode = BD_Mode`放到`ProcessTask`了,等於說兩者現在處於不同的 task 中,設想幾個情況... 1. 如果在執行到`Pre_BD_Mode = BD_Mode`之前就被 time slice,`Pre_BD_Mode`沒有修改成功,會造成`Check_Break_Event_In_Mode`不斷被觸發。 2. 如果在執行週期性的 `Check_Break_Event_In_Mode`前就被 time slice 修改`Pre_BD_Mode = BD_Mode`,就會造成`BD_Mode`變數被改變了但實際上卻沒有切換。 <font color=red>**簡單來說,加上了 Semaphore 機制可以在各 task time slice 執行的同時確保任務之間的先後順序。**</font> - [ ] `EMS_Level` `EMS_Level`方面,我利用 Queue 讓中斷`HAL_GPIO_EXTI_Falling_Callback`跟任務`ProcessTask`做 inter-task communication。 ```c void ProcessTask(void*) { . . . // EMS change if (xQueueReceive(xQueue1, &EMS_Op, 0) == pdPASS) { Process_EMS(); EMS_LED_Ctrl(EMS_Op); } } ``` ```c void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin) { . . . //EMS level SW active Hi if (GPIO_Pin == EMS_LEVEL_SW_Pin) { Ems_level_event++; if ((htim14_I_Cnt - Ems_level_exti_time) >= BUTTON_DEBOUNCE) { //debounce 100ms GPI_flag = 1; if (BD_Mode == HYDRA_MODE || BD_Mode == GLOW_MODE) { EMS_Level++ ; EMS_Level = EMS_Level % 4; xQueueSend(xQueue1, &EMS_Level, portMAX_DELAY); } } } } ``` :::warning :heavy_exclamation_mark: 經過思考後,我覺得應該用原本的`Pre_EMS_Level != EMS_Level`條件做 polling check 會比較好。原因有兩個: 1. 如果用 Queue 傳訊息的話需要在 callback 中多執行`xQueueSend`,callback 應該要做越簡單越少的事越好! 2. Queue 的優勢在於 <font color=red>**inter-task communication**</font>,然而`EMS_Level`是一個全域變數,其實不需要 Queue 也可以達到不同 task 間互動的效果。 ::: - [ ] `UARTHandlerTask` 在原本的版本,我們將訊息轉換的函式`str2int()`跟打印的函式`DB_printf`放在`HAL_UARTEx_RxEventCallback`中,這是不太的做法。 ```c void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &huart2) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(xUARTSemaphore1, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } /* NOTE : This function should not be modified, when the callback is needed, the HAL_UARTEx_RxEventCallback can be implemented in the user file. */ } ``` ```c void UARTHandlerTask(void*) { // waiting for start signal ... xSemaphoreTake(xCountingSemaphore, portMAX_DELAY); for (;;) { if (xSemaphoreTake(xUARTSemaphore1, portMAX_DELAY) == pdTRUE) { // transfer data char to int CMD_Bh = str2int(CMD_rxData); . . . } vTaskDelay(10); } } ``` 所以我稍微改良了運作方式,僅讓 callback 負責送一個 Semaphore 出去,剩下的複雜行為交給`UARTHandlerTask`處理。 :::info :book: **portYIELD_FROM_ISR()** 檢查是否需要切換到更高優先級的任務: * 當 ISR 結束時,FreeRTOS 通常會返回到被中斷的任務繼續執行。 * 如果在 ISR 中喚醒了一個優先級更高的任務,FreeRTOS 提供的 portYIELD_FROM_ISR 會立即切換到這個高優先級任務,而不是返回到被中斷的任務。 :::