# STM32 教學系列 第 3 節：GPIO 進階（按鍵、中斷、去彈跳）

## 🎯 學習目標

1. **掌握 GPIO 輸入配置** - 實現按鍵檢測功能
2. **理解外部中斷系統（EXTI）** - 學會事件驅動編程模式
3. **實現按鍵去彈跳算法** - 透過軟體解決機械按鍵的抖動問題

---

## 🎓 GPIO 進階概念

### GPIO 模式回顧

GPIO（通用輸入/輸出）支援多種工作模式：

| 模式 | 描述 | 用途 |
|------|------|------|
| **GPIO_MODE_OUTPUT_PP** | 開路輸出（Push-Pull） | 驅動 LED、控制繼電器 |
| **GPIO_MODE_INPUT** | 浮空輸入 | 一般輸入（需注意雜訊） |
| **GPIO_MODE_INPUT_PU** | 上拉輸入 | 按鍵檢測（推薦） |
| **GPIO_MODE_INPUT_PD** | 下拉輸入 | 特殊應用 |
| **GPIO_MODE_AF_PP** | 複用功能 | UART、SPI 等外設 |
| **GPIO_MODE_IT_FALLING** | 下降邊緣中斷 | 按鍵按下時觸發 |

### 按鍵接線原理

標準的按鍵接線方式：

```
         ┌──────────┐
         │  按鍵 K1 │
         └─────┬────┘
               │
        ┌──────┴─────┐
        │ (按下時接通)│
        │             │
       GND          GPIO_PIN_XX (上拉狀態)
        
狀態說明：
- 按鍵未按下：GPIO = 高電位 (3.3V)  → GPIO_PIN_SET
- 按鍵按下：   GPIO = 低電位 (0V)   → GPIO_PIN_RESET
```

### 按鍵抖動（Bouncing）問題

機械按鍵存在物理特性：按下或鬆開時，接點會產生多次快速的通/斷切換，稱為「抖動」。

```
無抖動情況（理想）：
GPIO ┌─────┐
     └─────┘

有抖動情況（真實）：
GPIO ┌──┐ ┌───┐ ┌──┐
     └──┘─┘   └─┘  └─ (多次跳變)
     
後果：會觸發多次中斷，導致計數錯誤
```

### 去彈跳方法

| 方法 | 優點 | 缺點 | 成本 |
|------|------|------|------|
| **軟體去彈跳** | 無需硬體 | 佔用 CPU 時間 | 低 |
| **硬體濾波** | 無需軟體 | 需增加 RC 電路 | 中 |
| **中斷+計時器** | 精確 | 程式複雜 | 中 |

本課程採用 **軟體去彈跳**（最常用）。

---

## 🛠️ 硬體接線

### 所需元件

| 元件 | 數量 | 說明 |
|------|------|------|
| 按鈕開關（Push Button） | 1 | 常開型 |
| 220Ω 限流電阻 | 1 | 保護 GPIO（選配） |
| 跳線 | 2 | 連接按鍵和 GPIO |

### 接線圖

| 組件 | 接腳 | 連接說明 |
|------|------|---------|
| 按鈕 A 端 | 3.3V | 一端連接電源 |
| 按鈕 B 端 | PA0 | 另一端連接 GPIO（上拉） |
| PA0 | GND | 經過上拉電阻到地 |
| LED | PA5 | 用於指示按鍵狀態（可選） |

**CubeMX 配置 PA0：**
- **GPIO mode**: Input
- **Pull**: Pull-up（上拉）
- **Label**: KEY_Input

### 麵包板接線參考

```
┌─────────────────┐
│     3.3V (VDD)  │ ← 電源
└────────┬────────┘
         │
      ┌──┴──┐
      │ KEY │ ← 按鍵
      └──┬──┘
         │
      ┌──┴──────┐
      │ PA0     │ ← STM32 GPIO（上拉狀態）
      └─────────┘
         │
      ┌──┴──────┐
      │   GND   │ ← 地線（內部上拉已連接）
      └─────────┘
```

---

## ⚙️ CubeMX 配置步驟

### 步驟 1：GPIO 輸入配置

