# 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);
}
```
:::

:::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 端穩定下來。一個電子產品若有彈跳現象的話,最常見到的「症狀」是按下一個開關,結果數字跳好幾下。
- 
- 如何 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)