--- # ISR → 丟 Queue → Task 處理(FreeRTOS 新手教程) ## 1) 核心概念(你要背到反射) * **ISR**:只做「讀資料 + 清旗標 + 送通知」,越短越好 * **Task**:做「重處理」(解析/濾波/通訊/printf/寫Flash/演算法) 原因就兩點: 1. ISR 做太久 → 中斷延遲變大、系統即時性崩 2. 很多重活會 **阻塞/拿鎖/不可重入**(printf、malloc、flash、driver lock) --- ## 2) ISR 裡必做 / 禁做清單 ### ISR 必做(建議順序) 1. **確認中斷來源**(同一 IRQ 可能多事件) 2. **讀資料**(先讀出來,避免被覆蓋) 3. **清旗標**(不清就會一直進 ISR) 4. **用 FromISR API 丟出去**(Queue/Notify/RingBuffer index) 5. **必要時切換到高優先 Task**:`portYIELD_FROM_ISR(...)` ### ISR 禁做 * `printf`(慢、可能鎖) * `malloc/free`(碎片化/非 reentrant/可能鎖) * 長迴圈、大量計算、等待(busy wait / delay) * 呼叫會 block 的 RTOS API(要用 `...FromISR`) --- ## 3) Queue 方案:事件型(ADC、GPIO、Timer、狀態機事件) ### 3.1 事件資料結構:小、固定長度 ```c typedef struct { uint32_t ts; // timestamp(可選) uint8_t id; // event id uint16_t value; // payload(例:ADC值) } evt_t; static QueueHandle_t q_evt; ``` ### 3.2 初始化:Queue 深度怎麼選? * 深度 = 事件爆發時「Task 來不及處理」的緩衝 * 常見先用 16 或 32,再用統計觀察是否溢出 ```c void app_init(void) { q_evt = xQueueCreate(16, sizeof(evt_t)); configASSERT(q_evt); // init hardware... // enable IRQ... } ``` ### 3.3 ISR:FromISR + Yield(標準模板) ```c void ADC_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 1) 讀資料 uint16_t adc = (uint16_t)ADC->RESULT; // 2) 清旗標(依晶片而定) ADC->EVENTS_END = 0; // 3) 打包最小事件 evt_t e = { .ts = xTaskGetTickCountFromISR(), .id = 1, .value = adc }; // 4) 丟 queue(ISR專用API) if (xQueueSendFromISR(q_evt, &e, &xHigherPriorityTaskWoken) != pdPASS) { // Queue滿了:不要卡在ISR // 做法:記錄drop計數、或設定旗標 // drop_count++; } // 5) 如果喚醒了更高優先Task,就立即切換 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } ``` ### 3.4 Task:阻塞式接收 + 重處理 ```c void task_evt(void *arg) { evt_t e; for (;;) { if (xQueueReceive(q_evt, &e, portMAX_DELAY) == pdPASS) { // 重處理:濾波/控制/打log/送通訊 // filter(e.value); // send_uart(...); // printf(...); // 放這裡OK } } } ``` --- ## 4) Queue 滿了怎麼辦? Queue 滿通常是:**ISR 產生事件速度 > Task 消化速度**。常見策略: ### 策略 A:丟掉新事件(最簡單) * `xQueueSendFromISR` 失敗就 drop,並累計 drop 次數 * 適合「事件不是每個都必須」:例如按鍵抖動、狀態更新 ### 策略 B:保留最新(狀態型最適合) * 如果事件是「最新狀態才重要」,用 **Overwrite** 概念 * FreeRTOS 有 `xQueueOverwriteFromISR`(但 queue 長度需為 1) ### 策略 C:不要在 Queue 放大資料,改放 index/pointer * ISR 只丟「ring buffer 的索引」或「指標」 * Task 取 index 再去 buffer 拿資料 * 適合 UART 大量 bytes、音訊、sensor stream --- ## 5) 什麼時候不要用 Queue?改用 Task Notification(更快) Queue 有複製資料、管理結構的成本。若是: * 單一 ISR → 單一 Task(1 producer / 1 consumer) * 只要傳「計數」或「bit flag」或「32-bit 值」 用 **Direct-to-Task Notification** 通常更省 RAM、更快。 ### 5.1 通知計數:ISR 加 1,Task 一次拿走累積 ```c static TaskHandle_t h_evt_task; void ADC_IRQHandler(void) { BaseType_t hpw = pdFALSE; // ...讀資料/清旗標... vTaskNotifyGiveFromISR(h_evt_task, &hpw); portYIELD_FROM_ISR(hpw); } void task_evt(void *arg) { for (;;) { // 等到至少有一次通知 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 做重處理(可一次處理多筆/批次) } } ``` ### 5.2 傳 32-bit 值(例如最新 ADC 值) ```c static TaskHandle_t h_evt_task; void ADC_IRQHandler(void) { BaseType_t hpw = pdFALSE; uint32_t v = (uint32_t)ADC->RESULT; ADC->EVENTS_END = 0; // 覆蓋最新值(只保留最後一次) xTaskNotifyFromISR(h_evt_task, v, eSetValueWithOverwrite, &hpw); portYIELD_FROM_ISR(hpw); } void task_evt(void *arg) { uint32_t v; for (;;) { xTaskNotifyWait(0, 0xFFFFFFFF, &v, portMAX_DELAY); // v 是最新 ADC 值 } } ``` > 選擇建議: > > * 「每個事件都要」→ Queue > * 「只要最新」→ Notify overwrite / queue length=1 overwrite > * 「只要知道有沒有發生/發生幾次」→ Notify give/take --- ## 6) UART / 大量資料:Ring Buffer + 丟 index(新手最常踩坑) Queue 不適合塞一大串 bytes(成本高、容易滿)。常見做法: * ISR:把 byte 塞進 ring buffer(或 DMA buffer),只通知 task「有資料」 * Task:被喚醒後把 ring buffer 一次拉出來做解析 ### 6.1 基本骨架(概念版) ```c #define RB_SIZE 256 static uint8_t rb[RB_SIZE]; static volatile uint16_t w = 0, r = 0; static TaskHandle_t h_uart_task; static inline void rb_push(uint8_t b) { uint16_t nw = (w + 1) % RB_SIZE; if (nw != r) { rb[w] = b; w = nw; } // 滿了就丟(或記錄overflow) } static inline int rb_pop(uint8_t *out) { if (r == w) return 0; *out = rb[r]; r = (r + 1) % RB_SIZE; return 1; } void UART_IRQHandler(void) { BaseType_t hpw = pdFALSE; uint8_t b = UART->RXD; // clear flags... rb_push(b); vTaskNotifyGiveFromISR(h_uart_task, &hpw); portYIELD_FROM_ISR(hpw); } void task_uart(void *arg) { for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); uint8_t b; while (rb_pop(&b)) { // parse byte stream... } } } ``` --- ## 7) 新手常見地雷(超高頻) 1. **忘記清中斷旗標** → ISR 一直進、系統像被鬼附身 2. **ISR 用了非 FromISR API** → 隨機死、偶發卡死 3. **Queue 放太大物件** → memcpy 成本高、爆 RAM、效率差 4. **Task 優先權太低** → queue 一直滿(你以為 ISR 很快,其實 task 完全追不上) 5. **在 ISR 做重處理** → latency 爆、音訊/通訊抖動 6. `volatile` 誤用: * ISR/task 共用的「旗標/索引」常需要 `volatile` * 但多執行緒正確性不只靠 volatile(必要時用臨界區/原子) --- ## 8) 你可以直接套用的「選型小抄」 * **事件一筆一筆重要**(按鍵事件、狀態事件、sensor event):Queue * **只要最新值**(最新電量、最新溫度、最新 ADC):Notify overwrite 或 queue length=1 overwrite * **大量連續資料流**(UART、I2S、SPI burst):DMA/RingBuffer + Notify ---