---
# 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
---