[应用相关] STM32按键实现-有长按和短按功能

[复制链接]
24|10
磨砂 发表于 2026-3-16 07:49 | 显示全部楼层 |阅读模式
STM32 4 按键实现方案(含 1 个长短按 + 3 个普通按键)
以下基于 STM32F103 系列芯片设计,采用 外部中断 + 定时器扫描 方案(兼顾低功耗与响应速度),支持:

3 个普通按键:按下时触发单次回调

1 个多功能按键:短按(按下 < 1.5s)、长按(按下≥1.5s)、长按释放触发回调

一、硬件设计 (关键配置)

5962169b3a98367767.png

说明:GPIO 配置为下拉输入,按键按下时 GPIO 电平由低变高(上升沿触发中断),避免悬空干扰。

二、软件实现(HAL 库)
1. 核心思路
外部中断
检测按键上升沿(按下)和下降沿(释放),触发中断后启动定时器扫描

定时器
10ms 中断一次,用于消抖和长按计时

状态机
记录按键当前状态(空闲 / 按下消抖 / 长按计时 / 长按保持),避免误触发

2. 代码实现(完整工程核心文件)
(1)头文件 key.h
#ifndef __KEY_H
#define __KEY_H
#include "stm32f1xx_hal.h"
// 按键GPIO定义
#define KEY1_PIN    GPIO_PIN_0
#define KEY2_PIN    GPIO_PIN_1
#define KEY3_PIN    GPIO_PIN_2
#define KEY4_PIN    GPIO_PIN_3
#define KEY_PORT    GPIOA
#define KEY_CLK_EN  __HAL_RCC_GPIOA_CLK_ENABLE()
// 按键状态枚举
typedef enum {
    KEY_IDLE = 0,        // 空闲状态
    KEY_PRESS_DEBOUNCE,  // 按下消抖
    KEY_LONG_PRESS_TIMER,// 长按计时中
    KEY_LONG_PRESS_HOLD  // 长按保持
} Key_StateTypeDef;
// 按键事件枚举
typedef enum {
    KEY_NO_EVENT = 0,    // 无事件
    KEY1_SHORT_PRESS,    // KEY1短按
    KEY1_LONG_PRESS,     // KEY1长按
    KEY1_LONG_RELEASE,   // KEY1长按释放
    KEY2_PRESS,          // KEY2按下
    KEY3_PRESS,          // KEY3按下
    KEY4_PRESS           // KEY4按下
} Key_EventTypeDef;
// 按键结构体
typedef struct {
    uint16_t        pin;            // 按键GPIO引脚
    Key_StateTypeDef state;         // 当前状态
    uint16_t        debounce_cnt;   // 消抖计数器(10ms/次)
    uint16_t        longpress_cnt;  // 长按计数器(10ms/次)
    Key_EventTypeDef event;         // 按键事件
} Key_HandleTypeDef;
// 外部函数声明
void Key_Init(void);                // 按键初始化(GPIO+中断+定时器)
Key_EventTypeDef Key_GetEvent(void); // 获取按键事件(主函数调用)
void Key_IRQHandler(uint16_t pin);  // 按键中断回调(内部使用)
void Key_TimerCallback(void);       // 定时器扫描回调(10ms一次)
#endif



