基于 STM32 单片机的多路按键管理方案
STM32 多路按键管理实现文档(中断 + 定时器 + 队列)
文档概述
本文档详细介绍基于 STM32 单片机的多路按键管理方案,采用外部中断 + 定时器 + 状态机 + 事件队列的设计思路,实现 4 路按键的非阻塞式检测,支持短按、长按、双击、三次连续短按功能,并通过队列统一管理按键事件,保证系统实时性和事件处理的灵活性。
一、功能特性
-
支持 4 路独立按键,每路按键状态独立管理;
-
非阻塞式消抖(定时器驱动,无中断内延时);
-
支持按键事件:短按、长按、双击、三次连续短按;
-
事件队列机制:按键事件异步入队,主循环非阻塞读取处理;
-
中断轻量化:外部中断仅检测电平变化,核心逻辑在定时器中断中执行。
二、实现原理
2.1 硬件设计
-
按键采用上拉输入模式:未按下时为高电平,按下时为低电平;
-
示例 4 路按键引脚配置:KEY1 (PA0)、KEY2 (PA1)、KEY3 (PA2)、KEY4 (PA3);
-
可根据实际硬件修改 GPIO 端口 / 引脚配置。
2.2 软件架构
| 模块 | 功能说明 |
|---|---|
| 外部中断模块 | 检测按键电平变化(上升沿 / 下降沿),仅清除中断标志,不做任何业务处理; |
| 定时器模块 | 1ms 定时中断,驱动所有按键的状态机(消抖、计时、事件判断); |
| 状态机模块 | 每路按键独立状态机:空闲→检测按下→已按下→检测释放→等待多击,实现消抖和多击判断; |
| 事件队列模块 | 循环队列缓存按键事件,保证事件不丢失,主循环非阻塞读取处理; |
2.3 核心逻辑
-
消抖机制:定时器 1ms 中断中累计消抖计数,达到消抖时间(默认 20ms)确认电平稳定;
-
长按判断:按键按下后累计时间,达到长按阈值(默认 1000ms)触发长按事件;
-
多击判断:短按释放后进入等待多击状态,在间隔时间(默认 500ms)内检测到新短按则计数,超时后根据计数触发单击 / 双击 / 三击事件;
-
队列管理:事件入队 / 出队操作加中断保护,保证原子性,避免队列操作异常。
三、完整代码实现
3.1 头文件(key.h)
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
// 按键ID枚举(支持4路按键)
typedef enum {
KEY1 = 0,
KEY2 = 1,
KEY3 = 2,
KEY4 = 3,
KEY_MAX_NUM = 4 // 按键总数
} KeyID;
// 按键事件枚举
typedef enum {
KEY_NO_EVENT = 0, // 无事件
KEY_SHORT_PRESS = 1, // 短按事件
KEY_LONG_PRESS = 2, // 长按事件
KEY_DOUBLE_CLICK = 3, // 双击事件
KEY_TRIPLE_CLICK = 4 // 三次连续短按事件
} KeyEvent;
// 按键状态枚举
typedef enum {
KEY_IDLE = 0, // 按键空闲
KEY_PRESS_DETECT = 1, // 检测到按下(消抖中)
KEY_PRESSED = 2, // 按键已按下
KEY_RELEASE_DETECT = 3,// 检测到释放(消抖中)
KEY_WAIT_MULTI = 4 // 等待多击(双击/三击)
} KeyState;
// 按键GPIO配置结构体
typedef struct {
GPIO_TypeDef* port; // GPIO端口
uint16_t pin; // GPIO引脚
uint32_t rcc; // 时钟使能
uint8_t extiLine; // EXTI线路
IRQn_Type irqChannel; // 中断通道
} KeyGPIOConfig;
// 按键结构体
typedef struct {
KeyState state; // 当前状态
KeyEvent event; // 按键事件
uint8_t level; // 当前电平
uint8_t stableLevel; // 稳定电平
uint16_t debounceCnt; // 消抖计数器
uint32_t pressTime; // 按下时间计数
uint8_t clickCount; // 短按次数计数
uint32_t waitMultiCnt; // 等待多击计数器
KeyID id; // 按键ID
} KeyTypeDef;
// 按键队列元素
typedef struct {
KeyID id; // 按键ID
KeyEvent event; // 事件类型
} KeyQueueElement;
// 按键配置参数(可根据需求修改)
#define DEBOUNCE_TIME 20 // 消抖时间(ms)
#define LONG_PRESS_TIME 1000 // 长按判定时间(ms)
#define MULTI_CLICK_INTERVAL 500 // 多击间隔时间(ms)
#define KEY_QUEUE_SIZE 32 // 事件队列大小
// 函数声明
void KEY_Init(void); // 按键初始化
uint8_t KEY_GetEvent(KeyQueueElement* event); // 获取按键事件
void Key_StateMachineHandler(KeyTypeDef* key); // 按键状态机处理
#endif /* __KEY_H */
3.2 源文件(key.c)
#include "key.h"
// 全局变量
static KeyTypeDef keys[KEY_MAX_NUM]; // 多路按键状态
static KeyGPIOConfig keyConfig[KEY_MAX_NUM] = { // 4路按键GPIO配置
{GPIOA, GPIO_Pin_0, RCC_APB2Periph_GPIOA, EXTI_Line0, EXTI0_IRQn}, // KEY1
{GPIOA, GPIO_Pin_1, RCC_APB2Periph_GPIOA, EXTI_Line1, EXTI1_IRQn}, // KEY2
{GPIOA, GPIO_Pin_2, RCC_APB2Periph_GPIOA, EXTI_Line2, EXTI2_IRQn}, // KEY3
{GPIOA, GPIO_Pin_3, RCC_APB2Periph_GPIOA, EXTI_Line3, EXTI3_IRQn} // KEY4
};
// 事件队列
static KeyQueueElement keyQueue[KEY_QUEUE_SIZE];
static uint8_t queueHead = 0;
static uint8_t queueTail = 0;
// 定时器配置 (1ms中断一次)
static void TIM_Configuration(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 72MHz时钟,预分频71,计数999 → 1ms中断
TIM_TimeBaseStructure.TIM_Period = 999;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 定时器中断配置
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
}
// GPIO和外部中断配置
static void KEY_GPIO_EXTI_Config(void) {
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
uint8_t i;
for (i = 0; i < KEY_MAX_NUM; i++) {
// 使能时钟
RCC_APB2PeriphClockCmd(keyConfig[i].rcc | RCC_APB2Periph_AFIO, ENABLE);
// 配置GPIO为上拉输入
GPIO_InitStructure.GPIO_Pin = keyConfig[i].pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(keyConfig[i].port, &GPIO_InitStructure);
// 配置EXTI线路
GPIO_EXTILineConfig(GPIO_GetPortSourceGPIOx(keyConfig[i].port),
GPIO_GetPinSource(keyConfig[i].pin));
EXTI_InitStructure.EXTI_Line = keyConfig[i].extiLine;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 双边沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 配置EXTI中断
NVIC_InitStructure.NVIC_IRQChannel = keyConfig[i].irqChannel;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = i; // 子优先级区分按键
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 初始化按键状态
keys[i].state = KEY_IDLE;
keys[i].event = KEY_NO_EVENT;
keys[i].level = 1;
keys[i].stableLevel = 1;
keys[i].debounceCnt = 0;
keys[i].pressTime = 0;
keys[i].clickCount = 0;
keys[i].waitMultiCnt = 0;
keys[i].id = (KeyID)i;
}
}
// 队列入队(带中断保护)
static uint8_t KeyQueue_Enqueue(KeyQueueElement element) {
uint8_t nextTail = (queueTail + 1) % KEY_QUEUE_SIZE;
uint8_t ret = 0;
__disable_irq(); // 关闭全局中断,保证原子操作
if (nextTail != queueHead) { // 队列未满
keyQueue[queueTail] = element;
queueTail = nextTail;
ret = 1;
}
__enable_irq(); // 开启全局中断
return ret;
}
// 触发多击事件并入队
static void TriggerMultiClickEvent(KeyTypeDef* key) {
KeyQueueElement elem;
elem.id = key->id;
switch(key->clickCount) {
case 1: elem.event = KEY_SHORT_PRESS; break;
case 2: elem.event = KEY_DOUBLE_CLICK; break;
case 3: elem.event = KEY_TRIPLE_CLICK; break;
default: elem.event = KEY_TRIPLE_CLICK; break;
}
KeyQueue_Enqueue(elem);
// 重置计数
key->clickCount = 0;
}
// 按键状态机处理(单路按键)
void Key_StateMachineHandler(KeyTypeDef* key) {
// 读取当前电平(按下为低电平,释放为高电平)
key->level = GPIO_ReadInputDataBit(keyConfig[key->id].port, keyConfig[key->id].pin);
switch(key->state) {
case KEY_IDLE:
// 检测到按键电平变化
if(key->level != key->stableLevel) {
key->state = KEY_PRESS_DETECT;
key->debounceCnt = 0;
}
break;
case KEY_PRESS_DETECT:
// 消抖计数
key->debounceCnt++;
if(key->debounceCnt >= DEBOUNCE_TIME) {
// 确认按键按下
if(key->level == 0) {
key->state = KEY_PRESSED;
key->stableLevel = 0;
key->pressTime = 0; // 开始计时
} else {
// 干扰信号,回到空闲状态
key->state = KEY_IDLE;
}
}
break;
case KEY_PRESSED:
// 按键按下时间累加
key->pressTime++;
// 检测到按键释放
if(key->level != key->stableLevel) {
key->state = KEY_RELEASE_DETECT;
key->debounceCnt = 0;
}
// 检测长按事件
else if(key->pressTime == LONG_PRESS_TIME) {
KeyQueueElement elem = {key->id, KEY_LONG_PRESS};
KeyQueue_Enqueue(elem);
// 长按触发后重置多击计数
key->clickCount = 0;
}
break;
case KEY_RELEASE_DETECT:
// 消抖计数
key->debounceCnt++;
if(key->debounceCnt >= DEBOUNCE_TIME) {
// 确认按键释放
if(key->level == 1) {
key->state = KEY_WAIT_MULTI; // 进入等待多击状态
key->stableLevel = 1;
key->waitMultiCnt = 0;
// 短按计数(未触发长按)
if(key->pressTime < LONG_PRESS_TIME) {
key->clickCount++;
} else {
// 长按后的释放,不计数
key->clickCount = 0;
key->state = KEY_IDLE;
}
} else {
// 干扰信号,回到按下状态
key->state = KEY_PRESSED;
}
}
break;
case KEY_WAIT_MULTI:
// 等待多击的时间计数
key->waitMultiCnt++;
// 检测到新的按键按下
if(key->level != key->stableLevel) {
key->state = KEY_PRESS_DETECT;
key->debounceCnt = 0;
}
// 超过多击间隔时间,触发多击事件
else if(key->waitMultiCnt >= MULTI_CLICK_INTERVAL) {
TriggerMultiClickEvent(key);
key->state = KEY_IDLE;
}
break;
default:
key->state = KEY_IDLE;
break;
}
}
// 按键初始化
void KEY_Init(void) {
// 初始化GPIO和EXTI
KEY_GPIO_EXTI_Config();
// 初始化定时器
TIM_Configuration();
// 初始化队列
queueHead = 0;
queueTail = 0;
}
// 获取按键事件(非阻塞)
uint8_t KEY_GetEvent(KeyQueueElement* event) {
uint8_t ret = 0;
__disable_irq();
if (queueHead != queueTail) { // 队列非空
*event = keyQueue[queueHead];
queueHead = (queueHead + 1) % KEY_QUEUE_SIZE;
ret = 1;
}
__enable_irq();
return ret;
}
// 外部中断服务函数(KEY1-EXTI0)
void EXTI0_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
// 外部中断服务函数(KEY2-EXTI1)
void EXTI1_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line1) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
// 外部中断服务函数(KEY3-EXTI2)
void EXTI2_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line2) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line2);
}
}
// 外部中断服务函数(KEY4-EXTI3)
void EXTI3_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line3) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line3);
}
}
// 定时器2中断服务函数(核心处理)
void TIM2_IRQHandler(void) {
uint8_t i;
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
// 遍历处理所有按键的状态机
for (i = 0; i < KEY_MAX_NUM; i++) {
Key_StateMachineHandler(&keys[i]);
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
3.3 主函数示例(main.c)
#include "stm32f10x.h"
#include "key.h"
int main(void) {
KeyQueueElement event;
// 系统初始化
SystemInit();
// 按键初始化
KEY_Init();
while (1) {
// 从队列获取按键事件
if (KEY_GetEvent(&event)) {
// 根据按键ID和事件类型处理
switch (event.id) {
case KEY1:
if (event.event == KEY_SHORT_PRESS) {
// KEY1短按处理逻辑
} else if (event.event == KEY_LONG_PRESS) {
// KEY1长按处理逻辑
} else if (event.event == KEY_DOUBLE_CLICK) {
// KEY1双击处理逻辑
} else if (event.event == KEY_TRIPLE_CLICK) {
// KEY1三击处理逻辑
}
break;
case KEY2:
// KEY2事件处理
break;
case KEY3:
// KEY3事件处理
break;
case KEY4:
// KEY4事件处理
break;
default:
break;
}
}
// 其他业务逻辑
// ...
}
}
四、使用指南
4.1 初始化
调用KEY_Init()完成所有按键的 GPIO、外部中断、定时器和队列初始化,建议在系统启动时执行。
4.2 事件获取与处理
在主循环中调用KEY_GetEvent(&event)获取按键事件:
-
返回值为 1:成功获取事件,
event.id为按键 ID,event.event为事件类型; -
返回值为 0:无事件,直接执行其他业务逻辑;
-
事件处理逻辑建议简洁,避免阻塞主循环。
4.3 参数配置
可在key.h中修改以下参数适配实际需求:
| 参数名 | 说明 | 默认值 |
|---|---|---|
| DEBOUNCE_TIME | 消抖时间(ms) | 20 |
| LONG_PRESS_TIME | 长按判定时间(ms) | 1000 |
| MULTI_CLICK_INTERVAL | 多击间隔时间(ms) | 500 |
| KEY_QUEUE_SIZE | 事件队列大小 | 32 |
4.4 硬件适配
修改key.c中keyConfig数组的 GPIO 配置,适配实际硬件接线:
static KeyGPIOConfig keyConfig[KEY_MAX_NUM] = {
{GPIOB, GPIO_Pin_0, RCC_APB2Periph_GPIOB, EXTI_Line0, EXTI0_IRQn}, // KEY1改为PB0
// 其他按键配置...
};
五、扩展说明
5.1 增加按键数量
-
扩展
KeyID枚举,增加新按键 ID; -
扩展
keyConfig数组,添加新按键的 GPIO / 中断配置; -
修改
KEY_MAX_NUM为新的按键总数。
5.2 自定义每路按键参数
在KeyTypeDef中增加独立参数(如每路按键的长按时间),在状态机中使用对应参数替代全局宏定义。
5.3 队列优化
-
增加队列溢出处理:如覆盖最旧事件、记录溢出次数;
-
增加队列状态查询函数:获取队列当前长度、剩余空间。
5.4 事件优先级
可在入队时根据事件类型设置优先级,或在出队时优先处理高优先级事件(如长按优先于多击)。
六、注意事项
-
定时器中断优先级需低于外部中断,保证电平变化能及时检测;
-
队列操作必须加中断保护,避免多线程(中断 / 主循环)操作异常;
-
多击间隔时间不宜过短(建议 300-800ms),否则用户操作难度大;
-
消抖时间建议 10-30ms,过短易受干扰,过长影响响应速度。

浙公网安备 33010602011771号