【STM32】定时器中断实现按键检测及消抖(标准库)

发布于:2025-03-15 ⋅ 阅读:(12) ⋅ 点赞:(0)

之前也写过一篇利用定时器实现按键检测及消抖的文章,但是那篇代码量太大,而且不便于移植,还有一堆不明变量,简直就是屎山代码,这次使用结构体数组优化一下,原理不变

一、基本原理

这是一个按键从按下到松开的过程

传统用while死循环实现按键的松手检测,但是这样会使程序卡死,浪费CPU资源,在延时函数里只进行+1+1+1+1的操作,对程序运行的实时性很不友好

我们使用定时器每5ms扫描一次按键,当检测到按键被按下时,我们的按键计数器(LongPressCount)就开始计数,每5ms增加一次,当检测到按键被松开时停止计数,接着通过判断LongPressCount的值来判断按键按下的状态(没按下、短按和长按),当LongPressCount的值在0-4之间时就意味着按键在0-20ms之间就松开了,就判定为没有按键按下,计数器继续计数,当LongPressCount的值在4-200之间,意味着按键在20-1000ms之间松开,判定为短按,将按键的状态设置为短按并清除LongPressCount,那长按就是LongPressCount大于200了。

现在假如我们的按键没有抖动,我们要实现按键按下一次a++但是不许使用delay和while,最简单的逻辑是这样的(伪代码)

while (1)
{
    if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
    {
	    a++;
    }
}

但是问题也随之而来,当我按键按下的时候a会一直++,所以我们要定义一个变量Key_flag,默认为0,当按键按下后flag变为1,不再执行a++,逻辑是这样的

while (1)
{
	if(Key_flag == 0)
	{
		if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
		{
			a++;
			Key_flag = 1;
		}			
	}
}

这样的话第一次按下正常,第二次按下就没反应,因为按键松开的时候还要恢复标志位,当标志位还是被置位(flag==1)时,如果按键是松开的就清除标志位

while (1)
{
	if(Key_flag == 0)
	{
		if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
		{
			a++;
			Key_flag = 1;
		}			
	}
	if(Key_flag == 1)
	{
		if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 1)
		{
			Key_flag = 0;
		}			
	}
}

以上就是一个不使用while死循环实现松手检测的例子,记住这种思想,下面要用

二、程序设计

1、定时器中断

void Timer_Init(uint16_t Psc, uint16_t Per)
{
	/*开启时钟*/	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
	
	TIM_InternalClockConfig(TIM6);
	
	/*时基单元初始化*/	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = Per - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = Psc - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStructure);
	
	TIM_ClearFlag(TIM6, TIM_FLAG_Update);
	TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
	
	/*中断配置*/	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_Cmd(TIM6, ENABLE);
}

Timer_Init(72, 1000);                                //先配置一个1ms的定时器中断

void TIM6_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM6, TIM_IT_Update) == SET)
	{
		static uint16_t count = 0;
		count++;
/****************************************5ms***************************************/		
		if(count % 5 == 0)
		{	
		}
/*****************************10ms任务********************************/				
		if(count % 10 == 0)
		{		
			count = 0;
		}
		TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
	}
}

 利用static 关键字,实现对定时器中断的充分利用

2、枚举类型

枚举是一种用户定义的类型,表示一组具有名称的整数常量。通过枚举,可以使代码更具可读性和可维护性。关键字 enum 用于定义枚举类型。

枚举特点:
  1. 枚举值是整数:每个枚举值默认从 0 开始递增,但可以显式指定值。
  2. 提高代码可读性:通过使用有意义的名称代替数字,代码更易理解。
  3. 作用范围受限:通常用于描述固定范围的选项,例如按键状态、按键事件等。
    typedef enum {
        KEY_DOWN,    // 默认值 0
        KEY_UP       // 默认值 1
    } KeyStatus;
    
    typedef enum {
        NULL_PRESS,  // 默认值 0
        SHORT_PRESS, // 默认值 1
        LONG_PRESS   // 默认值 2
    } KeyEvent;
    
  4. KEY_DOWNKEY_UPKeyStatus 枚举中的两个值。
  5. NULL_PRESSSHORT_PRESSLONG_PRESSKeyEvent 枚举中的三个值。
  6. 使用枚举后,代码可以直接使用这些名称,而无需记忆具体的数值,所以我使用枚举类型替代了大量标志位,增强代码可读性

3、typedef 是什么?

typedef 是 C 语言中的一个关键字,用于给现有类型定义新的类型别名。

特点:
  1. 简化代码:可以为复杂的类型取一个简短的别名。
  2. 增强可读性:通过使用有意义的名称,代码逻辑更加清晰。
  3. 方便维护:如果底层类型需要更改,只需修改别名定义即可,无需逐一修改代码。
    typedef enum {
        KEY_DOWN,
        KEY_UP
    } KeyStatus;
    
  4. typedef 为枚举类型创建了一个别名 KeyStatus
  5. 定义后,KeyStatus 就可以直接作为一种数据类型使用:

KeyStatus key = KEY_DOWN; // 声明变量 key,类型为 KeyStatus

对比没有 typedef 的写法:

enum KeyStatus {
    KEY_DOWN,
    KEY_UP
};

enum KeyStatus key = KEY_DOWN;

不使用 typedef 时,每次声明变量都需要加上 enum 关键字,代码显得繁琐

4、结构体

// 定义按键配置结构体
typedef struct {
    KeyStatus status;
    KeyEvent event;
    uint16_t LongPressCount;
} KeyHandler;

extern KeyHandler Key[4];

我使用了4个按键,所以使用extern KeyHandler Key[4];定义了一个长度为4的一维数组,下面都以Key[0]为例,这样定义都,Key[0]就有了四个成员,分别为        Key[0].event         Key[0].status         Key[0].LongPressCount 

5、按键检测

if(count % 5 == 0)
{				
	for(uint8_t i = 0; i < 4; i++)
	{
		if(GPIO_ReadInputDataBit(Key_Prot[i], Key_Pin[i]) == 0)
		{
			Key[i].status = KEY_DOWN;
			Key[i].LongPressCount++;
		}
		else
		{
			Key[i].status = KEY_UP;
		}
	}
	Key_handler();                      //调用按键状态机

这是5ms的任务,这里是四个按键,所以可以先把最外层的for循环去掉,判断按键的引脚是否被拉低,如果被拉低,Key[0].status按键的状态变为按下,同时按键计数器LongPressCount开始计数,每5ms计数一次,当检测到按键的引脚不为0时就将按键状态设置为抬起,调用按键处理函数检测

//-------------------------------------------------------------------------------------------------------------------
// 函数简介     按键处理函数
// 参数说明     void
// 返回参数     void
// 使用示例     Key_handler(); // 扫描按键状态并判断是否为短按或长按
// 备注信息     根据按键状态和按下持续时间,判断按键事件为短按或长按
//-------------------------------------------------------------------------------------------------------------------
void Key_handler(void)
{
    for(uint8_t i = 0; i < 4; i++)
    {
        if(Key[i].status == KEY_UP)
        {
            if(Key[i].LongPressCount <= 4)
            {
                Key[i].event = NULL_PRESS;
            }
            if(Key[i].LongPressCount > 4 && Key[i].LongPressCount <200)
            {
                Key[i].event = SHORT_PRESS;
                Key[i].LongPressCount = 0;
            }
            if(Key[i].LongPressCount >= 200)
            {
                Key[i].event = LONG_PRESS;
                Key[i].LongPressCount = 0;
            }
        }
    }
}

按键是松开的状态才会进行判断,在判断时如果按键计数器的值在0-4,则按键的事件为没按下,不对按键计数器进行清零操作,按键计数器继续计数

6、按键任务

			if(Key[0].event == SHORT_PRESS)	
			{
				a++;
			}	
			/**************************按键KEY1长按任务************************************/
			if(Key[0].event == LONG_PRESS)	
			{
				a += 10;
			}
			/**************************按键KEY2短按任务************************************/
			if(Key[1].event == SHORT_PRESS)	
			{
				a--;
			}
			/**************************按键KEY2长按任务************************************/
			if(Key[1].event == LONG_PRESS)	
			{
				a -= 10;
			}
			/**************************按键KEY3短按任务************************************/
			if(Key[2].event == SHORT_PRESS)	
			{
				a += 5;
			}	
			/**************************按键KEY3长按任务************************************/
			if(Key[2].event == LONG_PRESS)	
			{
				a -= 5;
			}	
			/**************************按键KEY4短按任务************************************/
			if(Key[3].event == SHORT_PRESS)	
			{
				a *= 2;
			}
			/**************************按键KEY4长按任务************************************/
			if(Key[3].event == LONG_PRESS)	
			{
				a /= 2;
			}	

判断按键的事件来进行相应的任务,那有人就问我,松手的时候也有抖动,这怎么就不需要消抖呢?其实一步一步看程序,当执行按键处理函数时按键计数器会被清零,如果有抖动,会开始新一轮的计数,计数值过小就无法执行按键的动作

三、完整源码

1、Key.h

#ifndef __KEY_H__
#define __KEY_H__

// 定义按键状态枚举
typedef enum {
    KEY_DOWN,
    KEY_UP,
} KeyStatus;

// 定义按键事件枚举
typedef enum {
    NULL_PRESS,
    SHORT_PRESS,
    LONG_PRESS
} KeyEvent;

// 定义按键配置结构体
typedef struct {
    KeyStatus status;
    KeyEvent event;
    uint16_t LongPressCount;
} KeyHandler;

extern KeyHandler Key[4];
extern uint16_t KEY[4];
extern GPIO_TypeDef* Key_Prot[4];
extern uint16_t Key_Pin[4];

void Key_Init(void);
void Key_handler(void);

#endif 

2、Key.c

#include "stm32f10x.h"                  // Device header
#include "Key.h"

//KEY1----->PC14	KEY2----->PC15		KEY3----->PA5		KEY4----->PC4
KeyHandler Key[4];

GPIO_TypeDef* Key_Prot[4] = {GPIOC, GPIOC, GPIOA, GPIOC};
uint16_t Key_Pin[4] = {GPIO_Pin_14, GPIO_Pin_15, GPIO_Pin_5, GPIO_Pin_4};
//-------------------------------------------------------------------------------------------------------------------
// 函数简介     按键初始化函数
// 参数说明     void
// 返回参数     void
// 使用示例     Key_Init(); 
// 备注信息     设置所有按键为上拉输入,并初始化按键事件、状态和长按计数器
//-------------------------------------------------------------------------------------------------------------------
void Key_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);		
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15 | GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStructure);	

	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);	
	
    for(uint8_t i; i < 4; i++)
    {
        Key[i].event = NULL_PRESS;
        Key[i].status = KEY_UP;
        Key[i].LongPressCount = 0;
    }

}

//-------------------------------------------------------------------------------------------------------------------
// 函数简介     按键处理函数
// 参数说明     void
// 返回参数     void
// 使用示例     Key_handler(); // 扫描按键状态并判断是否为短按或长按
// 备注信息     根据按键状态和按下持续时间,判断按键事件为短按或长按
//-------------------------------------------------------------------------------------------------------------------
void Key_handler(void)
{
    for(uint8_t i = 0; i < 4; i++)
    {
        if(Key[i].status == KEY_UP)
        {
            if(Key[i].LongPressCount <= 4)
            {
                Key[i].event = NULL_PRESS;
            }
            if(Key[i].LongPressCount > 4 && Key[i].LongPressCount <200)
            {
                Key[i].event = SHORT_PRESS;
                Key[i].LongPressCount = 0;
            }
            if(Key[i].LongPressCount >= 200)
            {
                Key[i].event = LONG_PRESS;
                Key[i].LongPressCount = 0;
            }
        }
    }
}

3、main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
#include "stdio.h"
#include "Key.h"
#include "Timer.h"

uint16_t a, Key_flag;
int main(void)
{	
	char text[20];
	
	LED_Init();
	OLED_Init();
	Timer_Init(72, 1000);
	Key_Init();
	while (1)
	{
		if(Key_flag == 0)
		{
			if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
			{
				a++;
				Key_flag = 1;
			}			
		}
		if(Key_flag == 1)
		{
			if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 1)
			{
				Key_flag = 0;
			}			
		}
		sprintf(text,"%d", a);		
		OLED_ShowString(1,1,text);
		OLED_Refresh();
	}
}

void TIM6_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM6, TIM_IT_Update) == SET)
	{
		static uint16_t count = 0;
		count++;
/*********************************************5ms任务*******************************************************/		
		if(count % 5 == 0)
		{				
			for(uint8_t i = 0; i < 4; i++)
			{
				if(GPIO_ReadInputDataBit(Key_Prot[i], Key_Pin[i]) == 0)
				{
					Key[i].status = KEY_DOWN;
					Key[i].LongPressCount++;
				}
				else
				{
					Key[i].status = KEY_UP;
				}
			}
			Key_handler();                      //调用按键状态机
			/**************************按键KEY1短按任务************************************/
			if(Key[0].event == SHORT_PRESS)	
			{
				a++;
			}	
			/**************************按键KEY1长按任务************************************/
			if(Key[0].event == LONG_PRESS)	
			{
				a += 10;
			}
			/**************************按键KEY2短按任务************************************/
			if(Key[1].event == SHORT_PRESS)	
			{
				a--;
			}
			/**************************按键KEY2长按任务************************************/
			if(Key[1].event == LONG_PRESS)	
			{
				a -= 10;
			}
			/**************************按键KEY3短按任务************************************/
			if(Key[2].event == SHORT_PRESS)	
			{
				a += 5;
			}	
			/**************************按键KEY3长按任务************************************/
			if(Key[2].event == LONG_PRESS)	
			{
				a -= 5;
			}	
			/**************************按键KEY4短按任务************************************/
			if(Key[3].event == SHORT_PRESS)	
			{
				a *= 2;
			}
			/**************************按键KEY4长按任务************************************/
			if(Key[3].event == LONG_PRESS)	
			{
				a /= 2;
			}				
		}
/*********************************************10ms任务*******************************************************/				
		if(count % 10 == 0)
		{		
			count = 0;
		}
		TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
	}
}