基于 STM32 单片机的多路按键管理方案

STM32 多路按键管理实现文档(中断 + 定时器 + 队列)

文档概述

本文档详细介绍基于 STM32 单片机的多路按键管理方案,采用外部中断 + 定时器 + 状态机 + 事件队列的设计思路,实现 4 路按键的非阻塞式检测,支持短按、长按、双击、三次连续短按功能,并通过队列统一管理按键事件,保证系统实时性和事件处理的灵活性。

一、功能特性

  1. 支持 4 路独立按键,每路按键状态独立管理;

  2. 非阻塞式消抖(定时器驱动,无中断内延时);

  3. 支持按键事件:短按、长按、双击、三次连续短按;

  4. 事件队列机制:按键事件异步入队,主循环非阻塞读取处理;

  5. 中断轻量化:外部中断仅检测电平变化,核心逻辑在定时器中断中执行。

二、实现原理

2.1 硬件设计

  • 按键采用上拉输入模式:未按下时为高电平,按下时为低电平;

  • 示例 4 路按键引脚配置:KEY1 (PA0)、KEY2 (PA1)、KEY3 (PA2)、KEY4 (PA3);

  • 可根据实际硬件修改 GPIO 端口 / 引脚配置。

2.2 软件架构

模块 功能说明
外部中断模块 检测按键电平变化(上升沿 / 下降沿),仅清除中断标志,不做任何业务处理;
定时器模块 1ms 定时中断,驱动所有按键的状态机(消抖、计时、事件判断);
状态机模块 每路按键独立状态机:空闲→检测按下→已按下→检测释放→等待多击,实现消抖和多击判断;
事件队列模块 循环队列缓存按键事件,保证事件不丢失,主循环非阻塞读取处理;

2.3 核心逻辑

  1. 消抖机制:定时器 1ms 中断中累计消抖计数,达到消抖时间(默认 20ms)确认电平稳定;

  2. 长按判断:按键按下后累计时间,达到长按阈值(默认 1000ms)触发长按事件;

  3. 多击判断:短按释放后进入等待多击状态,在间隔时间(默认 500ms)内检测到新短按则计数,超时后根据计数触发单击 / 双击 / 三击事件;

  4. 队列管理:事件入队 / 出队操作加中断保护,保证原子性,避免队列操作异常。

三、完整代码实现

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 增加按键数量

  1. 扩展KeyID枚举,增加新按键 ID;

  2. 扩展keyConfig数组,添加新按键的 GPIO / 中断配置;

  3. 修改KEY_MAX_NUM为新的按键总数。

5.2 自定义每路按键参数

在KeyTypeDef中增加独立参数(如每路按键的长按时间),在状态机中使用对应参数替代全局宏定义。

5.3 队列优化

  • 增加队列溢出处理:如覆盖最旧事件、记录溢出次数;

  • 增加队列状态查询函数:获取队列当前长度、剩余空间。

5.4 事件优先级

可在入队时根据事件类型设置优先级,或在出队时优先处理高优先级事件(如长按优先于多击)。

六、注意事项

  1. 定时器中断优先级需低于外部中断,保证电平变化能及时检测;

  2. 队列操作必须加中断保护,避免多线程(中断 / 主循环)操作异常;

  3. 多击间隔时间不宜过短(建议 300-800ms),否则用户操作难度大;

  4. 消抖时间建议 10-30ms,过短易受干扰,过长影响响应速度。

posted @ 2025-12-04 11:10  Afangdong  阅读(225)  评论(0)    收藏  举报