學完本章你將能夠:
說明搶佔式排程如何依優先權切換,以及時間片在同優先權下的角色。
建立週期任務與事件驅動任務,並為每個任務估算堆疊。
正確交接 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 相反——這點務必記牢)。
| 任務 | 優先權 | 理由 |
|---|---|---|
| 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 或卡死現象,然後改回安全值——用身體記住 堆疊溢位長什麼樣子。