方法一:延時阻塞
key.c:
#include "key.h"
#include "delay.h"//初始化GPIO
void key_init(void)
{GPIO_InitTypeDef gpio_initstruct;//打開時鐘__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA時鐘//調用GPIO初始化函數gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // 兩個KEY對應的引腳gpio_initstruct.Mode = GPIO_MODE_INPUT; // 輸入gpio_initstruct.Pull = GPIO_PULLUP; // 默認上拉,要結合實際電路,如果按下拉低接口,輸入一個低電平,那這里就是上拉輸入gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速HAL_GPIO_Init(GPIOA, &gpio_initstruct);
}//按鍵掃描函數
uint8_t key_scan(void)
{//檢測按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){//消抖delay_ms(10);//再次判斷按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){//如果確實是按下的狀態,等待按鍵松開while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);//返回按鍵值return 1;}}//檢測按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){//消抖delay_ms(10);//再次判斷按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){//如果確實是按下的狀態,等待按鍵松開while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);//返回按鍵值return 2;}}//返回默認值return 0;
}
key_scan()
里用 延時消抖(delay_ms(10))屬于 “時間消抖法”,優點是簡單,缺點是:
延時是阻塞的,CPU 在等待時不能干別的事;
多個按鍵時效率更低;
延時時間設置不好會影響響應速度。
實際上,按鍵消抖方法有很多,除了“延時消抖”,常見的還有:
🔹 1. 狀態機消抖法(非阻塞,推薦)
用一個狀態機記錄按鍵狀態(未按下、按下確認、按住、釋放確認),每次掃描時更新狀態。
掃描周期(比如 5ms 或 10ms)由 定時器中斷 或 RTOS 任務來保證。
只要穩定幾次掃描結果一致,才認為按下/松開。
優點:非阻塞,不用 delay,系統實時性好。
key.c:
#include "key.h"
#include "stm32f1xx_hal.h" // 確保包含 HAL 頭
#include <stdint.h>/* ========================= 配置區域 ========================= */
/* 掃描周期與確認次數:* - 建議每 5~10ms 調用一次 key_scan()(可在主循環里用 HAL_GetTick() 節拍控制,或用定時器/RTOS任務)* - 連續 N 次一致才確認狀態變化:N * SCAN_TICKS_MS 即為有效消抖時間*/
#define SCAN_TICKS_MS 5 // 每次掃描的理想間隔(毫秒)
#define CONFIRM_COUNT 3 // 連續 3 次一致才確認(約 15ms 消抖)/* 按鍵引腳(按你的原工程保持 A0/A1 上拉,低電平=按下) */
#define KEY0_PORT GPIOA
#define KEY0_PIN GPIO_PIN_0
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_PIN_1/* 物理讀取:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ========================= 狀態機定義 =========================* 我們用 4 態模型:* IDLE(穩定松開)* PRESS_CHECK(檢測到可能按下,進入確認期)* PRESSED(穩定按下)* RELEASE_CHECK(檢測到可能松開,進入確認期)* 時序:IDLE→PRESS_CHECK→PRESSED→RELEASE_CHECK→IDLE* 只有在 “PRESSED → RELEASE_CHECK → IDLE” 完成時,才認為一次完整按鍵發生并返回鍵值。*/
typedef enum {KEY_IDLE = 0, // 穩定松開KEY_PRESS_CHECK, // 按下確認中KEY_PRESSED, // 穩定按下KEY_RELEASE_CHECK // 松開確認中
} key_state_t;typedef struct {key_state_t state; // 當前狀態uint8_t cnt; // 連續一致計數
} key_fsm_t;/* 兩個按鍵的狀態機 */
static key_fsm_t key0 = { KEY_IDLE, 0 };
static key_fsm_t key1 = { KEY_IDLE, 0 };/* 節拍控制:保證 key_scan() 按 SCAN_TICKS_MS 的節奏運行(防止被過于頻繁調用導致“計時加快”) */
static uint32_t s_last_tick = 0;/* ========================= 用戶接口實現 ========================= *//* 初始化GPIO:與原邏輯一致(上拉輸入,低電平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 時鐘GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // 兩個 KEY 引腳gpio_initstruct.Mode = GPIO_MODE_INPUT; // 輸入模式gpio_initstruct.Pull = GPIO_PULLUP; // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW; // 輸入模式速度無關,LOW 即可HAL_GPIO_Init(GPIOA, &gpio_initstruct);// 狀態機復位key0.state = KEY_IDLE; key0.cnt = 0;key1.state = KEY_IDLE; key1.cnt = 0;s_last_tick = HAL_GetTick();
}/* 內部:推進單個鍵的狀態機;返回1表示“本次完成一次按鍵動作(按下并松開確認)”,否則0 */
static uint8_t step_fsm(key_fsm_t *k, uint8_t raw_press)
{switch (k->state){case KEY_IDLE: // 穩定松開if (raw_press) {k->state = KEY_PRESS_CHECK;k->cnt = 1;}break;case KEY_PRESS_CHECK: // 按下確認中if (raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_PRESSED; // 確認按下成立k->cnt = 0;}} else {// 搖回去k->state = KEY_IDLE;k->cnt = 0;}break;case KEY_PRESSED: // 穩定按下if (!raw_press) {k->state = KEY_RELEASE_CHECK;k->cnt = 1;}break;case KEY_RELEASE_CHECK: // 松開確認中if (!raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_IDLE; // 確認松開成立k->cnt = 0;return 1; // ——一次完整按鍵動作完成(與原版功能一致:松開后返回)}} else {// 又按下了,回到按下穩定k->state = KEY_PRESSED;k->cnt = 0;}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}return 0;
}/* 非阻塞按鍵掃描:* - 需被“周期性地”調用(推薦每 5~10ms 一次)* - 返回:0=無;1=KEY0 完成一次按鍵;2=KEY1 完成一次按鍵* - 行為與原版一致:只有“按下→松開”完整動作完成才返回鍵值*/
uint8_t key_scan(void)
{/* 簡單節拍器:若調用過快則不推進(防止消抖時間被縮短) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {return 0;}s_last_tick = now;/* 讀取原始電平(低電平=按下) */uint8_t k0_raw = key0_raw();uint8_t k1_raw = key1_raw();/* 推進兩個鍵的狀態機;誰先完成誰先返回 */if (step_fsm(&key0, k0_raw)) return 1;if (step_fsm(&key1, k1_raw)) return 2;return 0;
}
使用建議
在
while(1)
主循環里,每次循環都調用一次key_scan()
即可;內部已按SCAN_TICKS_MS
自節拍。若你在定時器中斷/RTOS里有固定 5ms 節拍,也可以去掉內部節拍器邏輯,直接每 5ms 調一次
key_scan()
(把頂部節拍相關代碼移除即可)。調參:
SCAN_TICKS_MS
:掃描周期;CONFIRM_COUNT
:確認次數;有效消抖時間約為SCAN_TICKS_MS × CONFIRM_COUNT
(默認 ≈ 15ms)。
如果還想要按下事件立即上報(而不是等松開),可以在 KEY_PRESSED
的進入處(PRESS_CHECK
確認完成時)返回一個“PRESS 事件”;并在 RELEASE_CHECK
確認完成時返回“RELEASE 事件”。
🔹 2. 定時器中斷消抖
使用定時器中斷(如 1ms)周期性采樣按鍵。
若某引腳狀態連續穩定一段時間(如 20ms),再確認按下或松開。
👉 思路類似狀態機法,但觸發源換成了硬件定時器。
key_init()
:初始化 GPIO + 配置并啟動 TIM3 的 1 ms 中斷采樣。key_scan()
:非阻塞,讀取由中斷產生的“完整按鍵事件”(按下→松開)并返回1/2/0
。
思路:在 TIM3 1 ms中斷里做采樣與消抖的狀態機(PRESS_CHECK / RELEASE_CHECK),當“按下并確認”再“松開并確認”完成時,置一個事件標志。主循環調用
key_scan()
只需讀標志即可,無 delay、無 while 等待。
key.c:
#include "key.h"
#include "stm32f1xx_hal.h"
#include <stdint.h>/* ===================== 用戶可調參數 ===================== */
/* 采樣周期:1ms(由 TIM3 產生) */
#define SAMPLE_PERIOD_MS 1/* 消抖確認時間:例如 20ms(按下與松開都采用該門限) */
#define STABLE_MS 20
#define CONFIRM_TICKS (STABLE_MS / SAMPLE_PERIOD_MS) // =20/* 兩個按鍵:PA0 / PA1,上拉輸入,低電平=按下 */
#define KEY0_PORT GPIOA
#define KEY0_PIN GPIO_PIN_0
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_PIN_1/* 物理采樣:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ===================== 定時器句柄 ===================== */
static TIM_HandleTypeDef htim3;/* ===================== 狀態機與事件 ===================== */
/* 4 態狀態機:* IDLE(穩定松開) → PRESS_CHECK(按下確認) → PRESSED(穩定按下) → RELEASE_CHECK(松開確認) → IDLE* 只有完成 PRESSED→RELEASE_CHECK→IDLE 時才記為“一次完整按鍵”。*/
typedef enum {KEY_IDLE = 0, // 穩定松開KEY_PRESS_CHECK, // 按下確認中KEY_PRESSED, // 穩定按下KEY_RELEASE_CHECK // 松開確認中
} key_state_t;typedef struct {key_state_t state; // 當前狀態uint8_t cnt; // 連續一致的計數(用于確認)
} key_fsm_t;/* 兩個鍵的狀態機實例 */
static key_fsm_t key0_fsm = { KEY_IDLE, 0 };
static key_fsm_t key1_fsm = { KEY_IDLE, 0 };/* 中斷里產生的“完整按鍵事件”標志(按下→松開完成) */
static volatile uint8_t key0_event = 0; // 置1表示有一次完整的 KEY0 事件待取
static volatile uint8_t key1_event = 0; // 置1表示有一次完整的 KEY1 事件待取/* ===================== 內部:推進一個鍵的狀態機(在中斷里調用) ===================== */
static void key_fsm_step(key_fsm_t *k, uint8_t raw_press, volatile uint8_t *event_flag)
{switch (k->state){case KEY_IDLE: /* 穩定松開 -> 看到“按下”則進入確認 */if (raw_press) {k->state = KEY_PRESS_CHECK;k->cnt = 1;}break;case KEY_PRESS_CHECK: /* 按下確認:連續 CONFIRM_TICKS 次都“按下”才成立 */if (raw_press) {if (++k->cnt >= CONFIRM_TICKS) {k->state = KEY_PRESSED; // 確認“穩定按下”k->cnt = 0;}} else {/* 中途又松開:判定失敗,回到 IDLE */k->state = KEY_IDLE;k->cnt = 0;}break;case KEY_PRESSED: /* 穩定按下 -> 觀察到“松開”則進入確認 */if (!raw_press) {k->state = KEY_RELEASE_CHECK;k->cnt = 1;}break;case KEY_RELEASE_CHECK: /* 松開確認:連續 CONFIRM_TICKS 次都“松開”才成立 */if (!raw_press) {if (++k->cnt >= CONFIRM_TICKS) {k->state = KEY_IDLE; // 確認“穩定松開”k->cnt = 0;*event_flag = 1; // ——一次完整按鍵(按下→松開)完成,置事件}} else {/* 中途又按下:回到穩定按下 */k->state = KEY_PRESSED;k->cnt = 0;}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}
}/* ===================== TIM3 初始化:1kHz(1ms)中斷 =====================* 典型 F1 時鐘:SYSCLK=72MHz, APB1=36MHz,但定時器時鐘在 APB1 分頻!=1 時倍頻到 72MHz。* 這里配置:Prescaler=7200-1 → 分頻 7200,計數頻率 10kHz;* Period =10-1 → 計滿 10 次產生更新 → 1kHz 中斷(1ms)。* 若你的時鐘不同,請按實際修改。* ====================================================================== */
static void TIM3_Init_1ms(void)
{__HAL_RCC_TIM3_CLK_ENABLE();htim3.Instance = TIM3;htim3.Init.Prescaler = 7200 - 1; // 72MHz / 7200 = 10kHzhtim3.Init.CounterMode = TIM_COUNTERMODE_UP;htim3.Init.Period = 10 - 1; // 10kHz / 10 = 1kHz → 1mshtim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;HAL_TIM_Base_Init(&htim3);HAL_TIM_Base_Start_IT(&htim3);/* 使能 NVIC 中斷 */HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0);HAL_NVIC_EnableIRQ(TIM3_IRQn);
}/* ===================== 用戶接口:GPIO + 定時器初始化 ===================== */
void key_init(void)
{/* GPIO:上拉輸入,低電平=按下(保持與原工程一致) */__HAL_RCC_GPIOA_CLK_ENABLE();GPIO_InitTypeDef io = {0};io.Pin = GPIO_PIN_0 | GPIO_PIN_1;io.Mode = GPIO_MODE_INPUT;io.Pull = GPIO_PULLUP;io.Speed = GPIO_SPEED_FREQ_LOW; // 輸入模式速度無關HAL_GPIO_Init(GPIOA, &io);/* 狀態機與事件清零 */key0_fsm.state = KEY_IDLE; key0_fsm.cnt = 0; key0_event = 0;key1_fsm.state = KEY_IDLE; key1_fsm.cnt = 0; key1_event = 0;/* 啟動 TIM3 1ms 周期中斷,進行消抖采樣與判定 */TIM3_Init_1ms();
}/* ===================== 中斷服務:1ms 采樣與消抖 ===================== */
/* HAL 的更新回調:由 TIM3_IRQHandler → HAL_TIM_IRQHandler → 觸發此回調 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM3){/* 讀取原始電平(低電平=按下) */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();/* 推進兩個鍵的狀態機;事件標志在狀態機內部置位 */key_fsm_step(&key0_fsm, r0, &key0_event);key_fsm_step(&key1_fsm, r1, &key1_event);}
}/* TIM3 IRQHandler(放在 stm32f1xx_it.c 里更規范;若沒該文件也可暫放此處) */
void TIM3_IRQHandler(void)
{HAL_TIM_IRQHandler(&htim3);
}/* ===================== 用戶接口:查詢一次事件 ===================== */
/* 行為與原版保持一致:* - 返回 1:KEY0 完成一次“按下→松開”* - 返回 2:KEY1 完成一次“按下→松開”* - 返回 0:無新事件* 非阻塞:不含 delay,不會卡主循環。*/
uint8_t key_scan(void)
{if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}
使用提示
保持原來的主循環不變,周期性調用
key_scan()
即可(現在 不會阻塞 主循環)。如果在工程里使用了 SysTick 1 ms,也可以改為在
SysTick_Handler
里調用一個Key_ISR_1ms()
來推進狀態機,思路完全一樣;這里用 TIM3 是為了不影響 SysTick。想調整消抖時間:把
STABLE_MS
改為你需要的值(例如 10/15/30 ms),1 ms 采樣會自動按CONFIRM_TICKS
計算確認門限。
🔹 3. 計數濾波法
每次掃描時對按鍵狀態計數:
檢測到按下 →
count++
檢測到松開 →
count--
當
count
超過上限(如 >3)時確認“按下”,小于下限(如 <0)時確認“松開”。
👉 優點:實現簡單,兼顧濾波和消抖。
key_init()
:初始化 GPIO(上拉輸入,低電平按下)。key_scan()
:非阻塞,建議每 5 ms 調用一次;當且僅當完成“一次按下→松開”的完整動作時返回1/2
,否則返回0
(與原代碼保持一致,但不再 delay/while 卡住主循環)。
計數濾波法要點:
每次掃描讀一次原始電平(按下=1,松開=0)。
計數器
cnt
:按下則+1
,松開則-1
,并在[0..CNT_MAX]
內飽和。當
cnt >= TH_ON
認為“穩定按下”;當cnt <= TH_OFF
認為“穩定松開”(帶回滯避免抖動來回翻轉)。只有從穩定按下轉到穩定松開時,才認定完成一次“點擊”,置事件供
key_scan()
返回(與原先“按下后等待松開再返回”的行為一致)。
key.c:
#include "key.h"
#include "stm32f1xx_hal.h"
#include <stdint.h>/* ====================== 可調參數 ====================== */
/* 建議每 5 ms 調用一次 key_scan()(主循環或定時器節拍) */
#define SCAN_TICKS_MS 5/* 計數濾波參數(積分 + 回滯):* - CNT_MAX:計數上限(越大濾波越強但響應越慢)* - TH_ON :達到/超過該值判定“穩定按下”* - TH_OFF :小于/等于該值判定“穩定松開”(回滯閾值,應小于 TH_ON)* 例如:CNT_MAX=10,TH_ON=7,TH_OFF=3 → 近似相當于 ~20~40ms 的抗抖(視抖動形態)*/
#define CNT_MAX 10
#define TH_ON 7
#define TH_OFF 3/* ====================== 硬件引腳 ====================== */
/* 與原工程保持一致:PA0 / PA1 上拉輸入,低電平=按下 */
#define KEY0_PORT GPIOA
#define KEY0_PIN GPIO_PIN_0
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_PIN_1/* 原始采樣:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ====================== 計數濾波 + 事件 ====================== */
/* 穩定狀態標記:0=穩定按下,1=穩定松開(用 1 表示松開更直觀) */
typedef struct {uint8_t cnt; // 0..CNT_MAX 的積分計數uint8_t state; // 當前穩定狀態:1=松開,0=按下
} key_counter_t;static key_counter_t key0 = { CNT_MAX, 1 }; // 初始偏向“松開”
static key_counter_t key1 = { CNT_MAX, 1 };static volatile uint8_t key0_event = 0; // 完整“按下→松開”事件
static volatile uint8_t key1_event = 0;static uint32_t s_last_tick = 0; // 內部節拍器,確保按 SCAN_TICKS_MS 推進/* 推進單鍵的計數濾波與事件判定:* - raw_press: 1=按下,0=松開* - 當從“穩定按下(0)”轉變為“穩定松開(1)”時,置 *click_flag = 1*/
static void key_counter_step(key_counter_t *k, uint8_t raw_press, volatile uint8_t *click_flag)
{/* 1) 計數積分:按下(+1) / 松開(-1),并飽和在 [0..CNT_MAX] */if (raw_press) {if (k->cnt < CNT_MAX) k->cnt++;} else {if (k->cnt > 0) k->cnt--;}/* 2) 回滯閾值判定,更新穩定狀態 */if (k->cnt >= TH_ON) {/* 穩定按下 */if (k->state != 0) {k->state = 0; // 進入“穩定按下”/* 此處可產生“PRESS 事件”(如果你需要立即上報按下) */}} else if (k->cnt <= TH_OFF) {/* 穩定松開 */if (k->state != 1) {/* 僅當從按下(0)回到松開(1)時,認為一次點擊完成 */if (k->state == 0) {*click_flag = 1; // ——一次“按下→松開”完成}k->state = 1; // 進入“穩定松開”}}
}/* ====================== 用戶接口實現 ====================== *//* 初始化GPIO:與原工程一致(上拉輸入,低電平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOAGPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // 兩個 KEYgpio_initstruct.Mode = GPIO_MODE_INPUT; // 輸入gpio_initstruct.Pull = GPIO_PULLUP; // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW; // 輸入速度無關HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 初始化計數與狀態、事件與節拍 */key0.cnt = CNT_MAX; key0.state = 1; key0_event = 0;key1.cnt = CNT_MAX; key1.state = 1; key1_event = 0;s_last_tick = HAL_GetTick();
}/* 非阻塞掃描:* - 建議每 5 ms 調用一次(內部也做了節拍限速,過快調用會直接返回 0)* - 返回:1=KEY0 完成一次“按下→松開”;2=KEY1 完成一次“按下→松開”;0=無事件* - 與原實現“按下后等待松開再返回”的功能一致,但不阻塞。*/
uint8_t key_scan(void)
{/* 簡單節拍器:確保按 SCAN_TICKS_MS 推進一次濾波(防止被高頻多次調用而縮短“有效消抖時間”) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {/* 沒到節拍,不推進濾波 */goto _check_event_and_return;}s_last_tick = now;/* 一次節拍:讀取原始電平并推進兩個鍵的計數濾波 */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();key_counter_step(&key0, r0, &key0_event);key_counter_step(&key1, r1, &key1_event);_check_event_and_return:/* 誰先完成一次點擊誰先返回;保證與原代碼返回語義一致 */if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}
說明與調參建議
非阻塞:沒有
delay_ms()
和while()
,主循環實時性更好。調用頻率:建議每 5 ms 調用一次
key_scan()
;若你已有固定節拍(比如用定時器/RTOS),也可以移除內部節拍器,固定周期調用即可。消抖強度:
增大
CNT_MAX
或TH_ON
可增強抗抖(響應更慢);減小
TH_OFF
可增加回滯,避免臨界抖動來回切換;一般保持
TH_OFF < TH_ON
,如示例TH_ON=7,TH_OFF=3
。
事件時機:當前代碼僅在從穩定按下→穩定松開時上報事件(與原行為一致)。如果你需要“按下就上報(PRESS)/松開再上報(RELEASE)”,可在
k->state
切換處各自置不同事件枚舉即可。?
🔹 4. 硬件RC電路消抖
在按鍵硬件電路上加:
電阻 + 電容(RC電路),濾掉機械抖動;
或者加 施密特觸發器(如 74HC14)來增強邊沿穩定性。
👉 優點:軟件不需要處理消抖,反應更快。缺點是需要增加硬件成本。
🔹 5. 軟件濾波算法
滑動平均:保存最近 N 次按鍵采樣結果,取多數值作為當前狀態;
數字濾波:如 IIR、FIR 濾波,減少抖動影響。
接口與語義保持不變:
key_init()
初始化;key_scan()
只有在一次“按下→松開”完整動作結束時,返回1/2
(否則返回0
),與原阻塞版行為一致;不再使用
delay_ms()
/while(...)
,建議每 5 ms 調用一次key_scan()
(也可用定時器/RTOS固定節拍調用)。
算法說明(軟件濾波)
維護每個鍵最近 N=8 次采樣的“按下位歷史(1=按下,0=松開)”
hist
;計算窗口內“按下”的個數
sum = popcount(hist)
;多數判決 + 回滯閾值:
sum >= MAJ_ON(6)
→ 判為“穩定按下”;sum <= MAJ_OFF(2)
→ 判為“穩定松開”;
只有當“穩定按下→穩定松開”完成時,置一次點擊事件,
key_scan()
返回鍵值;回滯 (
MAJ_OFF < MAJ_ON
) 可避免在臨界抖動時來回翻轉。
仍然可以把
SCAN_TICKS_MS
、WIN_BITS
、MAJ_ON
、MAJ_OFF
調一下,達到你想要的響應/抗抖平衡。
key.c:
#include "key.h"
#include "stm32f1xx_hal.h" // HAL 頭文件
#include <stdint.h>
// #include "delay.h" // 本實現不再使用延時,可刪/* ======================== 可調參數 ======================== */
/* 建議每 5 ms 調用一次 key_scan()(主循環節拍或定時器/RTOS) */
#define SCAN_TICKS_MS 5/* 滑動窗口參數(軟件濾波) */
#define WIN_BITS 8 /* 窗口長度:最近 8 次采樣 */
#define MAJ_ON 6 /* 多數判決:>=6/8 認為“穩定按下” */
#define MAJ_OFF 2 /* 多數判決:<=2/8 認為“穩定松開”(回滯,應當 < MAJ_ON) *//* ======================== 硬件引腳 ======================== */
/* 與原工程保持一致:PA0/PA1 上拉輸入,低電平=按下 */
#define KEY0_PORT GPIOA
#define KEY0_PIN GPIO_PIN_0
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_PIN_1/* 讀取原始電平:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ====================== 位歷史 + 事件 ====================== */
/* 穩定狀態語義:0=穩定按下,1=穩定松開(用 1 表示松開更直觀) */
typedef struct {uint8_t hist; // 最近 WIN_BITS 次的“按下=1/松開=0”歷史(這里是 8 位)uint8_t state; // 當前穩定狀態:1=松開,0=按下
} key_swf_t;static key_swf_t key0 = { 0x00, 1 }; // 初始歷史按 0(=松開),穩定狀態設為松開
static key_swf_t key1 = { 0x00, 1 };/* 完整“按下→松開”點擊事件(由濾波判定后置位,key_scan() 讀取后清零) */
static volatile uint8_t key0_event = 0;
static volatile uint8_t key1_event = 0;/* 內部節拍器,限制推進頻率(防止被高頻調用導致“有效消抖時間”變短) */
static uint32_t s_last_tick = 0;/* 高效 8 位 popcount(計算 hist 內 1 的個數) */
static inline uint8_t popcount8(uint8_t x)
{x = x - ((x >> 1) & 0x55);x = (x & 0x33) + ((x >> 2) & 0x33);return (uint8_t)(((x + (x >> 4)) & 0x0F));
}/* 推進一個鍵的滑動窗口濾波與事件判定* raw_press: 1=按下,0=松開* 過程:* 1) hist 左移一位,最低位寫入 raw_press(按下=1/松開=0)* 2) 統計 1 的個數 sum = popcount(hist)* 3) 多數判決 + 回滯:sum >= MAJ_ON → 穩定按下;sum <= MAJ_OFF → 穩定松開* 4) 當“穩定按下→穩定松開”發生時,置 click_flag=1(生成一次點擊事件)*/
static void key_swf_step(key_swf_t *k, uint8_t raw_press, volatile uint8_t *click_flag)
{/* 1) 移位引入當前樣本 */k->hist = (uint8_t)((k->hist << 1) | (raw_press ? 1u : 0u));/* 2) 多數判決 */uint8_t sum = popcount8(k->hist);if (sum >= MAJ_ON) {/* 判為“穩定按下” */if (k->state != 0) {k->state = 0; // 進入穩定按下// 如需“按下立即上報”可在此處產生 PRESS 事件}} else if (sum <= MAJ_OFF) {/* 判為“穩定松開” */if (k->state != 1) {/* 只有從按下(0)回到松開(1)時,認為一次點擊完成 */if (k->state == 0) {*click_flag = 1; // ——一次“按下→松開”完成}k->state = 1; // 進入穩定松開}}
}/* ======================== 用戶接口 ======================== *//* 初始化 GPIO:與原始代碼一致(上拉輸入、低電平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 時鐘GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // 兩個 KEY 引腳gpio_initstruct.Mode = GPIO_MODE_INPUT; // 輸入gpio_initstruct.Pull = GPIO_PULLUP; // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW; // 輸入模式速度無關HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 初始化濾波與事件、節拍 */key0.hist = 0x00; key0.state = 1; key0_event = 0;key1.hist = 0x00; key1.state = 1; key1_event = 0;s_last_tick = HAL_GetTick();
}/* 非阻塞掃描(軟件濾波版本)* - 建議每 5 ms 調用一次;內部也做了 5 ms 的節拍限速* - 返回:1=KEY0 完成一次“按下→松開”;2=KEY1 完成一次“按下→松開”;0=無事件* - 與原代碼“按下后等待松開再返回”的語義一致,但不再阻塞主循環*/
uint8_t key_scan(void)
{/* 節拍器:確保按固定間隔推進一次濾波(防止被過快調用而降低消抖時間) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {/* 未到節拍,僅檢查是否已有事件 */goto _check_event_and_return;}s_last_tick = now;/* 采樣 + 推進兩個鍵的濾波器 */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();key_swf_step(&key0, r0, &key0_event);key_swf_step(&key1, r1, &key1_event);_check_event_and_return:/* 誰先完成一次點擊誰先返回;保證與原代碼返回時機一致(在“松開確認”時返回) */if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}
使用與調參建議
調用頻率:保持每 5 ms 調用一次
key_scan()
。若你已有固定節拍,可以去掉內部節拍器,固定周期調用。窗口與閾值:
增大
WIN_BITS
、MAJ_ON
,或減小MAJ_OFF
→ 抗抖更強但響應更慢;減小
WIN_BITS
,或降低MAJ_ON
、提高MAJ_OFF
→ 響應更快但抗抖變弱;一般保持
MAJ_OFF < MAJ_ON
形成回滯,避免邊界抖動來回判定。
🔹 6. 操作系統事件消抖
如果在 FreeRTOS 或 RT-Thread 中:
按鍵掃描任務用固定周期(比如 10ms);
用 信號量/消息隊列通知主任務。
這樣消抖邏輯集中在一個“鍵盤驅動任務”里。
key_init()
:初始化 GPIO,并創建一個“按鍵掃描任務”,每隔 10ms 采樣,做去抖狀態機;當完成“按下→松開”一次完整點擊時,把鍵值(1 或 2)投遞到消息隊列。key_scan()
:非阻塞,從隊列里取一次事件,有就返回1/2
,沒有返回0
。不再使用
delay_ms()
和while(...)
;delay.h
可保留但未使用。
算法:狀態機 + 固定周期采樣(10ms)。當同一狀態連續 N 次(這里 N=2 ? 約 20ms)保持一致,才確認“穩定按下/穩定松開”。當從“穩定按下”過渡到“穩定松開”時,判定一次完整點擊,并將鍵值通過 FreeRTOS 隊列發給主循環。
key.c:
#include "key.h"
#include "delay.h" // 本實現已不再使用 delay,可保留以兼容舊工程
#include "stm32f1xx_hal.h"/* ===== FreeRTOS 頭文件(事件/任務/定時) ===== */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdint.h>/* ===================== 用戶可調參數 ===================== */
/* 掃描周期:10ms(按鍵任務每 10ms 運行一次) */
#define SCAN_TICKS_MS 10/* 去抖確認次數:例如 2 次(2*10ms = 20ms),按下與松開都用該門限 */
#define CONFIRM_COUNT 2/* 隊列容量:能緩存這么多個“完整點擊事件” */
#define KEY_EVENT_QUEUE_LEN 8/* 硬件引腳:與原工程保持一致(PA0, PA1 上拉輸入,低電平=按下) */
#define KEY0_PORT GPIOA
#define KEY0_PIN GPIO_PIN_0
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_PIN_1/* 物理采樣:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ===================== OS 對象 ===================== */
static QueueHandle_t s_keyQueue = NULL; /* 用于把“完整點擊事件”發給主循環 */
static TaskHandle_t s_keyTask = NULL; /* 按鍵掃描任務 *//* ===================== 去抖狀態機 ===================== */
/* 4 態:* IDLE(穩定松開) → PRESS_CHECK(按下確認) → PRESSED(穩定按下)* → RELEASE_CHECK(松開確認) → IDLE* 只有通過 PRESSED→RELEASE_CHECK→IDLE,才認為一次“按下→松開”的完整點擊。*/
typedef enum {KEY_IDLE = 0, // 穩定松開KEY_PRESS_CHECK, // 按下確認中KEY_PRESSED, // 穩定按下KEY_RELEASE_CHECK // 松開確認中
} key_state_t;typedef struct {key_state_t state; // 當前狀態uint8_t cnt; // 連續一致計數(用于確認)
} key_fsm_t;/* 兩個鍵的狀態機實例 */
static key_fsm_t key0_fsm = { KEY_IDLE, 0 };
static key_fsm_t key1_fsm = { KEY_IDLE, 0 };/* 推進單鍵狀態機:* raw_press: 1=按下, 0=松開* 完成一次“按下→松開”時,通過隊列投遞鍵值 event_val(1 或 2)*/
static void key_fsm_step(key_fsm_t *k, uint8_t raw_press, uint8_t event_val)
{switch (k->state){case KEY_IDLE: /* 穩定松開 → 看到“按下”則進入確認 */if (raw_press) { k->state = KEY_PRESS_CHECK; k->cnt = 1; }break;case KEY_PRESS_CHECK: /* 按下確認:連續 CONFIRM_COUNT 次均為按下才成立 */if (raw_press) {if (++k->cnt >= CONFIRM_COUNT) { k->state = KEY_PRESSED; k->cnt = 0; }} else {k->state = KEY_IDLE; k->cnt = 0; /* 回退 */}break;case KEY_PRESSED: /* 穩定按下 → 觀察到“松開”進入確認 */if (!raw_press) { k->state = KEY_RELEASE_CHECK; k->cnt = 1; }break;case KEY_RELEASE_CHECK: /* 松開確認:連續 CONFIRM_COUNT 次均為松開才成立 */if (!raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_IDLE; k->cnt = 0;/* ——一次完整點擊完成:發事件給主循環 —— */if (s_keyQueue) { uint8_t v = event_val; xQueueSend(s_keyQueue, &v, 0); }}} else {k->state = KEY_PRESSED; k->cnt = 0; /* 回退 */}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}
}/* ===================== 按鍵掃描任務 =====================* 周期:每 10ms 運行一次* 邏輯:讀取原始電平 → 推進兩個鍵的狀態機 → 若形成“完整點擊”,將鍵值投遞到隊列*/
static void KeyScanTask(void *arg)
{(void)arg;for (;;){/* 1) 采樣兩鍵原始電平(低電平=按下) */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();/* 2) 推進兩路狀態機;完成點擊時由內部發隊列 */key_fsm_step(&key0_fsm, r0, 1); // 鍵0 → 事件值 1key_fsm_step(&key1_fsm, r1, 2); // 鍵1 → 事件值 2/* 3) 固定周期休眠(10ms) */vTaskDelay(pdMS_TO_TICKS(SCAN_TICKS_MS));}
}/* ===================== 用戶接口:初始化 ===================== */
void key_init(void)
{/* 1) GPIO 初始化(與原工程保持一致) */__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 時鐘GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // 兩個 KEY 引腳gpio_initstruct.Mode = GPIO_MODE_INPUT; // 輸入gpio_initstruct.Pull = GPIO_PULLUP; // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW; // 輸入模式速度無關HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 2) 狀態機復位 */key0_fsm.state = KEY_IDLE; key0_fsm.cnt = 0;key1_fsm.state = KEY_IDLE; key1_fsm.cnt = 0;/* 3) 創建事件隊列(緩存多個“完整點擊”) */s_keyQueue = xQueueCreate(KEY_EVENT_QUEUE_LEN, sizeof(uint8_t));/* 4) 創建按鍵掃描任務(優先級略高于空閑任務即可) */xTaskCreate(KeyScanTask, "key_scan", 256 /*棧字(4B)*/, NULL,tskIDLE_PRIORITY + 1, &s_keyTask);/* 注意:請確保在 main() 里調用 vTaskStartScheduler() 啟動調度器 */
}/* ===================== 用戶接口:獲取一次事件 ===================== */
/* 行為保持與原代碼一致:* - 返回 1:KEY0 完成一次“按下→松開”* - 返回 2:KEY1 完成一次“按下→松開”* - 返回 0:當前無事件* 非阻塞:0 超時讀取隊列*/
uint8_t key_scan(void)
{if (!s_keyQueue) return 0;uint8_t v = 0;if (xQueueReceive(s_keyQueue, &v, 0) == pdPASS) {return v; // 1 或 2}return 0;
}
使用說明
在系統初始化流程中調用
key_init()
,隨后啟動 RTOS:vTaskStartScheduler()
。主循環或你的業務任務里,像以前一樣周期性調用
key_scan()
:返回
1/2
代表相應按鍵完成一次按下→松開;返回
0
表示暫無事件;
調參:
改
SCAN_TICKS_MS
控制掃描周期;改
CONFIRM_COUNT
控制按下/松開確認次數(SCAN_TICKS_MS * CONFIRM_COUNT
≈ 去抖時間,默認 ≈ 20ms)。
如果更想用軟件定時器而不是任務,也很容易:把 KeyScanTask
換成 xTimerCreate(..., pdMS_TO_TICKS(10), pdTRUE, ...)
的回調里做同樣的狀態機推進與 xQueueSend
即可,API 保持不變。
? 總結:
簡單項目:延時法足夠。
稍復雜的單片機項目:狀態機法 / 計數法(非阻塞)。
追求穩定 + 硬件條件允許:RC 濾波 + 軟件確認最佳。
RTOS 系統:定時掃描 + 消息隊列更優雅。