如果有更好的解决方案或是发现天神的方案有问题,欢迎大家热烈讨论!
明确按键的使用环境和终极目标
使用环境
首先我们的按键使用在有操作系统的环境中,不能使用占用CPU的延时函数,使用操作系统的延时每20ms对按键进行一次检测。
终极目标
我们的按键需要实现的终极目标是检测按键按下、长按、松开、长松(一般没啥用)。按键的按下我们希望按下一次,程序中只反应出一次按下来,而不是唰唰响应了一长串,同样松开也是。对于长按我们希望在按键按下后过一会才反应出来,这个是需要唰唰一直响应的,只要不松开程序就一直反应出长按。长松和长按是一样的。
按键的程序信号、逻辑状态、物理状态、开启计数、关闭计数
根据我们的环境和目标,天神总结出来我们的按键需要有1+4个信息来记录按键的状态,也就是标题中的程序信号、逻辑状态、物理状态、开启计数、关闭计数。其中程序信号写成信号是因为这些信号不需要存储,通过逻辑状态、物理状态的关系直接返回。对此天神画了一个图进行分析:
以下进行详细介绍(不含长松状态):
- 程序信号
程序信号有四种:关闭、开启、长按、等待
其中等待状态是为了当按键已经按下或是松开后程序不再重复响应而设置- 逻辑状态
逻辑状态有三种:关闭、开启、长按
没有等待状态- 物理状态
物理状态是按键按下后单片机IO口接收到的状态,是有抖动的- 开启计数与关闭计数
将这两个计数分开是为了在开启和关闭时都做出消抖,并且开启计数可以用来作为对长按的延迟计数。当状态为(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()函数,这个函数要在操作系统的任务中循环执行,因此其中不能有阻塞延时。
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);
}
}
效果
可以看到,没有多余的ON和OFF回来,同时我们的代码也高度对称,堪称完美。当然两个按键同时按下也没有问题,不过调试的截图不容易看出来两个的效果就没有放图片。如果需要还可以添加长松状态,代码就会完全对称!太棒了!