STM32 教學系列 第 3 節:GPIO 進階(按鍵、中斷、去彈跳)
STM32 教學系列 第 3 節:GPIO 進階(按鍵、中斷、去彈跳)
🎯 學習目標
- 掌握 GPIO 輸入配置 - 實現按鍵檢測功能
- 理解外部中斷系統(EXTI) - 學會事件驅動編程模式
- 實現按鍵去彈跳算法 - 透過軟體解決機械按鍵的抖動問題
🎓 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 輸入配置
- 開啟 CubeMX,在 Pinout 圖上找到 PA0
- 右鍵點擊 PA0,選擇 GPIO_Input
- 在左側 System Core 中選擇 GPIO
- 展開 GPIOA 並確認 PA0 的設定:
- GPIO mode: Input
- Pull: Pull-up
- Speed: Medium
- User Label: KEY_Input
步驟 2:配置外部中斷(EXTI)
- 返回 Pinout 圖,右鍵點擊 PA0
- 選擇 GPIO_EXTI0(改為中斷模式)
- 在左側選擇 System Core → NVIC
- 找到 EXTI line0 interrupt 並勾選 Enabled
- 設定優先級:
- Preemption Priority: 0
- Sub Priority: 0
步驟 3:系統時脈配置(使用第 2 節配置)
確保 SysTick 定時器已啟用(用於 HAL_Delay)
步驟 4:生成程式碼
點擊 Project → Generate 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);
}
🔍 測試與除錯
預期結果
✅ 功能驗證:
⚠️ 無抖動現象: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):
-
Preemption Priority(搶佔優先級):數值小優先級高
- 優先級 0 > 優先級 1 > 優先級 2 ...
- 高優先級中斷可中斷低優先級中斷執行
-
Sub Priority(子優先級):同搶佔優先級下的排隊順序
/* 設定最高優先級 */
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 節將實現:
- UART 配置與初始化
- 收發單個字元與字串
- 建立簡單命令解析系統
相關資源
✨ GPIO 進階完成!下節深入通訊協議 - UART 🚀