什麼是中斷?
在嵌入式系統中,GPIO 中斷(GPIO Interrupt)是讓微控制器即時響應外部事件的核心機制——不用 CPU 一直輪詢(Polling)GPIO 腳位的電位變化,而是當事件發生時由硬體主動通知 CPU 暫停當前工作去處理。
想像你在看書(主程式在跑),有人敲門(GPIO 觸發),你放下一本書去開門(執行 ISR),開完門回來繼續看書(恢復主程式)——這就是中斷的日常類比。
相較於輪詢(Polling),中斷的好處非常明顯:
- 省電:CPU 不需要一直去檢查 GPIO 狀態,可以待在睡眠模式
- 即時性:事件發生到回應的延遲(Latency)可控且極短
- 效率:不用浪費 CPU 週期在無意義的查詢上
中斷與 DMA 傳輸經常搭配使用——DMA 負責搬資料,中斷負責通知 CPU「資料搬好了」。
GPIO 外部中斷的工作原理
GPIO 外部中斷的硬體路徑大致如下:
- GPIO 腳位:外部訊號(按鈕、感測器輸出、其他 MCU 的訊號)接到 GPIO
- 邊緣/電平檢測電路:每個 GPIO 內部都有硬體電路可以檢測 Rising Edge、Falling Edge 或 Low Level
- NVIC 或 CPU 中斷控制器:當中斷條件滿足時,向 CPU 發送中斷請求
- 向量表中查找 ISR:CPU 根據中斷編號,在 Vector Table 中找到對應的中斷服務函式入口
- 執行 ISR:CPU 保存當前 Context,執行 ISR
- 返回:ISR 執行完畢,CPU 恢復 Context 回到原本的程式

中斷觸發方式
GPIO 外部中斷支援四種觸發方式:
- Rising Edge Trigger(上升緣觸發):GPIO 從 Low 變 High 時觸發
- Falling Edge Trigger(下降緣觸發):GPIO 從 High 變 Low 時觸發
- Both Edge Trigger(雙緣觸發):電平無論上升或下降都觸發
- Level Trigger(電平觸發):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。
// 設定中斷優先級(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 就是最後一道防線。
文章評論