1. 開啟 CubeMX，在 Pinout 圖上找到 **PA0**
2. 右鍵點擊 PA0，選擇 **GPIO_Input**
3. 在左側 **System Core** 中選擇 **GPIO**
4. 展開 **GPIOA** 並確認 PA0 的設定：
   - **GPIO mode**: Input
   - **Pull**: Pull-up
   - **Speed**: Medium
   - **User Label**: KEY_Input

### 步驟 2：配置外部中斷（EXTI）

1. 返回 Pinout 圖，右鍵點擊 **PA0**
2. 選擇 **GPIO_EXTI0**（改為中斷模式）
3. 在左側選擇 **System Core** → **NVIC**
4. 找到 **EXTI line0 interrupt** 並勾選 **Enabled**
5. 設定優先級：
   - **Preemption Priority**: 0
   - **Sub Priority**: 0

### 步驟 3：系統時脈配置（使用第 2 節配置）

確保 SysTick 定時器已啟用（用於 HAL_Delay）

### 步驟 4：生成程式碼

點擊 **Project** → **Generate Code**

---

## 💻 完整程式碼

### main.c - 按鍵中斷 + 去彈跳

```c
/* STM32 Lesson 03 - Key Press with Debouncing
 * 功能：透過按鍵控制 LED，使用軟體去彈跳技術
 * 難度：中級
 */

#include "main.h"
#include "gpio.h"

/* 私有函數宣告 */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);
void Debounce_Key(void);

/* 去彈跳參數 */
#define DEBOUNCE_DELAY_MS  20   /* 去彈跳延遲時間（毫秒） */
#define DEBOUNCE_SAMPLES   3    /* 採樣次數 */

/* 全域變數 */
volatile uint8_t key_pressed = 0;       /* 按鍵按下標誌 */
volatile uint32_t last_interrupt_time = 0;  /* 上次中斷時間 */
uint8_t led_state = 0;                  /* LED 狀態：0=暗，1=亮 */

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  
  /* 初始狀態：LED 熄滅 */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
  led_state = 0;

  while (1)
  {
    /* 軟體去彈跳檢測 */
    if (key_pressed)
    {
      Debounce_Key();
      key_pressed = 0;  /* 清除標誌 */
    }
    
    /* 主程式邏輯（可添加其他功能） */
  }
}

/**
  * @brief GPIO 外部中斷回調函數
  * 當檢測到下降邊緣時自動調用
  * @param GPIO_Pin: 觸發中斷的引腳
  */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_0)
  {
    /* 中斷防抖：檢查距離上次中斷是否超過 50ms */
    uint32_t current_time = HAL_GetTick();
    
    if (current_time - last_interrupt_time > 50)
    {
      key_pressed = 1;  /* 設置標誌，在主迴圈中處理 */
      last_interrupt_time = current_time;
    }
  }
}

/**
  * @brief 軟體去彈跳檢測函數
  * 採用多次採樣驗證按鍵狀態
  */
void Debounce_Key(void)
{
  uint8_t stable_count = 0;
  
  /* 進行多次採樣 */
  for (uint8_t i = 0; i < DEBOUNCE_SAMPLES; i++)
  {
    /* 讀取按鍵狀態 */
    GPIO_PinState key_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
    
    /* 若狀態為低電位（按鍵按下），計數器增加 */
    if (key_state == GPIO_PIN_RESET)
    {
      stable_count++;
    }
    
    /* 每次採樣間隔 */
    HAL_Delay(DEBOUNCE_DELAY_MS);
  }
  
  /* 若所有採樣都檢測到低電位，則確認按鍵按下 */
  if (stable_count == DEBOUNCE_SAMPLES)
  {
    /* 切換 LED 狀態 */
    led_state = !led_state;
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, led_state ? GPIO_PIN_SET : GPIO_PIN_RESET);
  }
}

/**
  * @brief 系統時脈配置（同第 2 節）
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 8;
  RCC_OscInitStruct.PLL.PLLN = 336;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 7;

  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                              | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief GPIO 初始化函數
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO 埠時脈使能 */
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /* 配置 PA5（LED 輸出） */
  GPIO_InitStruct.Pin = GPIO_PIN_5;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* 配置 PA0（按鍵輸入 + 中斷） */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  /* 下降邊緣中斷 */
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* 啟用 EXTI0 中斷 */
  HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

/**
  * @brief 外部中斷 0 服務程式
  */
void EXTI0_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

/**
  * @brief 錯誤處理
  */
void Error_Handler(void)
{
  while (1);
}
```

