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:

麵包板接線參考

┌─────────────────┐
│     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 CoreNVIC
  4. 找到 EXTI line0 interrupt 並勾選 Enabled
  5. 設定優先級:
    • Preemption Priority: 0
    • Sub Priority: 0

步驟 3:系統時脈配置(使用第 2 節配置)

確保 SysTick 定時器已啟用(用於 HAL_Delay)

步驟 4:生成程式碼

點擊 ProjectGenerate Code


💻 完整程式碼

main.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 模式中查看按鍵電位值

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

📚 進階理解

中斷優先級機制

STM32 支援 優先級分組(Priority Grouping):

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

中斷延遲防抖技術

除了多次採樣,也可用時間判斷:

#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 秒)並執行不同操作

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 節將實現:

相關資源


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


Revision #2
Created 2026-04-01 02:06:13 UTC by TaipeiTechRacing
Updated 2026-04-06 06:23:12 UTC