按键检测:软件消抖+轮询检测+单双击及长按

2023.10.12更新:

按键轮询在中断时触发,只扫描按键电平,不做回调,不然如果回调函数运行时间太久的话,会占用其他任务的运行时间,所以回调函数存消息队列,然后主循环空闲时再做相应的任务处理

单片机操作系统,按键与FIFO_TianYaKe-天涯客的博客-CSDN博客


2023.8.10更新:

原理:

按键检测原理:
这里假设按下时为低电平,平时为高电平
1.每隔1ms进入一次按键扫描
2.当按键还没按下时,每隔1ms读取一次电平,直到检测到低电平
3.当第一次检测到低电平,开始按键周期倒数(keyx->checkInterval),一般设置500ms为一个按键周期
4.第一次按下后,定时读取(keyx->timedReadInterval)按下的电平状态,若为低电平,则定时读取低电平的次数(keyx->timedReadRes)加1
5.每次按下时,当设定的读取进入状态(keyx->readKeyLevel)为低电平时,每次检测到低电平,进行软件消抖,10ms后再读取,若为低电平,则按下次数+1(keyx->keyDownTimes),
6.第5步完成后,同时设置读取进入状态(keyx->readKeyLevel)为高电平,重复第5个步骤,直到按键周期结束
7.按键周期结束,定时读取低电平的次数(keyx->timedReadRes)大于某个值,则认为是长按,否则根据按下次数计数(keyx->keyDownTimes)来确定是短按还是双击

 key.h

#ifndef __KEY_H
#define __KEY_H	 

#include "includes.h"

typedef enum {
	keyNone = 0,
	keyShort,
	keyDouble,
	keyTriple,
	keyLong
} keyState;

typedef struct key_s {
	u8 keyNum;
	bool bIsChecking;		//正在检测
	bool bTaskProcessing;
	bool readKeyLevel;
	keyState 	preState;	//上一个按键状态
	keyState 	state;	
	u8	timedReadInterval;	//定时读取的间隔
	u8	keyDownTimes;		//一个检测周期按下的次数
	u16	timedReadRes;		//定时读取,如果在一个检测周期按下的次数为1或2,而每隔n ms读取的低电平次数大于某个值,可认为是长按
	u16 shakeInterval;		//抖动多少时间后进行检测
	u16 checkInterval;		//整个检测的持续时间
	u16 keyLongInterval;	//长按后隔一段时间再检测,避免检测到短按
	void (*pfnKeyCallBack)(void);
} KEY_S;

extern KEY_S key1;
extern KEY_S key2;
extern KEY_S key3;
extern KEY_S key4;
extern KEY_S key5;
extern KEY_S key6;
extern KEY_S key7;
extern KEY_S key8;
extern KEY_S key9;
extern KEY_S key10;

#define keyLongTimes		40
#define timedReadIntervalMs	10	
#define shakeIntervalMs		10
#define checkIntervalMs		500

#define		KEY_ON      0	//按键平时为高电平,按下为低电平
#define		KEY_OFF     1

#define KEY1_GPIO_PIN           GPIO_Pins_4
#define KEY1_GPIO_PORT          GPIOA
#define KEY1_GPIO_CLK           RCC_APB2PERIPH_GPIOA

#define KEY2_GPIO_PIN           GPIO_Pins_6
#define KEY2_GPIO_PORT          GPIOA
#define KEY2_GPIO_CLK           RCC_APB2PERIPH_GPIOA

#define KEY3_GPIO_PIN           GPIO_Pins_0
#define KEY3_GPIO_PORT          GPIOB
#define KEY3_GPIO_CLK           RCC_APB2PERIPH_GPIOB

#define KEY4_GPIO_PIN           GPIO_Pins_2
#define KEY4_GPIO_PORT          GPIOB
#define KEY4_GPIO_CLK           RCC_APB2PERIPH_GPIOB

#define KEY5_GPIO_PIN           GPIO_Pins_11
#define KEY5_GPIO_PORT          GPIOB
#define KEY5_GPIO_CLK           RCC_APB2PERIPH_GPIOB

#define KEY6_GPIO_PIN           GPIO_Pins_5
#define KEY6_GPIO_PORT          GPIOA
#define KEY6_GPIO_CLK           RCC_APB2PERIPH_GPIOA

#define KEY7_GPIO_PIN           GPIO_Pins_7
#define KEY7_GPIO_PORT          GPIOA
#define KEY7_GPIO_CLK           RCC_APB2PERIPH_GPIOA

#define KEY8_GPIO_PIN           GPIO_Pins_1
#define KEY8_GPIO_PORT          GPIOB
#define KEY8_GPIO_CLK           RCC_APB2PERIPH_GPIOB

#define KEY9_GPIO_PIN           GPIO_Pins_10
#define KEY9_GPIO_PORT          GPIOB
#define KEY9_GPIO_CLK           RCC_APB2PERIPH_GPIOB

#define KEY10_GPIO_PIN           GPIO_Pins_12
#define KEY10_GPIO_PORT          GPIOB
#define KEY10_GPIO_CLK           RCC_APB2PERIPH_GPIOB

void KeyOsInit(void);
//void KeyLoopTask(void);
void key1TaskProc(void);
void key2TaskProc(void);
void key3TaskProc(void);
void key4TaskProc(void);
void key5TaskProc(void);
void key6TaskProc(void);
void key7TaskProc(void);
void key8TaskProc(void);
void key9TaskProc(void);
void key10TaskProc(void);

#endif

key.c 

#include "includes.h"

KEY_S key1;
KEY_S key2;
KEY_S key3;
KEY_S key4;
KEY_S key5;
KEY_S key6;
KEY_S key7;
KEY_S key8;
KEY_S key9;
KEY_S key10;

//按键平时为高电平,按下为低电平
static void Key_GPIO_Config(void)
{
	GPIO_InitType  GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK, ENABLE);	
	GPIO_InitStruct.GPIO_Pins = KEY1_GPIO_PIN;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;		
	GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct);	
	
	GPIO_InitStruct.GPIO_Pins = KEY2_GPIO_PIN;
	GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY3_GPIO_PIN;
	GPIO_Init(KEY3_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY4_GPIO_PIN;
	GPIO_Init(KEY4_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY5_GPIO_PIN;
	GPIO_Init(KEY5_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY6_GPIO_PIN;
	GPIO_Init(KEY6_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY7_GPIO_PIN;
	GPIO_Init(KEY7_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY8_GPIO_PIN;
	GPIO_Init(KEY8_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY9_GPIO_PIN;
	GPIO_Init(KEY9_GPIO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pins = KEY10_GPIO_PIN;
	GPIO_Init(KEY10_GPIO_PORT, &GPIO_InitStruct);
}

void KeyOsInit(void)
{
	Key_GPIO_Config();
	
	key1.keyNum = 1;
	key1.bIsChecking = 0;
	key1.state = keyNone;
	key1.checkInterval = checkIntervalMs;
	key1.keyDownTimes = 0;
	key1.pfnKeyCallBack = key1TaskProc;
	
	key2.keyNum = 2;
	key2.bIsChecking = 0;
	key2.state = keyNone;
	key2.checkInterval = checkIntervalMs;
	key2.keyDownTimes = 0;
	key2.pfnKeyCallBack = key2TaskProc;
	
	key3.keyNum = 3;
	key3.bIsChecking = 0;
	key3.state = keyNone;
	key3.checkInterval = checkIntervalMs;
	key3.keyDownTimes = 0;
	key3.pfnKeyCallBack = key3TaskProc;
	
	key4.keyNum = 4;
	key4.bIsChecking = 0;
	key4.state = keyNone;
	key4.checkInterval = checkIntervalMs;
	key4.keyDownTimes = 0;
	key4.pfnKeyCallBack = key4TaskProc;
	
	key5.keyNum = 5;
	key5.bIsChecking = 0;
	key5.state = keyNone;
	key5.checkInterval = checkIntervalMs;
	key5.keyDownTimes = 0;
	key5.pfnKeyCallBack = key5TaskProc;
	
	key6.keyNum = 6;
	key6.bIsChecking = 0;
	key6.state = keyNone;
	key6.checkInterval = checkIntervalMs;
	key6.keyDownTimes = 0;
	key6.pfnKeyCallBack = key6TaskProc;
	
	key7.keyNum = 7;
	key7.bIsChecking = 0;
	key7.state = keyNone;
	key7.checkInterval = checkIntervalMs;
	key7.keyDownTimes = 0;
	key7.pfnKeyCallBack = key7TaskProc;
	
	key8.keyNum = 8;
	key8.bIsChecking = 0;
	key8.state = keyNone;
	key8.checkInterval = checkIntervalMs;
	key8.keyDownTimes = 0;
	key8.pfnKeyCallBack = key8TaskProc;
	
	key9.keyNum = 9;
	key9.bIsChecking = 0;
	key9.state = keyNone;
	key9.checkInterval = checkIntervalMs;
	key9.keyDownTimes = 0;
	key9.pfnKeyCallBack = key9TaskProc;
	
	key10.keyNum = 10;
	key10.bIsChecking = 0;
	key10.state = keyNone;
	key10.checkInterval = checkIntervalMs;
	key10.keyDownTimes = 0;
	key10.pfnKeyCallBack = key10TaskProc;
}


static bool readKeyGpioLevel(u8 keyNum)
{
	bool keyRes = KEY_OFF;
	switch ( keyNum ){
		case 1:
			keyRes = GPIO_ReadInputDataBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN);
			break;
		case 2:
			keyRes = GPIO_ReadInputDataBit(KEY2_GPIO_PORT, KEY2_GPIO_PIN);
			break;
		case 3:
			keyRes = GPIO_ReadInputDataBit(KEY3_GPIO_PORT, KEY3_GPIO_PIN);
			break;
		case 4:
			keyRes = GPIO_ReadInputDataBit(KEY4_GPIO_PORT, KEY4_GPIO_PIN);
			break;
		case 5:
			keyRes = GPIO_ReadInputDataBit(KEY5_GPIO_PORT, KEY5_GPIO_PIN);
			break;
		case 6:
			keyRes = GPIO_ReadInputDataBit(KEY6_GPIO_PORT, KEY6_GPIO_PIN);
			break;
		case 7:
			keyRes = GPIO_ReadInputDataBit(KEY7_GPIO_PORT, KEY7_GPIO_PIN);
			break;
		case 8:
			keyRes = GPIO_ReadInputDataBit(KEY8_GPIO_PORT, KEY8_GPIO_PIN);
			break;
		case 9:
			keyRes = GPIO_ReadInputDataBit(KEY9_GPIO_PORT, KEY9_GPIO_PIN);
			break;
		case 10:
			keyRes = GPIO_ReadInputDataBit(KEY10_GPIO_PORT, KEY10_GPIO_PIN);
			break;
		default:
			break;
	}
	return keyRes;
}

static void keyScanLoop(KEY_S *keyx)
{
	if ( keyx->state == keyLong ){
		if (KEY_ON == readKeyGpioLevel(keyx->keyNum)){
			return;
		} else {
			keyx->preState = keyx->state;
			keyx->state = keyNone;
			
		}			
	}
	
	//	不在检测状态时按键按下
	if ( 0 == keyx->bIsChecking ){	
		if (KEY_ON == readKeyGpioLevel(keyx->keyNum)){
			keyx->readKeyLevel = KEY_ON;
			keyx->bIsChecking = 1;
			keyx->keyDownTimes = 0;		
			keyx->preState = keyx->state;
			keyx->state = keyNone;
			keyx->timedReadRes = 0;
			keyx->checkInterval = checkIntervalMs;
			keyx->shakeInterval = shakeIntervalMs;
		}
	} else {
		//	按键周期倒计时
		if ( keyx->checkInterval ){
			keyx->checkInterval--;
			
			//	定时读取
			if ( keyx->timedReadInterval ){
				keyx->timedReadInterval--;
			} else {
				keyx->timedReadInterval = timedReadIntervalMs;
				if( KEY_ON == readKeyGpioLevel(keyx->keyNum) ){
					keyx->timedReadRes++;
				}
			}
	
			//	检测状态时按键按下
			if ( KEY_ON == keyx->readKeyLevel ) {				
				//	按键软件消抖
				if ( keyx->shakeInterval ){
					keyx->shakeInterval--;
				} else {
					keyx->shakeInterval = shakeIntervalMs;
					if (KEY_ON == readKeyGpioLevel(keyx->keyNum)){
						keyx->keyDownTimes++;
						//	读取的电平反转
						keyx->readKeyLevel = KEY_OFF;
					}
				} 				
			} else {
				if (KEY_OFF == readKeyGpioLevel(keyx->keyNum)){
					keyx->readKeyLevel = KEY_ON;
				}
			}
		} 
		//	按键倒计时结束,通过按下次数和定时读取次数判断按键键值
		else {
			keyx->bIsChecking = 0;			
			switch (keyx->keyDownTimes){
				case keyNone:
					keyx->state = keyNone;
					return;
				case keyShort:
					if ( keyLong == keyx->preState ) {
						keyx->state = keyNone;
						keyx->keyLongInterval = checkIntervalMs;
						return;
					} else {
						keyx->state = keyShort;
					}
					break;
				case keyDouble:
				case keyTriple:	//按下三次也算双击
					keyx->state = keyDouble;
					break;
				default :
					keyx->state = keyLong;
					break;
			}
			if ( keyx->timedReadRes > keyLongTimes ){	//可自定义读取次数
				keyx->state = keyLong;	
			}
			keyx->pfnKeyCallBack();
		}
	}
}

//定时器设置为1ms进一次中断
void TMR4_GLOBAL_IRQHandler(void)
{
	if ( TMR_GetINTStatus( TMR4, TMR_INT_Overflow) != RESET ) 
	{
		keyScanLoop(&key1);
		keyScanLoop(&key2);
		keyScanLoop(&key3);
		keyScanLoop(&key4);
		keyScanLoop(&key5);
		keyScanLoop(&key6);
		keyScanLoop(&key7);
		keyScanLoop(&key8);
		keyScanLoop(&key9);
		keyScanLoop(&key10);
		TMR_ClearITPendingBit(TMR4 , TMR_INT_Overflow);  		 
	}	
}

void key1TaskProc(void)
{	
	//do something
}

前言:

在网上看到有用按键的软件消抖,但是基本上用的是delay函数,占用了CPU太多资源了,无法实现实时调用;高级一点的用定时器+外部中断的方式,但是无法实现单击、双击、长按功能。

所以这里开发了一种功能,不占用太多CPU资源的同时实现轮询检测,且使用指针结构体,多个按键的情况下可复用性强、移植性强。

本人使用的单片机芯片型号是STM32f103VET6

有纰漏请指出,转载请说明。

学习交流请发邮件 1280253714@qq.com

代码实现:

key.h

#ifndef __KEY_H
#define __KEY_H	 

#include "includes.h"

typedef enum {
	keyNone = 0,
	keyShort,
	keyDouble,
	keyTriple,
	keyLong
} keyState;

typedef struct key_s {
	u8 keyNum;
	bool keyDownFlag;
	bool bIsChecking;		//正在检测
	bool bCanReadGpio;		//防止消抖后一直读取电平
	keyState 	preState;			//上一个按键状态
	keyState 	state;
	u8	timedReadInterval;	//定时读取的间隔
	u8	keyDownTimes;		//一个检测周期按下的次数
	u16	timedReadRes;		//定时读取,如果在一个检测周期按下的次数为1或2,而每隔n ms读取的低电平次数大于某个值,可认为是长按
	u16 shakeInterval;		//抖动多少时间后进行检测
	u16 checkInterval;		//整个检测的持续时间
} KEY_S;

extern KEY_S key1;
extern KEY_S key2;

#define keyLongTimes		80
#define timedReadIntervalMs	10	
#define shakeIntervalMs		10
#define checkIntervalMs		1000

#define		KEY_ON      1	//按键平时为低电平,按下为高电平
#define		KEY_OFF     0


#define KEY1_GPIO_PIN           GPIO_Pin_0
#define KEY1_GPIO_PORT          GPIOA
#define KEY1_GPIO_CLK           RCC_APB2Periph_GPIOA

void EXIT_Key_Config(void);

#endif

key.c

#include "includes.h"

KEY_S key1;
KEY_S key2;

static void KeyOsInit(void)
{
	key1.keyNum = 1;
	key1.bIsChecking = 0;
	key1.state = keyNone;
	key1.checkInterval = checkIntervalMs;
	key1.keyDownTimes = 0;
	
	key2.keyNum = 2;
	key2.bIsChecking = 0;
	key2.state = keyNone;
	key2.checkInterval = checkIntervalMs;
	key2.keyDownTimes = 0;
}

static void EXTI_NVIC_Config(void)
{
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);	
	NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);
}

void EXIT_Key_Config(void)
{
	GPIO_InitTypeDef  GPIO_InitStruct;
	EXTI_InitTypeDef  EXTI_InitStruct;
	
	KeyOsInit();
	EXTI_NVIC_Config();
	
	RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK, ENABLE);	
	GPIO_InitStruct.GPIO_Pin = KEY1_GPIO_PIN;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;		
	GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct);	
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
	
	EXTI_InitStruct.EXTI_Line = EXTI_Line0;
	EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_InitStruct.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStruct);	
}

static void keyScan(KEY_S *keyx){
	if ( 0 == keyx->bIsChecking )
	{
		keyx->bIsChecking = 1;
		keyx->keyDownTimes = 0;
		keyx->preState = keyx->state;
		keyx->state = keyNone;
		keyx->timedReadRes = 0;
		keyx->checkInterval = checkIntervalMs;
		keyx->bCanReadGpio = 1;
		keyx->shakeInterval = shakeIntervalMs;
	} else {
		keyx->shakeInterval = shakeIntervalMs;
		keyx->bCanReadGpio = 1;
	}
}

static void keyLoopTask(KEY_S *keyx){
	
	if ( 0 == keyx->bIsChecking )
	{
		return;
	}

	if ( keyx->checkInterval ){
		keyx->checkInterval--;
		if ( keyx->shakeInterval ){
			keyx->shakeInterval--;
		} else {		
		if ( 1 == keyx->bCanReadGpio ){
			if( GPIO_ReadInputDataBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON ){
				keyx->keyDownTimes++;
			}
				keyx->bCanReadGpio = 0;
			}
		}
		if ( keyx->timedReadInterval ){
			keyx->timedReadInterval--;
		} else {
			keyx->timedReadInterval = timedReadIntervalMs;
			if( GPIO_ReadInputDataBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON ){
				keyx->timedReadRes++;
			}
		}
	} else {
		keyx->bIsChecking = 0;			
		if ( keyx->timedReadRes > keyLongTimes ){	//可自定义读取次数
			keyx->state = keyLong;
			keyx->keyDownFlag = 1;
			return;		
		}
		switch (keyx->keyDownTimes){
			case keyNone:
				keyx->state = keyNone;
				return;
			case keyShort:
				keyx->state = keyShort;
				break;
			case keyDouble:
			case keyTriple:	//按下三次也算双击
				keyx->state = keyDouble;
				break;
			default :
				keyx->state = keyLong;
				break;
		}
		keyx->keyDownFlag = 1;		
	}
}

void EXTI0_IRQHandler(void)
{
	if(EXTI_GetITStatus(EXTI_Line0) != RESET)
	{
		keyScan(&key1);
	}
	EXTI_ClearITPendingBit(EXTI_Line0);
}

//定时器设置为1ms进一次中断
void TIM3_IRQHandler()
{
	if ( TIM_GetITStatus( TIM3, TIM_IT_Update) != RESET ) 
	{		
		keyLoopTask(&key1);
		TIM_ClearITPendingBit(TIM3 , TIM_FLAG_Update);  		 
	}	
}

main.c

#include "includes.h"

int main(void)
{ 
	TimerOsInit();
	Delay_init();
	EXIT_Key_Config();
	
	while (1){
		if( 1 == key1.keyDownFlag ){
			key1.keyDownFlag = 0;
			if ( keyShort == key1.state )
			{
				
			}
		}
	}
}

实验结果:

睡了

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32是一款高性能、低功耗的微控制器,可以通过编程实现击、双击和长按功能。在使用STM32进行轮询时,可以通过以下方式实现这些功能。 首先,我们需要了解STM32的GPIO输入模式和中断模式。GPIO输入模式可以读取引脚的电平变化,而中断模式可以在引脚状态发生变化时立即触发一段程序的执行。 为了实现击功能,我们可以在轮询中不断读取引脚的电平变化,并使用一个计数器记录按下的持续时间。当电平变为低电平时,计数器开始计时;当电平变为高电平时,计数器停止计时。当计数器达到一定的阈值时,我们可以认为发生了击事件。 双击功能的实现方式类似击,不同之处在于我们需要维护两个计数器,一个用于检测第一次击事件,另一个用于检测第二次击事件。当检测到第一次击事件后,我们可以启动第二个计数器,并在一定的时间内检测第二次击事件是否发生。 长按功能可以通过轮询的方式判断按键状态的持续时间是否超过设定的阈值。如果按键状态持续时间超过阈值,则可以认为发生了长按事件。 总结起来,STM32可以通过读取引脚的状态变化来实现击、双击和长按功能。使用计数器来记录按键状态的持续时间,并与设定的阈值进行比较,从而判断是否发生了相应的事件。这种轮询的方式可以有效地实现按键功能,并能满足实际应用需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值