學完本章你將能夠:

  • 說明搶佔式排程如何依優先權切換,以及時間片在同優先權下的角色。

  • 建立週期任務與事件驅動任務,並為每個任務估算堆疊。

  • 正確交接 SysTick,使用 |vTaskDelay| 而不是 |HAL_Delay|。

  • 用 |uxTaskGetStackHighWaterMark| 證明堆疊沒有默默溢位。

從本章起,超級迴圈退役。系統變成一群「看起來同時在跑」的函式——任務 (Task)。語音控制器至少需要音訊、辨識、設備、感測、通訊、監控六類任務, 先用三個 LED 任務把肌肉練好。

任務是什麼

任務就是一個永不返回的 C 函式:

void vTaskDevice(void *arg) (void)arg; for (;;) /* 等待命令、驅動 GPIO/PWM */

每個任務有:優先權、堆疊、狀態(Running / Ready / Blocked / Suspended)。 高優先權就緒時,低優先權立刻被搶佔。同優先權則依 |configTICK_RATE_HZ| 做時間片輪轉(需開啟 |configUSE_TIME_SLICING|)。

本專案的優先權表

數字越大越優先(FreeRTOS 慣例,與 NVIC 相反——這點務必記牢)。

建議優先權(app_types.h)
任務 優先權 理由
Audio / ASR RX 5 避免 DMA/UART 環形緩衝溢位
Wake / ASR parse 4 推論或模組句柄要盡快消化
Device control 3 使用者可感知的動作
Comms 2 允許被控制打斷
Gateway(第 13 章) 2 助理指令與狀態回推,與 Comms 同級
Sensor 1 數秒級週期
Monitor 1 統計與餵狗,可與 Sensor 同級
Idle 0 核心自帶,可掛低功耗

不要把所有任務都設成同一高優先權「以求保險」。那樣只是把超迴圈拆成 幾個互相搶時間片的迴圈,失去搶佔的意義,而且堆疊浪費。

建立任務

CMSIS-RTOS v2 與原生 FreeRTOS API 都可以。本課程應用層用原生 API, 語意較清楚;Cube 產生的 |MX_FREERTOS_Init| 可只保留 kernel 啟動。

#define STK_SMALL 256

void app_os_start(void) xTaskCreate(vTaskBlink, "blink1", STK_SMALL, (void *)0, 1, NULL); xTaskCreate(vTaskBlink, "blink2", STK_SMALL, (void *)1, 1, NULL); xTaskCreate(vTaskBlink, "blink3", STK_SMALL, (void *)2, 2, NULL); vTaskStartScheduler();

static void vTaskBlink(void *arg) const uint32_t id = (uint32_t)arg; const TickType_t period[] = pdMS_TO_TICKS(200), pdMS_TO_TICKS(400), pdMS_TO_TICKS(1000) ; for (;;) app_led_toggle((uint8_t)id); vTaskDelay(period[id]);

第三個任務優先權較高,它就緒時另外兩個會被打斷——用邏輯分析儀看三根 LED,高優先權的邊緣會「插入」另外兩條波形。

延遲:相對 vs 絕對

|vTaskDelay(T)| 從「現在」再等 T。若任務本身執行超過預期,週期會漂。 音訊統計、感測器取樣應使用 |vTaskDelayUntil|,鎖在絕對時間表上。

void vTaskSensor(void *arg) TickType_t last = xTaskGetTickCount(); (void)arg; for (;;) sensor_sample(); vTaskDelayUntil(&last, pdMS_TO_TICKS(2000));

堆疊與 heap

|xTaskCreate| 的堆疊單位是 Word(Cortex-M 上 4 bytes)。 256 words = 1 KB,對閃燈夠用;含 |snprintf| 的通訊任務建議 512–1024。 路徑 B 的推論任務視模型而定,常要數 KB,且張量本身應在獨立緩衝。

Cube 的 FreeRTOSConfig.h 裡 |configTOTAL_HEAP_SIZE| 必須大於「所有任務堆疊 + 佇列 + 對齊損耗」。不夠時 |xTaskCreate| 回傳 |pdFAIL|——一定要檢查,否則系統少一個 任務卻假裝正常。

在 Monitor 任務每 5 秒印 |uxTaskGetStackHighWaterMark| 與 |xPortGetFreeHeapSize()|。 高水位接近 0 就是溢位前兆。這比等 HardFault 再查便宜得多。

實驗:三任務心跳

firmware/lab04_freertos_blinky/。三顆 LED 不同週期;UART 每秒印一次任務名稱與堆疊高水位。確認 |HAL_Delay| 已從任務裡消失。

把 Blink3 的優先權降到與另外兩個相同,再拉高。觀察 UART 時間戳與 LED 相位變化。另:故意把某任務堆疊改成 64 words 並在裡頭呼叫 |snprintf|,記錄 HardFault 或卡死現象,然後改回安全值——用身體記住 堆疊溢位長什麼樣子。