爱收集资源网

按键检测终极目标:在操作系统中实现按下、长按、松开、多击等功能

爱收集资源网 2024-08-17 16:14

假如有更好的解决方案或是发觉天神的方案有问题,欢迎你们热烈讨论!

明晰键盘的使用环境和终极目标使用环境

首先我们的键盘使用在有操作系统的环境中,不能使用占用CPU的延时函数,使用操作系统的延时每20ms对键盘进行一次检查。

补充:经过天神的思索,发觉在操作系统中进行循环和使用定时器中断疗效虽然是一样的,完全可以将键盘测量函数放进一个定时20ms的定时器之中,疗效是一样的。(操作系统本质上也是使用了systick进行定时的)

终极目标

我们的键盘须要实现的终极目标是测量键盘

按下、长按、松开、多击(双击三击等等)。

键盘的程序讯号、逻辑状态、物理状态、开启计数、关闭计数、多击标志、多击计数

按照我们的环境和目标,天神总结下来我们的键盘须要有1+4个信息来记录键盘的状态,也就是标题中的程序讯号、逻辑状态、物理状态、开启计数、关闭计数。其中程序讯号写成讯号是由于这种讯号不须要储存,通过逻辑状态、物理状态的关系直接返回。对此天神画了一个图进行剖析:

以下进行详尽介绍(不含长松状态):

程序讯号

程序讯号有多种,包括:关掉、开启、长按、等待、双击、三击、四击等等

其中等待状态是为了当键盘早已按下或是握住后程序不再重复响应而设置

逻辑状态

逻辑状态有三种:关掉、开启、长按

没有等待状态

化学状态

化学状态是单片机IO口接收的状态,是有晃动的,有两种:关掉、开启

(逻辑状态,化学状态)以后类似(0,0)这样的方法来表示

开启计数与关掉计数

将这两个计数分开是为了在开启和关掉时都作出消抖,但是开启计数可以拿来作为对长按的延后计数。当状态为(0,1)或则(1,0)时计数,出现异响即发生(0,0)或(1,1)状态时清零。

据悉还有两个状态所对应几种实际情况,根据图中从左到右(红灰绿蓝灰哪里)次序总结一下分别为

关掉情况开启晃动开启计数累加情况开启兼长按检查情况长按情况关掉晃动关掉计数累加情况

首先要明晰,程序最后接收到的讯号,与键盘的逻辑状态是有区别的。如图中所示,键盘的逻辑状态只有三种,关掉、开启、长按,并没有等待状态。这样是为了便捷进行剖析。程序最后接收到的讯号与键盘的逻辑状态关系是:

1.在逻辑状态的上升沿,程序接收到开启讯号

2.在逻辑状态的增长沿,程序接收到关掉讯号

3.在逻辑状态为长按量,程序也接收到长按讯号

4.其余时间程序都接收到等待讯号

其次看图的左下角部份,分别反映的是逻辑状态、物理状态所对应的实际情况。须要注意的是,二者为(1,1)时的状态,在键盘的关掉晃动过程中也有出现,二者为(0,0)的状态,在键盘的开启晃动过程中也有出现。因而须要在这两个状态中分别对关掉计数和开启计数清零。同时,二者为(1,1)的状态正是要检查键盘是不是步入长按的时刻,因而要对开启计数进行累加,抵达给定值后切换到(2,1)状态。

最后是右上角对于一个完整的按钮开启、长按、关闭过程的具体剖析,用不同颜色代表了各个实际情况,应当一目了然,就不多做解释了。

具体代码(在stm32上实现)

按照以上剖析,天神得出推论,对于每一个键盘都须要储存它的逻辑状态、物理状态、开启计数、关闭计数,最后反馈给程序的讯号是由这种状态估算而至。注意是每一个按钮,也就是说假如你有10个按钮,就须要存10组。因此定义一个结构体:

//按键状态结构体,存储四个变量
typedef struct
{
 	uint8_t KeyLogic;
	uint8_t KeyPhysic;
 	uint8_t KeyONCounts;
 	uint8_t KeyOFFCounts;
}KEY_TypeDef;

一些宏定义,假如你的开关时按下低电平,握住高电平就把KEY_OFF,KEY_ON对调一下就ok了。

//宏定义
#define    	KEY_OFF	   		0
#define    	KEY_ON	   	 	1
#define    	KEY_HOLD		2
#define		KEY_IDLE		3
#define		KEY_ERROR		10
#define		HOLD_COUNTS			50
#define 	SHAKES_COUNTS		5

创建一个结构体字段,拿来对应每一个实际按钮,我这儿有两个。