(2)源文件 key.c
#include "key.h"
#include "tim.h"
// 按键对象初始化
static Key_HandleTypeDef key1 = {KEY1_PIN, KEY_IDLE, 0, 0, KEY_NO_EVENT};
static Key_HandleTypeDef key2 = {KEY2_PIN, KEY_IDLE, 0, 0, KEY_NO_EVENT};
static Key_HandleTypeDef key3 = {KEY3_PIN, KEY_IDLE, 0, 0, KEY_NO_EVENT};
static Key_HandleTypeDef key4 = {KEY4_PIN, KEY_IDLE, 0, 0, KEY_NO_EVENT};
/**
* @brief  按键GPIO+中断初始化
* @NOTE   PA0-PA3 配置为下拉输入,上升沿触发中断
*/
void Key_Init(void) {
    GPIO_InitTypeDef gpio_init = {0};
    // 使能GPIO时钟
    KEY_CLK_EN;
    // GPIO配置:下拉输入
    gpio_init.Pin = KEY1_PIN | KEY2_PIN | KEY3_PIN | KEY4_PIN;
    gpio_init.Mode = GPIO_MODE_IT_RISING_FALLING;  // 上升沿+下降沿触发(检测按下和释放)
    gpio_init.Pull = GPIO_PULLDOWN;
    gpio_init.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(KEY_PORT, &gpio_init);
    // 配置中断优先级(需根据实际工程调整)
    HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);  // KEY1(PA0)对应EXTI0
    HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 1);  // KEY2(PA1)对应EXTI1
    HAL_NVIC_SetPriority(EXTI2_IRQn, 1, 2);  // KEY3(PA2)对应EXTI2
    HAL_NVIC_SetPriority(EXTI3_IRQn, 1, 3);  // KEY4(PA3)对应EXTI3
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
    HAL_NVIC_EnableIRQ(EXTI1_IRQn);
    HAL_NVIC_EnableIRQ(EXTI2_IRQn);
    HAL_NVIC_EnableIRQ(EXTI3_IRQn);
    // 启动定时器(10ms中断,用于扫描)
    HAL_TIM_Base_Start_IT(&htim2);  // 假设使用TIM2,需在tim.h中定义htim2
}
/**
* @brief  按键中断回调函数(由HAL库调用)
* @param  GPIO_Pin: 触发中断的GPIO引脚
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    Key_IRQHandler(GPIO_Pin);
}
/**
* @brief  按键中断处理(内部函数)
* @note   检测按键电平,启动消抖或停止计时
*/
void Key_IRQHandler(uint16_t pin) {
    Key_HandleTypeDef *key = NULL;
    // 匹配按键对象
    if (pin == key1.pin) key = &key1;
    else if (pin == key2.pin) key = &key2;
    else if (pin == key3.pin) key = &key3;
    else if (pin == key4.pin) key = &key4;
    if (key == NULL) return;
    // 检测当前GPIO电平
    if (HAL_GPIO_ReadPin(KEY_PORT, pin) == GPIO_PIN_SET) {
        // 上升沿(按键按下):进入消抖状态
        key->state = KEY_PRESS_DEBOUNCE;
        key->debounce_cnt = 0;  // 消抖计数器清零(需2次10ms扫描确认=20ms消抖)
    } else {
        // 下降沿(按键释放):处理释放事件
        if (key == &key1) {  // 仅KEY1有长按释放事件
            if (key->state == KEY_LONG_PRESS_HOLD) {
                key->event = KEY1_LONG_RELEASE;  // 长按释放事件
            } else if (key->state == KEY_PRESS_DEBOUNCE) {
                key->event = KEY1_SHORT_PRESS;   // 短按事件(未达到长按时间)
            }
        } else {
            // 普通按键:释放时确认按下事件(避免长按误触发)
            key->event = (key == &key2) ? KEY2_PRESS :
                        (key == &key3) ? KEY3_PRESS : KEY4_PRESS;
        }
        key->state = KEY_IDLE;  // 恢复空闲状态
        key->longpress_cnt = 0; // 长按计数器清零
    }
}
/**
* @brief  定时器扫描回调(10ms一次)
* @note   处理消抖和长按计时
*/
void Key_TimerCallback(void) {
    // 遍历所有按键
    Key_HandleTypeDef *keys[] = {&key1, &key2, &key3, &key4};
    for (uint8_t i = 0; i < 4; i++) {
        Key_HandleTypeDef *key = keys;
        switch (key->state) {
            case KEY_PRESS_DEBOUNCE:
                // 消抖计数:2次10ms扫描(共20ms)确认按下
                if (++key->debounce_cnt >= 2) {
                    if (key == &key1) {
                        key->state = KEY_LONG_PRESS_TIMER;  // KEY1进入长按计时
                        key->longpress_cnt = 0;
                    } else {
                        // 普通按键:消抖后直接恢复空闲(释放时触发事件)
                        key->state = KEY_IDLE;
                    }
                }
                break;
            case KEY_LONG_PRESS_TIMER:
                // 长按计时:150次10ms(共1.5s)触发长按事件
                if (++key->longpress_cnt >= 150) {
                    key->event = KEY1_LONG_PRESS;
                    key->state = KEY_LONG_PRESS_HOLD;  // 进入长按保持状态
                }
                break;
            case KEY_LONG_PRESS_HOLD:
                // 长按保持:可在此添加长按重复触发逻辑(如连续加1)
                break;
            default:
                break;
        }
    }
}
/**
* @brief  获取按键事件(主函数调用)
* @retval Key_EventTypeDef: 按键事件(无事件返回KEY_NO_EVENT)
*/
Key_EventTypeDef Key_GetEvent(void) {
    Key_EventTypeDef event = KEY_NO_EVENT;
    // 优先检测KEY1事件(长短按)
    if (key1.event != KEY_NO_EVENT) {
        event = key1.event;
        key1.event = KEY_NO_EVENT;  // 清除事件标志
    } else if (key2.event != KEY_NO_EVENT) {
        event = key2.event;
        key2.event = KEY_NO_EVENT;
    } else if (key3.event != KEY_NO_EVENT) {
        event = key3.event;
        key3.event = KEY_NO_EVENT;
    } else if (key4.event != KEY_NO_EVENT) {
        event = key4.event;
        key4.event = KEY_NO_EVENT;
    }
    return event;
}



