按键长短按检测

环境介绍

软件环境介绍

PC环境:Windows10 64-bit
IDE环境:MDK-ARM 5.24.2.0
工程名称:STM32_KeyTest
工程下载地址:github

硬件环境介绍

MCU平台:STM32F103C8T6
工程名称:STM32 Display Board V1.0
工程下载地址:github

应用环境介绍

按键检测是属于一个基础的单片机IO口应用,在消费级产品中如:洗衣机、抽油烟机、MP3…等等,几乎任何一个产品都会带有按键,因此在研发人员开发中就会遇到检测按键功能设计,本文从硬件原理到软件实现介绍如何检测按键的长短按,并触发相应的事件,可以轻松移植到任何MCU平台上。硬件平台使用的是以STM32F103C8T6位核心的一块STM32 Display Board V1.0开发板,开源在github上,可通过上面相应链接进行下载。软件工程也开源在github上可通过上面相应链接进行下载

原理介绍

硬件过程分析

按键电路一般是:一端接上拉电阻(IO口带上拉电阻配置可直接配置),一点接GND,当按键没有按下时位高电平,按下位低电平。按键在实际工作中会存在许多的干扰,如在按下的一瞬间会有抖动、误触发等。因为按键本身的特性原因,在按下的时候会有一个抖动电平,这个抖动会触发多个高低电平,因此会使按键无法正常直接检测。

状态与时间分析

本文章讨论按键长短按,上图就是一个按键长短按的整一个过程,在这个过程中可以把按键划分为如下几个状态:

  • A:IDLE-空闲状态,没有按键按下或按键释放后的状态
  • B:Debounce-消抖状态,按键按下瞬间的抖动状态
  • C:Pressed-按键按下状态,为低电平
  • D:Long_Pressed-按键长按状态,按键按下超过一定的时间
  • E:Release-按键释放状态,按键松开的状态

按键的触发过程是一个从左向右的单向过程,在这每个过程都对应着一个时间段,不过在检测长短按过程中只需重点要关注B:消抖时间、D:长按时间、E:释放时间

其他状态时间A:空闲状态时间、C:按键按下的时间可以有B、D、E的时间以及状态确定

在按键过程分析中,主要分析下图的三个过程:

  • 按键消抖过程(或误触发)
  • 按键短按过程
  • 按键长按过程

程序设计

数据结构定义

数据结构的定义位于button.h文件。从上面的分析可以知道,在按键长短按过程中主要涉及五个状态与三个重要时间段,因为在一个时间段内只能存在一种状态,程序中可以把状态定义为枚举类型:

1
2
3
4
5
6
7
8
typedef enum
{
IDLE, //空闲
Debounce, //消抖
Pressed, //按下
Long_Pressed, //长按
Release, //释放
}key_status; //按键状态

同时使用结构体类型定义按键长短按的时间变量:

1
2
3
4
5
6
typedef struct
{
unsigned int Time_Debounce; //消抖时间
unsigned int Time_LongPressed; //长按时间
unsigned int Time_Release; //释放时间
}key_time; //消抖、长按&释放时间

硬件上有六个按键,每一个按键都有长按短按两种情况,应为一次只能检测一个按键的长短按,所以使用枚举的方式定义KEY1~KEY6的长短按键值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef enum
{
NULL_Pressed=0, //无按键按下
KEY1_Pressed, //KEY1按下(短按)
KEY2_Pressed, //KEY2按下(短按)
KEY3_Pressed, //KEY3按下(短按)
KEY4_Pressed, //KEY4按下(短按)
KEY5_Pressed, //KEY5按下(短按)
KEY6_Pressed, //KEY6按下(短按)
KEY1_LongPressed, //KEY1长按
KEY2_LongPressed, //KEY2长按
KEY3_LongPressed, //KEY3长按
KEY4_LongPressed, //KEY4长按
KEY5_LongPressed, //KEY5长按
KEY6_LongPressed, //KEY6长按
}key_value; //键值

为了能方便的找到是哪个按键处于那个状态,还是短按(或长按),可以定义一个键表,方便在扫描过程中找对对应按键:

1
2
3
4
5
6
7
8
typedef struct
{
unsigned int Index; //按键索引(按键按下的键值)
key_value Value1; //键值1(KEYx按下)
key_value Value2; //键值2(KEYx长按)
key_status State1; //按键状态1(按下状态)
key_status State2; //按键状态2(长按状态)
}KEY_Table;

索引值指的是按键按下时对应的值,硬件中KEY1~KEY6都使用PB上的IO口,每个按键按下的键值(十六进制)都不一样,使用宏定义:

