學完本章你將能夠:
用 Queue 傳遞命令結構,避免任務之間共享可變全域狀態。
區分二進位信號量、計數信號量與 Mutex,並選對「DMA 完成」同步。
在 ISR 裡只呼叫 |*FromISR|,並正確處理 |pxHigherPriorityTaskWoken|。
解釋優先權反轉,以及 Mutex 的優先權繼承如何緩解它。
多任務一旦開始「順便改同一個 |g_light_on|」,bug 就會變成時序競賽。 正確做法:誰擁有資料,誰才改它;其他人傳訊息。
Queue:把命令當成郵件
本專案的核心型別:
typedef enum CMD_NONE = 0, CMD_LIGHT_ON, CMD_LIGHT_OFF, CMD_LIGHT_UP, CMD_LIGHT_DOWN, CMD_FAN_ON, CMD_FAN_OFF, CMD_FAN_UP, CMD_FAN_DOWN, CMD_DOOR_OPEN, CMD_DOOR_CLOSE, CMD_RELAY_ON, CMD_RELAY_OFF, CMD_BUZZER_BEEP, CMD_BUZZER_OFF, CMD_QUERY_TEMP, CMD_ALL_OFF, CMD_ENTER_SLEEP, CMD_HOME_LEAVE, /* 離家模式(第 10 章練習) */ CMD_LIGHT_SET, CMD_FAN_SET /* 絕對值,智慧助理用(第 13 章) */ cmd_id_t;
typedef enum /* 指令來源:稽核與安全政策用 */ CMD_SRC_LOCAL = 0, /* 路徑 A/B 本地語音 */ CMD_SRC_GATEWAY, /* 路徑 C 智慧助理(第 13 章) */ CMD_SRC_DEBUG cmd_src_t;
typedef struct cmd_id_t id; uint8_t arg; /* 可選參數,例如亮度絕對值 */ uint8_t src; /* cmd_src_t;省略初始化時即為本地 */ uint32_t tick; /* 產生時間,便於逾時丟棄 */ cmd_msg_t;
ASR 任務(或 UART 除錯任務)|xQueueSend|;Device 任務 |xQueueReceive|。|src| 標記指令從哪裡來——本地語音、智慧助理 閘道或除錯口——第 13 章的安全政策與稽核 log 都靠它。佇列深度 8 通常 夠用:語音指令是人類速度,不是每毫秒一筆。音訊幀則用另一條更深、元素更大的佇列,或改 ring buffer + 信號量(第 8 章)。
void vTaskDevice(void *arg) cmd_msg_t msg; (void)arg; for (;;) if (xQueueReceive(g_cmd_q, &msg, portMAX_DELAY) == pdTRUE) device_apply(&msg);
Semaphore:事件,不是資料
I2S DMA 半滿/全滿時,ISR 給二進位信號量,Audio 任務被喚醒去處理 已經在靜態緩衝裡的資料——不必把 320 bytes 複製進佇列。
void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s) BaseType_t woken = pdFALSE; (void)hi2s; xSemaphoreGiveFromISR(g_audio_sem, &woken); portYIELD_FROM_ISR(woken);
計數信號量適合「生產比消費快一點」的短突發;若計數一直漲,代表消費者 太慢,應降取樣或升優先權,而不是把最大值設成 1000 假裝沒事。
Mutex:保護共享匯流排
I2C 上若掛了溫濕度與 OLED,兩個任務不能交錯發出起始條件。Mutex 與 二進位信號量的差別:Mutex 有持有者、可優先權繼承,且不可在 ISR 裡 Give/Take。
void sensor_read_temp(float *out) xSemaphoreTake(g_i2c_mtx, portMAX_DELAY); dht_or_sht_read(out); xSemaphoreGive(g_i2c_mtx);
UART 輸出同樣需要鎖,或(較佳)讓唯一的 Log 任務從佇列取字串再送, 其他任務只 |xQueueSend|。後者沒有「持鎖時被搶佔導致亂碼」的問題。
優先權反轉(課堂必考)
低優先權任務持有 I2C Mutex,高優先權任務也要 I2C,卻被一個不需要 I2C 的中優先權任務一直搶 CPU——高優先權等於被中優先權卡住。Mutex 的 優先權繼承會把持鎖者暫時提升,縮短這個窗口。信號量沒有繼承,因此 I2C/SPI 這種臨界區請用 Mutex。
實驗:按鍵經佇列切設備
firmware/lab05_queue_semaphore/。EXTI 按鍵 ISR 送 CMD_LIGHT_ON/OFF 進佇列;Device 任務執行。另用一個週期任務 嘗試印 log,證明沒有 mutex/log 佇列時會交錯,加上之後就整齊。
void HAL_GPIO_EXTI_Callback(uint16_t pin) cmd_msg_t msg = .id = CMD_LIGHT_ON, .arg = 0, .tick = xTaskGetTickCountFromISR() ; BaseType_t woken = pdFALSE; if (pin == USER_Btn_Pin) xQueueSendFromISR(g_cmd_q, &msg, &woken); portYIELD_FROM_ISR(woken);
把佇列深度改成 1,在 ISR 裡連續模擬 5 次送出,觀察哪些命令被丟棄 (檢查 |xQueueSendFromISR| 回傳值並點亮 LD3)。然後改回 8,並在 Device 任務人為 |vTaskDelay(500)|,再感受「深度用來吸收突發」。