//按键结构体数组,初始状态都是关闭
static KEY_TypeDef Key[2] =
	{{KEY_OFF, KEY_OFF, 0, 0},
	 {KEY_OFF, KEY_OFF, 0, 0}};

接下里是关键的key_scan()函数,这个函数要在操作系统的任务中循环执行,因而其中不能有阻塞延时。

/*
 * 函数名:Key_Scan
 * 描述  :检测是否有按键按下
 * 输入  :GPIOx:gpio的port
 *		   GPIO_Pin:gpio的pin
 * 输出  :KEY_OFF、KEY_ON、KEY_HOLD、KEY_IDLE、KEY_ERROR
 */
 
uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
	KEY_TypeDef *KeyTemp;
	//检查按下的是哪一个按钮
	switch ((uint32_t)GPIOx)
	{
	case ((uint32_t)KEY1_GPIO_PORT):
		switch (GPIO_Pin)
		{
		case KEY1_GPIO_PIN:
			KeyTemp = &Key[0];
			break;
		//port和pin不匹配
		default:
			printf("error: GPIO port pin not match\r\n");
			return KEY_IDLE;
		}
		break;
	case ((uint32_t)KEY2_GPIO_PORT):
		switch (GPIO_Pin)
		{
		case KEY2_GPIO_PIN:
			KeyTemp = &Key[1];
			break;
		//port和pin不匹配
		default:
			printf("error: GPIO port pin not match\r\n");
			return KEY_IDLE;
		}
		break;
	default:
		printf("error: key do not exist\r\n");
		return KEY_IDLE;
	}
	/* 检测按下、松开、长按 */
	KeyTemp->KeyPhysic = GPIO_ReadInputDataBit(GPIOx, GPIO_Pin);
	switch (KeyTemp->KeyLogic)
	{
	
	case KEY_ON:
		switch (KeyTemp->KeyPhysic)
		{
		
		//(1,1)中将关闭计数清零,并对开启计数累加直到切换至逻辑长按状态
		case KEY_ON:
			KeyTemp->KeyOFFCounts = 0;
			KeyTemp->KeyONCounts++;
			if (KeyTemp->KeyONCounts >= HOLD_COUNTS)
			{
				KeyTemp->KeyONCounts = 0;
				KeyTemp->KeyLogic = KEY_HOLD;
				return KEY_HOLD;
			}
			return KEY_IDLE;
			
		//(1,0)中对关闭计数累加直到切换至逻辑关闭状态
		case KEY_OFF:
			KeyTemp->KeyOFFCounts++;
			if (KeyTemp->KeyOFFCounts >= SHAKES_COUNTS)
			{
				KeyTemp->KeyLogic = KEY_OFF;
				KeyTemp->KeyOFFCounts = 0;
				return KEY_OFF;
			}
			return KEY_IDLE;
		default:
			break;
		}
	case KEY_OFF:
		switch (KeyTemp->KeyPhysic)
		{
		
		//(0,1)中对开启计数累加直到切换至逻辑开启状态
		case KEY_ON:
			(KeyTemp->KeyONCounts)++;
			if (KeyTemp->KeyONCounts >= SHAKES_COUNTS)
			{
				KeyTemp->KeyLogic = KEY_ON;
				KeyTemp->KeyONCounts = 0;
				return KEY_ON;
			}
			return KEY_IDLE;
			
		//(0,0)中将开启计数清零
		case KEY_OFF:
			(KeyTemp->KeyONCounts) = 0;
			return KEY_IDLE;
		default:
			break;
		}
	case KEY_HOLD:
		switch (KeyTemp->KeyPhysic)
		{
		
		//(2,1)对关闭计数清零
		case KEY_ON:
			KeyTemp->KeyOFFCounts = 0;
			return KEY_HOLD;
		//(2,0)对关闭计数累加直到切换至逻辑关闭状态
		case KEY_OFF:
			(KeyTemp->KeyOFFCounts)++;
			if (KeyTemp->KeyOFFCounts >= SHAKES_COUNTS)
			{
				KeyTemp->KeyLogic = KEY_OFF;
				KeyTemp->KeyOFFCounts = 0;
				return KEY_OFF;
			}
			return KEY_IDLE;
		default:
			break;
		}
	default:
		break;
	}
	
	//一般不会到这里
	return KEY_ERROR;
}

最后在主程序中对键盘进行循环检查,天神使用的是FREERTOS操作系统。

