上篇文章,介绍了将按键检测增加长按功能,并将按下抖动与松开抖动共用一个抖动状态来表示,其状态图如下:

仔细研究这个状态图,其它还存在一些问题:

  • 短按状态,只要按下去,不需要等按键再释放,就会触发短按事件。对于需要按下再松开作为一次短按的应用来说,此状态图也不满足需求
  • 长按状态,必须先经过短按状态,即长按按键,会先触发一个短按,再触发一个长按。如果实际应用中需要分别使用短按和长按,则此状态图不满足要求

本篇,就来解决上述两个问题,并再增加一个按键双击检测,实现一个功能更全面的按键检测。

1 增加双击检测

增加一个双击检测,需要增加两个状态:

  • 等待再次按下
  • 确认第2次按下

同时,之前的“短按状态”和“长按状态”分别改为“确认按下”和“确认长按”。

1.1 状态图修改

修改后的状态图如下,有以下几点需要注意:

  • 确认按下”不是短按触发的条件,需要等松开后,经消抖进入到“等待再次按下”一段时间后(200ms),没有再次被按下,才触发短按事件,这样就解决了本篇开头提到的第1个问题
  • 确认按下”不是短按触发的条件,另一个用途是,当此状态继续保持按下状态一段时间后(1s),则会单独触发长按事件,同时进入到“确认长按”状态,这样就解决了本篇开头提到的第2个问题
  • 对于双击事件的检测,首先按下按键进入“确认按下”状态,然后在1s内松开进入“等待再次按下”状态,接着在200ms内再次按下进入“确认第2次按下”状态,然后在1s内松开,即可触发双击事件,并同时进入“稳定松开”状态
  • 注意,在“确认第2次按下”状态下,如果在1s内没有松开,也会进入到“确认长按”状态

1.2 程序编写