1
2
3
4
5
6
7
#define KEY_VALUE		0x3c03			//没按键按下的键值
#define KEY1_VALUE 0x3c02 //KEY1按下的键值
#define KEY2_VALUE 0x3c01 //KEY2按下的键值
#define KEY3_VALUE 0x3803 //KEY3按下的键值
#define KEY4_VALUE 0x3403 //KEY4按下的键值
#define KEY5_VALUE 0x2c03 //KEY5按下的键值
#define KEY6_VALUE 0x1c03 //KEY6按下的键值

时间参数一般消抖采用50ms,释放3ms,长按2s,按键的检测周期位10ms,对应时间可以使用宏定义:

1
2
3
#define	RELEASE_TIMES			3			//按键释放时间
#define DEBOUNCE_TIMES 5 //按键消抖时间
#define LONG_PRESSED_TIMES 200 //按键长按时间

按键扫描过程程序设计

按键扫描过程程序位于button.c文件。在扫描前先定义一个键值表存放每个按键的索引、键值、状态:

1
2
3
4
5
6
7
8
9
10
//键值表
const KEY_Table key_tab[]=
{
{KEY1_VALUE, KEY1_Pressed, KEY1_LongPressed, Pressed, Long_Pressed}, //KEY1-短按-长按
{KEY2_VALUE, KEY2_Pressed, KEY2_LongPressed, Pressed, Long_Pressed}, //KEY2-短按-长按
{KEY3_VALUE, KEY3_Pressed, KEY3_LongPressed, Pressed, Long_Pressed}, //KEY3-短按-长按
{KEY4_VALUE, KEY4_Pressed, KEY4_LongPressed, Pressed, Long_Pressed}, //KEY4-短按-长按
{KEY5_VALUE, KEY5_Pressed, KEY5_LongPressed, Pressed, Long_Pressed}, //KEY5-短按-长按
{KEY6_VALUE, KEY6_Pressed, KEY6_LongPressed, Pressed, Long_Pressed}, //KEY6-短按-长按
};

按键扫描过程:

  • 读取键值,如果有按键按下则赋值释放时间,然后判断上一次的状态
    • 如果为空闲则证明按键第一次按下,赋值消抖时间并把状态改为消抖
    • 如果在消抖状态并消抖完成,赋值长按时间并在键值表中找出对应的键值,把状态改为按下;如果没有找到则判断为误触发或多按键同时按下
    • 如果在按下状态并检测到长按时间完成,在键值表中找出对应的键值,把状态改为长按;如果没有找到则判断为误触发或多按键同时按下
    • 如过在长按状态下则不处理
    • 其他情况把状态改为空闲并退出
  • 读取键值后,发现是没有按键按下,则贩毒案释放时间是否完成,如果完成则判断状态
    • 如果为按下状态,则找出对应按键并返回它的值(按下)
    • 如果为长按状态,则找出对应按键并返回它的值(长按)
    • 其他状态,如空闲、消抖等等;把状态置为空闲后退出
      注:按键短按长按要在释放后才有效;函数中有多处是使用static修饰的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* @函数名 check_user_key
* @功 能 按键扫描
* @参 数 无
* @返回值 key_value:键值
*/
key_value KEY_Scan(void)
{
static key_status state; //按键状态
static key_time time; //按键消抖和释放的时间
volatile key_value KEY_Value = NULL_Pressed; //键值,初始为空
static unsigned char i; //扫描键值表计数:i
unsigned int key_temp;

key_temp = (GPIO_ReadInputData(GPIOB) & KEY_VALUE); //读取键值
if(key_temp != KEY_VALUE) //判断是否有按键按下
{
time.Time_Release = RELEASE_TIMES; //有按键按下(在按下的状态),赋值释放时间
switch(state) //判断上一次按键的状态
{
case IDLE: //空闲状态
time.Time_Debounce = DEBOUNCE_TIMES; //赋值消抖时间
state = Debounce; //将状态改为消抖状态
break; //退出

case Debounce: //消抖状态
if(--time.Time_Debounce == 0) //消抖完成
{
time.Time_LongPressed = LONG_PRESSED_TIMES; //长按的时间赋值

for(i=0; ;i++) //循环查询键值表
{
if(key_temp == key_tab[i].Index) //循环查询索引
{
state = key_tab[i].State1; //对应索引的状态为按下(PRESSED)
break; //退出循环
}
else if(i >= 6) //超出按键扫描范围,或同时按到其他按键
{
state=IDLE; //把状态重新配置为空闲
return NULL_Pressed; //返回空值
}
}
}
break;

case Pressed: //按键按下状态
if(--time.Time_LongPressed == 0) //长按消抖完成
{
for(i=0; ;i++) //循环查询键值表
{
if(key_temp == key_tab[i].Index) //重新循环查询索引
{
state = key_tab[i].State2; //对应索引的状态为长按(LONG_PRESSED)
break; //退出循环
}
else if(i > 6) //超出按键扫描范围,或同时按到其他按键
{
state=IDLE; //把状态重新配置为空闲
return NULL_Pressed; //返回空值
}
}
}
break;


case Long_Pressed: //一直保持长按,退出,不处理
break;

default: //其他情况,将状态设为空闲,退出
state=IDLE;
break;
}
}
else
{
if(time.Time_Release != 0) //判断按键是否已经释放完成(空闲状态时stkey.release一直等于0)
{
if(--time.Time_Release == 0) //如果按键已经释放
{
switch(state)
{
case Pressed: //按键按下,返回单击键值
KEY_Value=key_tab[i].Value1;
state=IDLE; //将状态设为空闲
break;

case Long_Pressed: //按键长按,返回按键长按键值
KEY_Value=key_tab[i].Value2;
state=IDLE; //将状态设为空闲
break;

default: //其他情况(消抖期间释放,或一直没按键按下),将状态设为空闲,退出
state=IDLE;
break;
}
}
}
}
return KEY_Value; //返回键值
}

