0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

GPIO 中斷(外部中斷)完整教學:從原理到 ESP32/STM32 實作

2026 年 6 月 4 日 9點熱度 0人點贊 0條評論

什麼是中斷?

在嵌入式系統中,GPIO 中斷(GPIO Interrupt)是讓微控制器即時響應外部事件的核心機制——不用 CPU 一直輪詢(Polling)GPIO 腳位的電位變化,而是當事件發生時由硬體主動通知 CPU 暫停當前工作去處理。

想像你在看書(主程式在跑),有人敲門(GPIO 觸發),你放下一本書去開門(執行 ISR),開完門回來繼續看書(恢復主程式)——這就是中斷的日常類比。

相較於輪詢(Polling),中斷的好處非常明顯:

  • 省電:CPU 不需要一直去檢查 GPIO 狀態,可以待在睡眠模式
  • 即時性:事件發生到回應的延遲(Latency)可控且極短
  • 效率:不用浪費 CPU 週期在無意義的查詢上

中斷與 DMA 傳輸經常搭配使用——DMA 負責搬資料,中斷負責通知 CPU「資料搬好了」。

GPIO 外部中斷的工作原理

GPIO 外部中斷的硬體路徑大致如下:

  1. GPIO 腳位:外部訊號(按鈕、感測器輸出、其他 MCU 的訊號)接到 GPIO
  2. 邊緣/電平檢測電路:每個 GPIO 內部都有硬體電路可以檢測 Rising Edge、Falling Edge 或 Low Level
  3. NVIC 或 CPU 中斷控制器:當中斷條件滿足時,向 CPU 發送中斷請求
  4. 向量表中查找 ISR:CPU 根據中斷編號,在 Vector Table 中找到對應的中斷服務函式入口
  5. 執行 ISR:CPU 保存當前 Context,執行 ISR
  6. 返回:ISR 執行完畢,CPU 恢復 Context 回到原本的程式

NVIC 中斷控制器架構

中斷觸發方式

GPIO 外部中斷支援四種觸發方式:

  • Rising Edge Trigger(上升緣觸發):GPIO 從 Low 變 High 時觸發
  • Falling Edge Trigger(下降緣觸發):GPIO 從 High 變 Low 時觸發
  • Both Edge Trigger(雙緣觸發):電平無論上升或下降都觸發
  • Level Trigger(電平觸發):GPIO 保持在特定電平時持續觸發(較少用於 GPIO)

GPIO 外部中斷觸發時序

上圖展示了三種中斷信號的差異:

  • Line A:每當 GPIO 從 0→1 時,Rising Edge 觸發
  • Line B:每當 GPIO 從 1→0 時,Falling Edge 觸發
  • Line C, D:Both Edge 模式在每個 Transitions 都觸發

選擇哪種觸發方式取決於應用:

  • 按鈕按下 → 通常用 Falling Edge(內部 Pull-Up,按下=Low)
  • 編碼器(Encoder)→ Both Edge 才能捕捉所有脈衝
  • 感測器中斷輸出 → 看 datasheet 指定 Rising 還是 Falling

ESP32 GPIO 中斷實作

ESP32 使用 Tensilica Xtensa LX6 核心(或 LX7 in S3),中斷控制器支援所有 GPIO 的中斷。

Arduino 框架:attachInterrupt()

// ESP32 Arduino - GPIO 外部中斷範例(按鈕計數)
const int BUTTON_PIN = 0;   // BOOT button on most dev boards
const int LED_PIN    = 2;
volatile int press_count = 0;
volatile bool triggered = false;

void IRAM_ATTR button_isr() {
    press_count++;
    triggered = true;
}

void setup() {
    Serial.begin(115200);
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);

    // 設定 Falling Edge 觸發(按鈕按下時 GPIO 從 High→Low)
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), button_isr, FALLING);
}

void loop() {
    if (triggered) {
        triggered = false;
        Serial.printf("按鈕已被按下 %d 次\n", press_count);
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    }
}