根据状态图,修改对应的状态机逻辑,修改后的代码如下:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
void key_status_check()
{
switch(g_keyStatus)
{
//按键释放(初始状态)
case KS_RELEASE:
{
//检测到低电平,先进行消抖
if (KEY0 == 0)
{
g_keyStatus = KS_SHAKE;
}
}
break;

//抖动
case KS_SHAKE:
{
if (KEY0 == 1)
{
//从松开状态来的抖动
if (KS_RELEASE == g_lastKeyStatus)
{
g_keyStatus = KS_RELEASE;
}
//从等待再次按下状态来的抖动
else if (KS_WAIT_PRESS_AGAIN == g_lastKeyStatus)
{
g_keyStatus = KS_WAIT_PRESS_AGAIN;
}
//从确认按下状态来
else if (KS_AFFIRM_SHORT_PRESS == g_lastKeyStatus)
{
g_WaitPressAgainCnt = 0;
g_keyStatus = KS_WAIT_PRESS_AGAIN;
}
//从确认再次按下状态来
else if (KS_AFFIRM_PRESS_AGAIN == g_lastKeyStatus)
{
printf("=====> key double press\r\n");
g_keyStatus = KS_RELEASE;
}
//从确认长按状态来
else if (KS_AFFIRM_LONG_PRESS == g_lastKeyStatus)
{
g_keyStatus = KS_RELEASE;
}
else
{
printf("err!\r\n");
}
}
else
{
//从确认按下状态来的抖动
if (KS_AFFIRM_SHORT_PRESS == g_lastKeyStatus)
{
g_keyStatus = KS_AFFIRM_SHORT_PRESS;
}
//从第2次按下状态来的抖动
else if (KS_AFFIRM_PRESS_AGAIN == g_lastKeyStatus)
{
g_keyStatus = KS_AFFIRM_PRESS_AGAIN;
}
//从确认长按状态来的抖动
else if (KS_AFFIRM_LONG_PRESS == g_lastKeyStatus)
{
g_keyStatus = KS_AFFIRM_LONG_PRESS;
}
//从松开状态而来
else if (KS_RELEASE == g_lastKeyStatus)
{
g_PressTimeCnt = 0;
g_keyStatus = KS_AFFIRM_SHORT_PRESS;
//printf("=====> key short press\r\n");
}
//从等待再次看下(的松开)状态而来
else if (KS_WAIT_PRESS_AGAIN == g_lastKeyStatus)
{
g_Press2TimeCnt = 0;
g_keyStatus = KS_AFFIRM_PRESS_AGAIN;
}
else
{
printf("err!\r\n");
}
}
}
break;

//确认按下
case KS_AFFIRM_SHORT_PRESS:
{
//检测到高电平,先进行消抖
if (KEY0 == 1)
{
g_keyStatus = KS_SHAKE;
}
else
{
if (g_LongPressTimeCnt % 20 == 0) //每隔1000ms打印一次
{
printf("=====> key long press:%d\r\n", g_LongPressTimeCnt/20);

keyEvent = KE_LONG_PRESS;
}
g_LongPressTimeCnt++;
}
}
break;

//等待再次按下
case KS_WAIT_PRESS_AGAIN:
{
//检测到低电平,先进行消抖
if (KEY0 == 0)
{
g_keyStatus = KS_SHAKE;
}

g_WaitPressAgainCnt++;
if (g_WaitPressAgainCnt == 4) //200ms没有再次按下
{
printf("=====> key single press\r\n");
g_keyStatus = KS_RELEASE;
}
}
break;

//确认第2次按下
case KS_AFFIRM_PRESS_AGAIN:
{
//检测到高电平,先进行消抖
if (KEY0 == 1)
{
g_keyStatus = KS_SHAKE;
}

g_Press2TimeCnt++;
if (g_Press2TimeCnt == 20) //1000ms
{
g_LongPressTimeCnt = 0;
g_keyStatus = KS_AFFIRM_LONG_PRESS;
}
}
break;

//确认长按
case KS_AFFIRM_LONG_PRESS:
{
//检测到高电平,先进行消抖
if (KEY0 == 1)
{
g_keyStatus = KS_SHAKE;
}

g_LongPressTimeCnt++;
if (g_LongPressTimeCnt % 20 == 0) //每隔1000ms打印一次
{
printf("=====> key long press:%d\r\n", g_LongPressTimeCnt/20);
}
}
break;

default:break;
}

if (g_keyStatus != g_nowKeyStatus)
{
g_lastKeyStatus = g_nowKeyStatus;
g_nowKeyStatus = g_keyStatus;
//printf("new key status:%d(%s)\r\n", g_keyStatus, key_status_name[g_keyStatus]);
}
}

最后注释掉的一句是调试打印,调试时可打开,方便观察状态变化

1.3 测试

短按、长按、双击的测试结果如下:

还有从确认第2次按下状态到达的长按状态:

2 功能优化

上面的代码实现,是在主函数中,每50ms延时执行一次状态机循环(主函数代码如下),仅用做演示按键状态机的运行机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void)
{
delay_init();
KEY_Init();
uart_init(115200);

printf("hello\r\n");

while(1)
{
key_status_check();
delay_ms(50);
}
}

实际开发中,按键检测程序,应该作为一个独立的模块运行,当检测到某一按键状态触发时,通知应用程序来使用。

对于stm32裸机开发来说,可以将按键状态机放到一个定时器中断服务函数中运行,当检测到某一按键状态触发后,通知应用程序:

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
//主函数
int main(void)
{
delay_init();
KEY_Init();
uart_init(115200);
TIM3_Int_Init(500-1,7200-1); //调用定时器使得50ms产生一个中断

printf("hello\r\n");

while(1)
{
}
}

//定时器3中断服务程序
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx更新中断标志

KEY_EVENT keyEvent = key_status_check();
switch (keyEvent)
{
case KE_SHORT_PRESS: printf("检测到单击\r\n"); break;
case KE_DOUBLE_PRESS: printf("检测到双击\r\n"); break;
case KE_LONG_PRESS: printf("检测到长按\r\n"); break;
default:break;
}
}
}

3 总结

本篇在前两篇按键状态机的基础上,继续介绍增加按键的双击功能,并解决之前状态存在的两个问题,通过实测验证,演示短按、长按、双击的使用效果。最后对代码结构进行优化,使其更符合实际开发应用。