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

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

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

学习交流请发邮件 [email protected]

代码实现:

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 )
			{
				
			}
		}
	}
}

实验结果:

按键检测:软件消抖+轮询检测+单双击及长按_第1张图片

按键检测:软件消抖+轮询检测+单双击及长按_第2张图片

睡了

你可能感兴趣的:(STM32,外设学习+项目实践,单片机,stm32,嵌入式硬件)