關鍵重點:

  • IRAM_ATTR — 強制 ISR 程式碼放在 IRAM 中,避免 Flash 快取未命中造成延遲
  • volatile — 編譯器不會優化這個變數,確保 ISR 和主迴圈之間共用資料正確
  • ISR 中只做最小必要操作,複雜邏輯放主迴圈

中斷也常與 FreeRTOS 多工管理搭配,在 ISR 中用 xQueueSendFromISR() 將事件傳遞給任務。

ESP-IDF 框架

// ESP-IDF - GPIO 中斷範例
#include "driver/gpio.h"

#define GPIO_BUTTON   GPIO_NUM_0
#define GPIO_LED      GPIO_NUM_2

static QueueHandle_t gpio_evt_queue = NULL;

static void IRAM_ATTR gpio_isr_handler(void* arg) {
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

void gpio_task_example(void* arg) {
    uint32_t io_num;
    for(;;) {
        if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            printf("GPIO %d 觸發中斷!\n", io_num);
            gpio_set_level(GPIO_LED, !gpio_get_level(GPIO_LED));
        }
    }
}

void app_main() {
    gpio_config_t io_conf = {
        .intr_type    = GPIO_INTR_NEGEDGE,
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = 1,
        .pin_bit_mask = (1ULL << GPIO_BUTTON),
    };
    gpio_config(&io_conf);
    gpio_set_direction(GPIO_LED, GPIO_MODE_OUTPUT);
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
    gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
    gpio_isr_handler_add(GPIO_BUTTON, gpio_isr_handler, (void*) GPIO_BUTTON);
}

STM32 GPIO 中斷實作

STM32 使用 ARM Cortex-M 核心搭配 NVIC(Nested Vectored Interrupt Controller),中斷機制比 ESP32 更嚴謹且支援巢狀搶佔。關於 硬體定時器(Timer)的中斷原理類似,但定時器中斷是由內部計數器觸發而非 GPIO。

STM32 HAL 框架

// STM32 HAL - GPIO 外部中斷範例 (STM32F4)
void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitStruct.Pin   = GPIO_PIN_0;
    GPIO_InitStruct.Mode  = GPIO_MODE_IT_FALLING;
    GPIO_InitStruct.Pull  = GPIO_PULL_UP;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0) {
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
    }
}

STM32 中斷優先級管理

ARM Cortex-M 的 NVIC 支援 巢狀中斷:高優先級的中斷可以打斷正在執行的低優先級 ISR。

NVIC 中斷優先級搶佔示意

// 設定中斷優先級(STM32F4)
HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 0);   // 感測器中斷(高優先級)
HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0);   // 按鈕中斷(低優先級)

中斷服務函式(ISR)最佳實踐

中斷處理流程

寫 ISR 跟寫一般函式很不一樣,以下是最重要的幾條守則:

✅ 可以做的事 ❌ 不該做的事
讀取/寫入 GPIO delay() / HAL_Delay()
設定 volatile 標記 Serial.print() / printf()
xQueueSendFromISR() malloc() / new
清除中斷標記 長時間迴圈 / 阻塞操作
讀取感測器暫存器 操作 I²C/SPI 通訊(除非 DMA)

按鈕消抖(Debouncing)

機械式按鈕按下時會有彈跳(Bouncing)現象,GPIO 會在短時間內快速變換,造成多次中斷。

按鈕彈跳與軟體消抖時序

// 軟體消抖範例(ESP32 Arduino)
const int DEBOUNCE_MS = 50;
volatile unsigned long last_interrupt_time = 0;

void IRAM_ATTR debounced_isr() {
    unsigned long now = millis();
    if (now - last_interrupt_time > DEBOUNCE_MS) {
        last_interrupt_time = now;
        button_pressed = true;
    }
}

中斷向量表(Vector Table)