键值处理过程程序设计

键值处理过程程序位于button.c文件。将上面获取的键值,根据不同的键值进行不同的处理,需要注意的是:程序中使用窗口打印信息,一般不建议在处理函数中做复杂的处理,这会影响程序运行的时间,这里主要作为演示效果使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* @函数名 KeyProcess
* @功 能 按键处理,根据键值来执行相应的操作
* @参 数 key_val:键值
* @返回值 无
*/
void KEY_Process(key_value key_value)
{
switch(key_value)
{
case KEY1_Pressed: //KEY1按下(短按)
printf("KEY1_Pressed! \r\n"); //打印信息
break;

case KEY2_Pressed: //KEY2按下(短按)
printf("KEY2_Pressed! \r\n"); //打印信息
break;

case KEY3_Pressed: //KEY3按下(短按)
printf("KEY3_Pressed! \r\n"); //打印信息
break;

case KEY4_Pressed: //KEY4按下(短按)
printf("KEY4_Pressed! \r\n"); //打印信息
break;

case KEY5_Pressed: //KEY5按下(短按)
printf("KEY5_Pressed! \r\n"); //打印信息
break;

case KEY6_Pressed: //KEY6按下(短按)
printf("KEY6_Pressed! \r\n"); //打印信息
break;

case KEY1_LongPressed: //KEY1长按
printf("KEY1_LongPressed! \r\n"); //打印信息
break;

case KEY2_LongPressed: //KEY2长按
printf("KEY2_LongPressed! \r\n"); //打印信息
break;

case KEY3_LongPressed: //KEY3长按
printf("KEY3_LongPressed! \r\n"); //打印信息
break;

case KEY4_LongPressed: //KEY4长按
printf("KEY4_LongPressed! \r\n"); //打印信息
break;

case KEY5_LongPressed: //KEY5长按
printf("KEY5_LongPressed! \r\n"); //打印信息
break;

case KEY6_LongPressed: //KEY6长按
printf("KEY6_LongPressed! \r\n"); //打印信息
break;

default: //其他
break;
}
}

主函数处理

在初始化好相应的接口后,只需要提供10ms作为按键扫描的时基,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @函数名 main
* @功 能 主函数入口
* @参 数 无
* @返回值 无
*/
int main(void)
{
key_value temp; //键值变量

LED_Init(); //LED初始化
KEY_Init(); //KEY初始化
USART1_Init(); //串口初始化
Delay_Init(); //延迟初始化

GPIO_ResetBits(LED1_PORT,LED1_PIN); //LED1亮
GPIO_ResetBits(LED2_PORT,LED2_PIN); //LED2亮


while(1)
{
temp = KEY_Scan(); //获取键值
if(temp != NULL_Pressed) //如果键值非空(有按键按下)
KEY_Process(temp); //处理键值
Delay_ms(10); //时间单位10ms
}
}

实现效果

使用USB转TTL把串口链接PC,打开上位机(这里使用的是SecureCRT 8.3,其他的串口助手也一样),配置波特率115200、停止位1位,数据位8位,无奇偶校验,无流控。效果中首先短按KEY1、KEY3,然后长按KEY2、KEY4:

本文标题:按键长短按检测

文章作者:LGG001

发布时间:2018年10月30日 - 20:10

最后更新:2019年01月13日 - 21:01

原始链接:http://yoursite.com/2018/10/30/按键长短按检测/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------
Thank You For Your Approval !