static void DataProcess_Task(void *parameter)
{
    while (1)
    {
        switch (Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN))
        {
        case KEY_ON:
            printf("Key1ON\n");
            break;
        
        case KEY_HOLD:
            printf("Key1HOLD\n");
            break;
        case KEY_OFF:
            printf("Key1OFF\n");
            break;
        case KEY_ERROR:
            printf("error\n");
            break;
        default:
            break;
        }
        switch (Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN))
        {
        case KEY_ON:
            printf("Key2ON\n");
            break;
        
        case KEY_HOLD:
            printf("Key2HOLD\n");
            break;
        
        case KEY_OFF:
            printf("Key2OFF\n");
            break;
        case KEY_ERROR:
            printf("error\n");
            break;
        default:
          
            break;
        }
        
        vTaskDelay(20);
    }
}

疗效

按下按钮1两秒钟后抬起

可以看见,没有多余的ON和OFF回去,同时我们的代码也高度对称,可谓完美。其实两个按钮同时按下也没有问题,不过调试的截图不容易看下来两个的疗效就没有放图片。假如须要还可以添加长松状态,代码才会完全对称!太棒了!

多击功能:双击、三击、四击、N击均可

为了实现多击功能,我们要在上文的基础上添加内容。首先添加两个储存的信息,分别是多击标志和多击计数,以下进行解释:

多击标志

要注意多击标志是一种预备状态,而后面的逻辑状态化学状态是当前时刻的状态。诸如双击标志的意思是预备双击,也就是键盘按下的第一次到第二次中的等待时间。若果超过200ms还没有按下第二次这么双击标志才会退回到单击标志。多击计数

多击计数就是拿来判定两次按下时间有没有超过200ms,超过的话就回到单机状态了。

特别须要注意的就是多击标志是一种预备状态,再指出一遍。

双击功能虽然就是在单击的基础上进行两次单击间隔的时间判定,假如大于最大时间间隔就返回双击讯号,不然两次均返回单击;三击功能虽然就是在判定双击的第二击与第三击的时间间隔;推川一下,N击就是在判定第N-1击的最后一击与第N击之间的时间间隔。由此我们看出多击虽然是可以递推的。

我们简单剖析一下在多击发生的过程:

接出来我们用代码进行实现。

在上文基础上实现多击功能

//按键状态结构体,存储四个变量
typedef struct
{
 	uint8_t KeyLogic;
	uint8_t KeyPhysic;
 	uint8_t KeyONCounts;
 	uint8_t KeyOFFCounts;
	
	//增加两个信息
	uint8_t MulClickCounts;
	uint8_t MulClickFlag;
}KEY_TypeDef;

降低一些宏定义,注意让第2击到第N击的标号是连续的,这样我们就可以便捷进行递推了。代码写完后,假如要添加更高次数的多击功能,只须要添加宏定义,不须要更改代码,是不是很棒?天神这儿设置的最多5击,其实理论上不论多少次都可以的。

#define    	KEY_OFF	   		0
#define    	KEY_ON	   	 	1
#define    	KEY_HOLD		7
#define    	KEY_1ClICK				KEY_ON
#define    	KEY_2ClICK				2
#define    	KEY_3ClICK				3
#define    	KEY_4ClICK				4
#define    	KEY_5ClICK				5
#define     KEY_MAX_MULCLICK		KEY_5ClICK
#define		KEY_IDLE		8
#define		KEY_ERROR		10
#define		HOLD_COUNTS					100
#define 	SHAKES_COUNTS				8
#define		MULTIPLE_CLICK_COUNTS		20

键盘扫描函数,用于在定时器或则操作系统中进行10ms定时的循环检查,逻辑KEY_ON和KEY_HOLD的情况相比里面的代码没有变化,主要变化在逻辑KEY_OFF中对多击情况的处理

/*
 * 函数名:Key_Scan
 * 描述  :检测是否有按键按下
 * 输入  :GPIOx:x 可以是 A,B,C,D或者 E
 *		     GPIO_Pin:待读取的端口位 	
 * 输出  :KEY_OFF(没按下按键)、KEY_ON(按下按键)
 */
static KEY_TypeDef Key[KEY_NUMS] =
	{{KEY_OFF, KEY_OFF, 0, 0, 0, KEY_1ClICK},
	 {KEY_OFF, KEY_OFF, 0, 0, 0, KEY_1ClICK}};
uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
	KEY_TypeDef *KeyTemp;
	uint8_t ReturnTemp;
	//检查按下的是哪一个按钮
	switch ((uint32_t)GPIOx)
	{
	case ((uint32_t)KEY1_GPIO_PORT):
		switch (GPIO_Pin)
		{
		case KEY1_GPIO_PIN:
			KeyTemp = &Key[0];
			break;
		//port和pin不匹配
		default:
			printf("error: GPIO port pin not match\r\n");
			return KEY_IDLE;
		}
		break;
	case ((uint32_t)KEY2_GPIO_PORT):
		switch (GPIO_Pin)
		{
		case KEY2_GPIO_PIN:
			KeyTemp = &Key[1];
			break;
		//port和pin不匹配
		default:
			printf("error: GPIO port pin not match\r\n");
			return KEY_IDLE;
		}
		break;
	default:
		printf("error: key do not exist\r\n");
		return KEY_IDLE;
	}
	KeyTemp->KeyPhysic = GPIO_ReadInputDataBit(GPIOx, GPIO_Pin);
	/* 检测按下、松开、长按 */
	switch (KeyTemp->KeyLogic)
	{
	case KEY_ON:
		switch (KeyTemp->KeyPhysic)
		{
		//(1,1)中将关闭计数清零,并对开启计数累加直到切换至逻辑长按状态
		case KEY_ON:
			KeyTemp->KeyOFFCounts = 0;
			KeyTemp->KeyONCounts++;
			KeyTemp->MulClickCounts = 0;
			
			if(KeyTemp->MulClickFlag == KEY_2ClICK){ // ready for 2clcik, but still only 1 click
				if (KeyTemp->KeyONCounts >= HOLD_COUNTS){
					KeyTemp->KeyONCounts = 0;
					KeyTemp->KeyLogic = KEY_HOLD;
					return KEY_HOLD;
				}
			}
			return KEY_IDLE;
		//(1,0)中对关闭计数累加直到切换至逻辑关闭状态
		case KEY_OFF:
			KeyTemp->KeyOFFCounts++;
			
			if (KeyTemp->KeyOFFCounts >= SHAKES_COUNTS)
			{
				KeyTemp->KeyLogic = KEY_OFF;
				KeyTemp->KeyOFFCounts = 0;
				
				return KEY_OFF;
			}
			return KEY_IDLE;
		default:
			break;
		}
	case KEY_OFF:
		switch (KeyTemp->KeyPhysic)
		{
		//(0,1)中对开启计数累加直到切换至逻辑开启状态
		case KEY_ON:
			(KeyTemp->KeyONCounts)++;
			if (KeyTemp->KeyONCounts >= SHAKES_COUNTS)
			{
				//KeyTemp->KeyLogic = KEY_ON;
				KeyTemp->KeyLogic = KEY_ON;			
				KeyTemp->KeyONCounts = 0;
				if(KeyTemp->MulClickFlag == KEY_1ClICK)
				{
					KeyTemp->MulClickFlag = KEY_2ClICK;  	//预备双击状态
					return KEY_IDLE;
				}
				else
				{
					if(KeyTemp->MulClickFlag != (KEY_MAX_MULCLICK + 1))
					{
						KeyTemp->MulClickFlag++;
						KeyTemp->MulClickCounts = 0;
					}					
				}				
			}
			return KEY_IDLE;
		//(0,0)中将开启计数清零,对多击计数
		case KEY_OFF:
			(KeyTemp->KeyONCounts) = 0;
			if(KeyTemp->MulClickFlag != KEY_1ClICK)
			{
				if(KeyTemp->MulClickCounts++ > MULTIPLE_CLICK_COUNTS) 	//超过多击最大间隔时间,关闭多击状态
				{
					ReturnTemp = KeyTemp->MulClickFlag - 1;
					KeyTemp->MulClickCounts = 0;
					KeyTemp->MulClickFlag = KEY_1ClICK;		
					return ReturnTemp;	
				}
			}
			
			return KEY_IDLE;
		default:
			break;
		}
	case KEY_HOLD:
		switch (KeyTemp->KeyPhysic)
		{
		//(2,1)对关闭计数清零
		case KEY_ON:
			KeyTemp->KeyOFFCounts = 0;
			KeyTemp->MulClickFlag = 0;
			KeyTemp->MulClickCounts = 0;
			return KEY_HOLD;
		//(2,0)对关闭计数累加直到切换至逻辑关闭状态
		case KEY_OFF:
			(KeyTemp->KeyOFFCounts)++;
			if (KeyTemp->KeyOFFCounts >= SHAKES_COUNTS)
			{
				KeyTemp->KeyLogic = KEY_OFF;
				KeyTemp->KeyOFFCounts = 0;
				return KEY_OFF;
			}
			return KEY_IDLE;
		default:
			break;
		}
	
	default:
		break;
	}
	return KEY_ERROR;
}

多击疗效

两个按钮同时测试(为了便捷展示不显示KEY_OFF状态):

可以见到两个按钮同时按下也没有问题。

天神刚写完就发觉问题了,键盘哪有五击的时侯也把2,3,4击也返回来的233。不过也挺好改,有空回去就改。还可以继续在基础上实现组合按钮,有空就添加。

以上代码可以保证正确了!

欢迎你们讨论学习,强调天神的不足或则提出更好的方案!

抖音双击和单击的区别