中斷向量表是 MCU 啟動時設定的表格,記錄了每個中斷編號對應的 ISR 函式位址。在 ARM Cortex-M 上,Vector Table 通常放在 Flash 開頭。中斷向量表的設計與 FIFO 緩衝區類似——都是為了解決生產者(硬體事件)和消費者(CPU)之間的速度匹配問題。

// STM32 預設中斷向量表 (節錄)
//   6        EXTI0_IRQHandler   // ← GPIO PA0 中斷
//   7        EXTI1_IRQHandler   // ← GPIO PA1 中斷
//  23        EXTI9_5_IRQHandler  // ← GPIO 5~9 共享
//  40        EXTI15_10_IRQHandler // ← GPIO 10~15 共享

STM32 的 GPIO 中斷是分組的:PA0~Px0 共享 EXTI0、PA1~Px1 共享 EXTI1 等,同一個 EXTI 編號的不同 Port 不能同時使用外部中斷。

ESP32 vs STM32 中斷對照表

功能 ESP32 (Arduino) STM32 (HAL)
註冊 ISR attachInterrupt(pin, isr, mode) HAL_GPIO_EXTI_IRQHandler(pin) + Callback
觸發方式 RISING, FALLING, CHANGE, LOW, HIGH GPIO_MODE_IT_RISING, _FALLING
ISR 中關中斷 portDISABLE_INTERRUPTS() __disable_irq()
優先級設定 ESP_INTR_FLAG_LEVELx HAL_NVIC_SetPriority()
中斷巢狀 有限支援 NVIC 完整支援
可用 GPIO 數量 所有 GPIO(最多 34 腳) 最多 16 個 EXTI(每個 Pin 可共享)

實戰:雙按鈕中斷控制 LED 亮度

綜合以上所學,來寫一個實戰範例:兩個按鈕分別控制 LED 亮度增減。PWM 信號產生可參考 PWM 完整教學。

// ESP32 - 雙按鈕中斷 PWM 控制 LED 亮度
const int BTN_UP   = 0;
const int BTN_DOWN = 4;
const int LED_PIN  = 2;

volatile bool up_pressed = false;
volatile bool down_pressed = false;

void IRAM_ATTR up_isr()   { up_pressed   = true; }
void IRAM_ATTR down_isr() { down_pressed = true; }

int brightness = 128;

void setup() {
    Serial.begin(115200);
    pinMode(BTN_UP,   INPUT_PULLUP);
    pinMode(BTN_DOWN, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(BTN_UP),   up_isr,   FALLING);
    attachInterrupt(digitalPinToInterrupt(BTN_DOWN), down_isr, FALLING);
    ledcSetup(0, 5000, 8);
    ledcAttachPin(LED_PIN, 0);
    ledcWrite(0, brightness);
}

void loop() {
    if (up_pressed) {
        up_pressed = false;
        brightness = min(brightness + 32, 255);
        ledcWrite(0, brightness);
        Serial.printf("亮度: %d\n", brightness);
    }
    if (down_pressed) {
        down_pressed = false;
        brightness = max(brightness - 32, 0);
        ledcWrite(0, brightness);
    }
    delay(50);
}

常見陷阱與除錯技巧

陷阱 1:ISR 中做太多事情

ISR 應該保持極簡——設定一個標記(Flag),然後馬上返回。

陷阱 2:忘記 volatile

ISR 與主迴圈/任務共用的全域變數必須加上 volatile。

陷阱 3:NVIC 優先級分組不一致

STM32 上,整個專案的 NVIC_PriorityGroup 必須一致。

陷阱 4:中斷標記未清除

void EXTI0_IRQHandler(void) {
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
        // 處理中斷...
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    }
}

總結

GPIO 外部中斷是嵌入式系統中不可或缺的基礎技術。從 ESP32 的 attachInterrupt() 到 STM32 的 HAL + NVIC,雖然語法和 API 不同,但核心概念是一致的。掌握中斷後,可以進一步學習 Watchdog Timer 的應用——當系統因中斷異常而卡住時,Watchdog 就是最後一道防線。

標籤: 教學
最後更新:2026 年 6 月 5 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang