stm32单片机按键消抖、长按、多击终极解决方案

您所在的位置:网站首页 西门子e67调试完按键都按不了 stm32单片机按键消抖、长按、多击终极解决方案

stm32单片机按键消抖、长按、多击终极解决方案

2024-07-09 22:08| 来源: 网络整理| 查看: 265

如果有更好的解决方案或是发现天神的方案有问题,欢迎大家热烈讨论!

明确按键的使用环境和终极目标 使用环境

首先我们的按键使用在有操作系统的环境中,不能使用占用CPU的延时函数,使用操作系统的延时每20ms对按键进行一次检测。 补充:经过天神的思考,发现在操作系统中进行循环和使用定时器中断效果其实是一样的,完全可以将按键检测函数放进一个定时20ms的定时器之中,效果是一样的。(操作系统本质上也是使用了systick进行定时的)

终极目标

我们的按键需要实现的终极目标是检测按键 按下、长按、松开、多击(双击三击等等)。

按键的按下我们希望按下一次,程序中只反应出一次按下来,而不是唰唰响应了一长串,同样松开也是。对于长按我们希望在按键按下后先反应出一次按下,之后持续按着按键并等待一小会时间后开始反应出长按,并且是唰唰一直响应的,只要不松开程序就一直不停的反应出长按。按键的多击其实就是当多次按下之间的时间小于200ms时程序只收到第一个按下的按下信号,之后几个按下反应出的是对应次数的多击信号。还有一个比较重要的点是不同按键不能互相影响,可以实现多个按键一起按下并且效果正确。 按键的程序信号、逻辑状态、物理状态、开启计数、关闭计数、多击标志、多击计数

根据我们的环境和目标,天神总结出来我们的按键需要有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击之间的时间间隔。由此我们看出多击其实是可以递推的。

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

在快速按下一次按键后,按键马上进入(0,0)状态,此时就要对多击计数进行累加,直到发生下一次按键或者超出设定时间。

同时在按键完成开启消抖,准备要返回信号时[注意此时是(0,1)状态],对多击标志进行判断,返回对应的多击信号,并且让标志变为下一击的标志,实现递推。在递推过程中只要有一次按键超出时长那就退回到单机模式。

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

在上文基础上实现多击功能 //按键状态结构体,存储四个变量 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。不过也很好改,有空回来就改。还可以继续在基础上实现组合按键,有空就添加。 以上代码可以保证正确了!

欢迎大家讨论学习,指出天神的不足或者提出更好的方案!


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3