---

## 🔍 測試與除錯

### 預期結果

✅ **功能驗證：**
1. 按開發板上的 **User Button（PC13）** 或自接按鍵
2. 第一次按下：LED 點亮
3. 第二次按下：LED 熄滅
4. 重複按下實現 LED 開關切換

⚠️ **無抖動現象**：LED 不會因為按鍵抖動而閃爍

### 常見問題與解決

| 問題 | 原因 | 解決方案 |
|------|------|--------|
| **按鍵無反應** | 中斷未啟用或 GPIO 配置錯誤 | 檢查 CubeMX 中 EXTI0_IRQn 是否 Enabled |
| **LED 多次切換** | 去彈跳延遲過短 | 增加 `DEBOUNCE_DELAY_MS` 至 30-50ms |
| **長按持續切換** | 中斷處理邏輯錯誤 | 確認 `key_pressed` 標誌已清除 |
| **編譯錯誤**「EXTI0_IRQHandler 重定義」| 自定義中斷處理與 HAL 衝突 | 確保中斷處理只定義一次 |

### 手動測試按鍵狀態

進階除錯方法：在 Debug 模式中查看按鍵電位值

```c
/* 在中斷回調中添加診斷程式碼 */
GPIO_PinState key_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
// key_state 應為 GPIO_PIN_RESET (0) = 按下
//         或 GPIO_PIN_SET (1) = 未按
```

---

## 📚 進階理解

### 中斷優先級機制

STM32 支援 **優先級分組**（Priority Grouping）：

- **Preemption Priority**（搶佔優先級）：數值小優先級高
  - 優先級 0 > 優先級 1 > 優先級 2 ...
  - 高優先級中斷可中斷低優先級中斷執行

- **Sub Priority**（子優先級）：同搶佔優先級下的排隊順序

```c
/* 設定最高優先級 */
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);  /* 搶佔=0, 子=0 */
```

### 中斷延遲防抖技術

除了多次採樣，也可用時間判斷：

```c
#define ANTI_BOUNCE_TIME 50  /* 50ms 防抖時間 */

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  static uint32_t last_time = 0;
  uint32_t now = HAL_GetTick();
  
  /* 若距離上次觸發 > 50ms，才確認有效按下 */
  if (now - last_time > ANTI_BOUNCE_TIME)
  {
    key_pressed = 1;
    last_time = now;
  }
}
```

---

## 📱 擴展應用

### 挑戰 1：計數功能

修改代碼，使每次按下按鍵時透過 UART 列印計數值（後續課程 UART 後實現）

### 挑戰 2：長按檢測

偵測按鍵是否長按（超過 2 秒）並執行不同操作

```c
void Handle_Long_Press(void)
{
  uint32_t press_time = HAL_GetTick();
  
  while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
  {
    if (HAL_GetTick() - press_time > 2000)  /* 2 秒 */
    {
      /* 長按邏輯 */
      return;
    }
  }
}
```

### 挑戰 3：多按鍵支援

擴展代碼支援多個按鍵，每個按鍵對應不同功能

---

## 🔗 延伸學習

### 下節預覽 - 第 4 節：UART 串列通訊 + 命令介面

第 4 節將實現：
- **UART 配置與初始化**
- **收發單個字元與字串**
- **建立簡單命令解析系統**

### 相關資源

- 📘 [STM32F446 GPIO and EXTI Reference](https://www.st.com/resource/en/reference_manual/dm00135183-stm32f446xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf)
- 🔗 [嵌入式系統按鍵去彈跳最佳實踐](https://www.embedded.com/)

---

**✨ GPIO 進階完成！下節深入通訊協議 - UART 🚀**