---
# System prepended metadata

title: ISR → 丟 Queue → Task 處理（FreeRTOS 新手教程）

---


---

# 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

---
