STM32 4 按键实现方案(含 1 个长短按 + 3 个普通按键)
以下基于 STM32F103 系列芯片设计,采用 外部中断 + 定时器扫描 方案(兼顾低功耗与响应速度),支持:
3 个普通按键:按下时触发单次回调
1 个多功能按键:短按(按下 < 1.5s)、长按(按下≥1.5s)、长按释放触发回调
一、硬件设计 (关键配置)
说明: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
|
|