# 2025 嵌入式作業系統分析與實作 Lab1 ## 1. 簡介 本實驗透過 FreeRTOS 建立 `LEDTask_App` 與 `ButtonTask_App`,並使用任務間通訊(Inter-Task Communication, ITC)機制來實現 LED 狀態的切換。透過本實驗,學習 FreeRTOS 任務建立、佇列傳遞(queue communication)、排程機制以及按鍵 debounce handling。 :::info 任務(task)是在 FreeRTOS 中執行的基本單位,每個 task 都是由一個 C 函數所組成,意思是你需要先定義一個 C 的函數,然後再用 `xTaskCreate()` 這個 API 來建立一個 task。 這個 C 函數有幾個特點,它的返回值必須是 `void`,其中通常會有一個無限迴圈,所有關於這個 task 的工作都會在迴圈中進行,而且這個函數不會有 return。 FreeRTOS 不允許 task 自行結束(使用 return 或執行到函數的最後一行)。 Task 被建立出來後,它會配置有自己的堆疊空間和 stack variable(就是 function 中定義的變數)。 ```c void ATaskFunction(void *pvParameters){ int i = 0; // 每個用這個函數建立的 task 都有自己的一份 i 變數 while(1){ /* do something here */ } /* * 如果你的 task 就是需要離開 loop 並結束 * 需要用 vTaskDelete 來刪除自己而非使用 return 或自然結束(執行到最後一行) * 這個參數的 NULL 值是表示自己 */ vTaskDelete(NULL); } ``` ::: ![image](https://hackmd.io/_uploads/ByC7--m6Jg.png) :::info * Ready:準備好要執行的狀態 * Running:正在由 CPU 執行的狀態 * Blocked:等待中的狀態(通常是在等待某個事件) * Suspended:等待中的狀態(透過 API 來要求退出排程) ::: :::info **Blocked vs Suspended** blocked 定義為若有個 task 將要等待某個目前無法取得的資源(被其他 task 佔用中),則會被設為 blocked 狀態,這是被動的,OS 會呼叫 blocking API 來設定 task 進入 blocked queue。 suspended 與 blocked 的差異在於,suspended 是 task 主動呼叫 API 來要求讓自己進入暫停狀態的。 ::: ## 2. 環境設置 - **硬體**: STM32 開發板 - **軟體**: STM32CubeIDE - **FreeRTOS 版本**: 內建於 STM32CubeMX ## 3. 操作步驟 ### 3.1 硬體設定 1. **LED 設定**: - PD12 設為 `GPIO_Output`,命名為 `GREEN_LED` - PD13 設為 `GPIO_Output`,命名為 `ORANGE_LED` - PD14 設為 `GPIO_Output`,命名為 `RED_LED` 2. **按鍵設定**: - PA0 設為 `GPIO_Input`,命名為 `BLUE_BUTTON` ### 3.2 程式實作 #### 3.2.1 任務函數 - **`LEDTask_App`**: - 具有兩種狀態(S1、S2)。 - S1: 紅燈亮 1 秒、橙燈亮 1 秒、綠燈亮 1 秒,循環運行。 - S2: 只有橙燈閃爍(2 秒亮,2 秒滅)。 - **`ButtonTask_App`**: - 偵測按鍵按下,透過 `Queue` 通知 `LEDTask_App` 切換狀態。 - 包含 debounce handling 與邊緣觸發偵測。 #### 3.2.2 任務程式碼 :::info ```c #include "FreeRTOS.h" #include "task.h" ``` 因為是透過 FreeRTOS 實作,所以要在 `main.c` 加上 include,兩者的 include 的順序不能對調,否則會報錯。 ::: :::info ```c HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinStatePinState) ``` 判斷開/關燈,開燈 `GPIO_PIN_SET`,關燈 `GPIO_PIN_RESET`。 ::: :::info ```c vTaskDelay(2000); xQueueReceive(MsgQueue, &led_state, 0); ``` 透過取得 MsgQueue 的資料,如果 MsgQueue 的內容是空的,則此 task 會進入 block state 等待 2000 個 Tick。若收到資料或是等待超過 2000 Tick, 則此 task 會重新進入到 ready state。 ::: ```c void LEDTask_App(void *pvParameters){ unsigned int led_state = 1; while(1){ while(led_state == 0){ // ORANGE_LED 2 second ON HAL_GPIO_WritePin(ORANGE_LED_GPIO_Port, GPIO_PIN_13, GPIO_PIN_SET); // ORANGE ON HAL_GPIO_WritePin(RED_LED_GPIO_Port, GPIO_PIN_14, GPIO_PIN_RESET); // RED OFF HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GPIO_PIN_12, GPIO_PIN_RESET); // GREEN OFF vTaskDelay(2000); xQueueReceive(MsgQueue, &led_state, 0); if(led_state == 1) break; // 檢查是否要做狀態切換 // ORANGE_LED 2 second OFF HAL_GPIO_WritePin(ORANGE_LED_GPIO_Port, GPIO_PIN_13, GPIO_PIN_RESET); vTaskDelay(2000); xQueueReceive(MsgQueue, &led_state, 0); if(led_state == 1) break; } while(led_state == 1){ // RED 1 SEC HAL_GPIO_WritePin(RED_LED_GPIO_Port, GPIO_PIN_14, GPIO_PIN_SET); // RED ON HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GPIO_PIN_12, GPIO_PIN_RESET); // GREEN OFF HAL_GPIO_WritePin(ORANGE_LED_GPIO_Port, GPIO_PIN_13, GPIO_PIN_RESET); // ORANGE OFF vTaskDelay(1000); xQueueReceive(MsgQueue, &led_state, 0); if(led_state == 0) break; // ORANGE 1 SEC HAL_GPIO_WritePin(ORANGE_LED_GPIO_Port, GPIO_PIN_13, GPIO_PIN_SET); // ORANGE ON HAL_GPIO_WritePin(RED_LED_GPIO_Port, GPIO_PIN_14, GPIO_PIN_RESET); // RED OFF HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GPIO_PIN_12, GPIO_PIN_RESET); // GREEN OFF vTaskDelay(1000); xQueueReceive(MsgQueue, &led_state, 0); if(led_state == 0) break; // GREEN 1 SEC HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GPIO_PIN_12, GPIO_PIN_SET); // GREEN ON HAL_GPIO_WritePin(ORANGE_LED_GPIO_Port, GPIO_PIN_13, GPIO_PIN_RESET); // ORANGE OFF HAL_GPIO_WritePin(RED_LED_GPIO_Port, GPIO_PIN_14, GPIO_PIN_RESET); // RED OFF vTaskDelay(1000); xQueueReceive(MsgQueue, &led_state, 0); if(led_state == 0) break; } } } ``` :::info ```c HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) ``` 用於檢查 Pin 腳有無訊號傳入。 ::: ```c void ButtonTask_App(void){ unsigned int state = 0; unsigned int count = 0; while(1){ if(HAL_GPIO_ReadPin(BLUE_BUTTON_GPIO_Port, GPIO_PIN_0)){ HAL_Delay(100); while(HAL_GPIO_ReadPin(BLUE_BUTTON_GPIO_Port, GPIO_PIN_0)) {;} /* 計算接收到了幾次訊號,每次接受到訊號都要做 task 的切換 */ ++count ; if(count & 0x01) state = 1; else state = 0; xQueueSend(MsgQueue,(int * ) &state,1); taskYIELD(); // 主動釋放處理器,使得其他在 ready list 中的 task 能快一點被執行。 } } } ``` #### 3.2.3 任務建立 兩個 task 的 priority 都設成相同,因為沒有要對 task 做其他的操作,所以 task handler 可以設成 `NULL`。 :::info ```c xTaskCreate(pdTASK_CODE pvTaskCode, const signed portCHAR * const pcName, unsigned portSHORT usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask); ``` - `pvTaskCode`:就是我們定義好用來建立 task 的 C 函數 - `pcName`:任意給定的 task name,這個名稱只被用來作識別,不會在 task 管理中被採用 - `usStackDepth`:堆疊的大小 - `pvParameters`:要傳給 task 的參數陣列,也就是我們在 C 函數宣告的參數 - `uxPriority`:定義這個任務的優先權,在 FreeRTOS 中,0 最低,(configMAX_PRIORITIES – 1) 最高 - `pxCreatedTask`:handle,是一個被建立出來的 task 可以用到的識別符號 ::: :::info `vTaskStartScheduler()` 來啟動排程器決定讓哪個 task 開始執行,當 `vTaskStartScheduler()` 被呼叫時,會先建立一個 idle task,這個 task 是為了確保 CPU 在任一時間至少有一個 task 可以執行(取代直接切換回 kernel task)而在 `vTaskStartScheduler()` 被呼叫時自動建立的 user task,idle task 的 priority 為 0 (lowest),目的是為了確保當有其他 user task 進入 ready list 時可以馬上被執行。 ::: ```c xTaskCreate(LEDTask_App,"LEDTask_App",128,NULL,1,NULL); xTaskCreate(ButtonTask_App, "ButtonTask_App", 128, NULL, 1, NULL); vTaskStartScheduler(); ``` ## 4. FreeRTOS 概念應用 ### 4.1 任務管理 - 使用 `xTaskCreate` 創建 `LEDTask_App` 與 `ButtonTask_App`。 - 透過 `vTaskDelay` 來管理 LED 燈閃爍間隔。 ### 4.2 任務間通訊 (Queue) - 透過 message queue 讓 task 之間可以做溝通,所以要在 `main.c` 加上 include。 ```c #include "queue.h" ``` - `xQueueCreate` 建立佇列,允許 `ButtonTask_App` 透過 `xQueueSend` 傳送訊息給 `LEDTask_App`。 ```c QueueHandle_t MsgQueue = NULL; int main(void){ MsgQueue = xQueueCreate(1, sizeof(unsigned int)); } ``` - `xQueueReceive` 在 `LEDTask_App` 內部接收訊息,以切換 LED 狀態。 ### 4.3 Debounce handling - 關於 Bounce - 按下電源開關時,電壓不會從 0 伏直接升到 VDD 伏。而是在 0 及 VDD 間震盪好幾次,最後才在 VDD 端穩定下來。一個電子產品若有彈跳現象的話,最常見到的「症狀」是按下一個開關,結果數字跳好幾下。 - ![image](https://hackmd.io/_uploads/BJV-Bgmpyl.png) - 如何 Debounce - `HAL_Delay(100)` 避免按單次按鈕被多次檢測,並再次讀取 GPIO 確保按鍵確實被按下。 - `while(HAL_GPIO_ReadPin(BLUE_BUTTON_GPIO_Port, GPIO_PIN_0))` 檢查按鈕是否長按,長按按鈕的話有可能會傳送一次以上的訊號,與 Lab 要求不合。 ```c void ButtonTask_App(void){ while(1){ if(HAL_GPIO_ReadPin(Blue_Button_GPIO_Port,GPIO_PIN_0)) HAL_Delay(100); //debounce while(HAL_GPIO_ReadPin(BLUE_BUTTON_GPIO_Port, GPIO_PIN_0)) {;} } } ``` ## 5. 結論 本實驗透過 FreeRTOS 實現 `LEDTask_App` 和 `ButtonTask_App`,學習如何建立多任務、使用 `Queue` 進行通訊、管理任務優先級及處理按鍵 Debounce 等概念。 ## 6. 參考資料 [成大 wiki FreeRTOS](https://wiki.csie.ncku.edu.tw/embedded/freertos) [2023 嵌入式作業系統分析與實作 Lab1 report](https://hackmd.io/@ccchen0820/rkeGFqKgh#tags-FreeRTOS)