Try   HackMD

2025 嵌入式作業系統分析與實作 Lab1

1. 簡介

本實驗透過 FreeRTOS 建立 LEDTask_AppButtonTask_App,並使用任務間通訊(Inter-Task Communication, ITC)機制來實現 LED 狀態的切換。透過本實驗,學習 FreeRTOS 任務建立、佇列傳遞(queue communication)、排程機制以及按鍵 debounce handling。

任務(task)是在 FreeRTOS 中執行的基本單位,每個 task 都是由一個 C 函數所組成,意思是你需要先定義一個 C 的函數,然後再用 xTaskCreate() 這個 API 來建立一個 task。
這個 C 函數有幾個特點,它的返回值必須是 void,其中通常會有一個無限迴圈,所有關於這個 task 的工作都會在迴圈中進行,而且這個函數不會有 return。
FreeRTOS 不允許 task 自行結束(使用 return 或執行到函數的最後一行)。
Task 被建立出來後,它會配置有自己的堆疊空間和 stack variable(就是 function 中定義的變數)。

void ATaskFunction(void *pvParameters){
   int i = 0;   // 每個用這個函數建立的 task 都有自己的一份 i 變數

   while(1){ /* do something here */ }

   /* 
    * 如果你的 task 就是需要離開 loop 並結束
    * 需要用 vTaskDelete 來刪除自己而非使用 return 或自然結束(執行到最後一行)
    * 這個參數的 NULL 值是表示自己 
    */
   vTaskDelete(NULL);
}

image

  • Ready:準備好要執行的狀態
  • Running:正在由 CPU 執行的狀態
  • Blocked:等待中的狀態(通常是在等待某個事件)
  • Suspended:等待中的狀態(透過 API 來要求退出排程)

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 任務程式碼

#include "FreeRTOS.h"
#include "task.h"

因為是透過 FreeRTOS 實作,所以要在 main.c 加上 include,兩者的 include 的順序不能對調,否則會報錯。

HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinStatePinState)

判斷開/關燈,開燈 GPIO_PIN_SET,關燈 GPIO_PIN_RESET

vTaskDelay(2000);
xQueueReceive(MsgQueue, &led_state, 0);

透過取得 MsgQueue 的資料,如果 MsgQueue 的內容是空的,則此 task 會進入 block state 等待 2000 個 Tick。若收到資料或是等待超過 2000 Tick, 則此 task 會重新進入到 ready state。

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;
        }
    }
}
HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

用於檢查 Pin 腳有無訊號傳入。

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

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 可以用到的識別符號

vTaskStartScheduler() 來啟動排程器決定讓哪個 task 開始執行,當 vTaskStartScheduler() 被呼叫時,會先建立一個 idle task,這個 task 是為了確保 CPU 在任一時間至少有一個 task 可以執行(取代直接切換回 kernel task)而在 vTaskStartScheduler() 被呼叫時自動建立的 user task,idle task 的 priority 為 0 (lowest),目的是為了確保當有其他 user task 進入 ready list 時可以馬上被執行。

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_AppButtonTask_App
  • 透過 vTaskDelay 來管理 LED 燈閃爍間隔。

4.2 任務間通訊 (Queue)

  • 透過 message queue 讓 task 之間可以做溝通,所以要在 main.c 加上 include。

    ​​​​#include "queue.h"
    
  • xQueueCreate 建立佇列,允許 ButtonTask_App 透過 xQueueSend 傳送訊息給 LEDTask_App

    ​​​​QueueHandle_t MsgQueue = NULL;
    
    ​​​​int main(void){
    ​​​​  MsgQueue = xQueueCreate(1, sizeof(unsigned int));
    ​​​​}
    
  • xQueueReceiveLEDTask_App 內部接收訊息,以切換 LED 狀態。

4.3 Debounce handling

  • 關於 Bounce

    • 按下電源開關時,電壓不會從 0 伏直接升到 VDD 伏。而是在 0 及 VDD 間震盪好幾次,最後才在 VDD 端穩定下來。一個電子產品若有彈跳現象的話,最常見到的「症狀」是按下一個開關,結果數字跳好幾下。
    • image
  • 如何 Debounce

    • HAL_Delay(100) 避免按單次按鈕被多次檢測,並再次讀取 GPIO 確保按鍵確實被按下。
    • while(HAL_GPIO_ReadPin(BLUE_BUTTON_GPIO_Port, GPIO_PIN_0)) 檢查按鈕是否長按,長按按鈕的話有可能會傳送一次以上的訊號,與 Lab 要求不合。
      ​​​​​​​​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_AppButtonTask_App,學習如何建立多任務、使用 Queue 進行通訊、管理任務優先級及處理按鍵 Debounce 等概念。

6. 參考資料

成大 wiki FreeRTOS
2023 嵌入式作業系統分析與實作 Lab1 report