(3)定时器配置(tim.c 关键代码)
需配置一个 10ms 中断 的定时器(以 TIM2 为例):


#include "tim.h"
TIM_HandleTypeDef htim2;
/**
* @brief  TIM2初始化(10ms中断)
* @note   假设APB1时钟为36MHz(STM32F103默认)
*/
void MX_TIM2_Init(void) {
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 3600 - 1;  // 预分频:36MHz / 3600 = 10kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 100 - 1;      // 周期:10kHz / 100 = 100Hz → 10ms中断
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_Base_Init(&htim2) != HAL_OK) {
        Error_Handler();
    }
}
/**
* @brief  定时器中断回调函数
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        Key_TimerCallback();  // 调用按键扫描函数
    }
}



(4)主函数调用示例
#include "main.h"
#include "key.h"
#include "tim.h"
int main(void) {
    // 初始化HAL库
    HAL_Init();
    // 初始化系统时钟(需根据硬件配置)
    SystemClock_Config();
    // 初始化定时器
    MX_TIM2_Init();
    // 初始化按键
    Key_Init();
    while (1) {
        // 获取按键事件
        Key_EventTypeDef key_event = Key_GetEvent();
        // 处理按键事件
        switch (key_event) {
            case KEY1_SHORT_PRESS:
                // KEY1短按:执行短按逻辑(如切换LED)
                HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
                break;
            case KEY1_LONG_PRESS:
                // KEY1长按:执行长按逻辑(如开启蜂鸣器)
                HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
                break;
            case KEY1_LONG_RELEASE:
                // KEY1长按释放:执行释放逻辑(如关闭蜂鸣器)
                HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
                break;
            case KEY2_PRESS:
                // KEY2按下:执行逻辑(如加1)
                printf("KEY2 Press\r\n");
                break;
            case KEY3_PRESS:
                printf("KEY3 Press\r\n");
                break;
            case KEY4_PRESS:
                printf("KEY4 Press\r\n");
                break;
            default:
                break;
        }
        HAL_Delay(10);  // 主循环延时,降低CPU占用
    }
}



三、关键参数调整
消抖时间
KEY_PRESS_DEBOUNCE 中 debounce_cnt >= 2 → 2×10ms=20ms(可根据按键机械特性调整为 1~3 次)

长按阈值
KEY_LONG_PRESS_TIMER 中 longpress_cnt >= 150 → 150×10ms=1.5s(可调整为 100=1s、200=2s 等)

中断优先级
HAL_NVIC_SetPriority 中的优先级需避免与其他中断冲突(数值越小优先级越高)

定时器选择
若 TIM2 被占用,可替换为 TIM3/TIM4(APB1 总线上的定时器)

四、注意事项
硬件消抖
建议在按键两端并联 100nF 电容,减少机械抖动干扰

GPIO 下拉
必须确保 GPIO 配置为下拉输入,否则按键未按下时电平悬空会导致误中断

中断触发方式
采用上升沿 + 下降沿触发,既检测按下(上升沿)也检测释放(下降沿)

事件清除
Key_GetEvent 中必须清除事件标志(key->event = KEY_NO_EVENT),避免重复触发

五、扩展功能
若需普通按键支持长按,可参考 KEY1 的状态机逻辑,在对应按键的 KEY_PRESS_DEBOUNCE 后进入 KEY_LONG_PRESS_TIMER

长按重复触发:在 KEY_LONG_PRESS_HOLD 状态中添加计数器,每 N 次 10ms 触发一次重复事件(如连续增减)

低功耗优化:无按键时可暂停定时器,中断触发后再启动定时器(需修改中断回调逻辑)

此方案兼顾稳定性和灵活性,可直接移植到 STM32F1/F4/L4 等系列芯片(只需调整 GPIO 时钟、定时器编号和中断线)。
————————————————
版权声明:本文为CSDN博主「王者级废铁」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40170041/article/details/158887901

公羊子丹 发表于 2026-3-16 08:33 | 显示全部楼层
这个外部中断+定时器的方案确实稳,我之前纯用中断做长短按老出误触。实测发现如果按键排线长的话,除了并100nF电容,还能在GPIO口串个1k限流电阻,抗干扰能再提一档,亲测有效!
周半梅 发表于 2026-3-16 08:35 | 显示全部楼层
想问下楼主,这个方案里TIM2是一直10ms中断扫描吗?如果做低功耗项目的话,是不是可以在无按键时把定时器停了,中断触发再开?这样睡眠模式下的功耗能降不少,想知道具体怎么改中断回调逻辑。
帛灿灿 发表于 2026-3-16 08:36 | 显示全部楼层
我试了把这个代码移植到STM32F407上,发现GPIO时钟使能那里要改成__HAL_RCC_GPIOA_CLK_ENABLE(),定时器预分频也要按APB1的84MHz重新算,新手移植的话一定要注意时钟树,别踩这个坑!
童雨竹 发表于 2026-3-16 08:37 | 显示全部楼层
这个状态机设计得挺合理的,消抖20ms刚好适配大部分机械按键。我之前贪快设成10ms,结果便宜按键老误触发,后来调到20-30ms就完美解决了,大家可以根据自己的按键质量微调这个数值。
万图 发表于 2026-3-16 08:38 | 显示全部楼层
楼主有没有试过给KEY1加长按重复触发?比如长按的时候每隔500ms触发一次加1功能,我想在KEY_LONG_PRESS_HOLD里加个计数器,但是试了几次老出重复触发的问题,求个具体的代码思路。
Wordsworth 发表于 2026-3-16 08:39 | 显示全部楼层
提醒下大家,配置中断优先级的时候,别把按键中断设得比系统时钟、串口这些核心中断还高,不然会导致串口接收丢数据、定时器不准,亲测踩过这个坑,优先级设成1-3档就够了。
Bblythe 发表于 2026-3-16 08:41 | 显示全部楼层
我用这个方案做了个小设备,发现按键释放的时候偶尔会漏触发,排查了半天发现是GPIO下拉电阻没配对,STM32的下拉默认是弱下拉,要是电源纹波大,建议外部再并个10k下拉电阻,稳很多。
Pulitzer 发表于 2026-3-16 08:42 | 显示全部楼层
哈哈,终于找到个能直接用的长短按代码了,之前自己写的状态机逻辑乱糟糟的,长按释放老和短按冲突。这个代码我直接复制过去,改了下GPIO口就用了,唯一要注意的是tim.h里要先定义htim2,不然会报错。
Uriah 发表于 2026-3-16 08:43 | 显示全部楼层
想请教下,这个方案里普通按键是释放时触发事件,要是想改成按下就触发,是不是要把Key_IRQHandler里普通按键的事件赋值移到上升沿那里?改了之后会不会有消抖的问题,需要额外处理吗?
Clyde011 发表于 2026-3-16 08:44 | 显示全部楼层
实测这个代码在STM32L051上也能用,就是低功耗模式下要注意EXTI中断的唤醒配置,得把GPIO的唤醒功能打开,不然休眠后按键按了没反应。另外定时器换成TIM6会更省资源,因为TIM6是基本定时器,占用外设少。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

191

主题

4590

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部
0