前言
本节内容,我们学习一下物理按键的结构与控制方式。按键是一种典型的输入元器件,认识了按键,我们就可以和之前的章节所学的元器件们联动起来了。
本节涉及到的封装源文件可在《模块功能封装汇总》中找到。
本节完整工程文件已上传GitHub,仓库地址,欢迎下载交流!
硬件介绍
在物理电路中,我们最熟知的一个词就是开关,那其实就是现实世界中按键(Key)的理想模型。
按键在我们日常生活中几乎无处不在:手机的音量键、电脑键盘、日光灯的开关、电视机的电源按钮…
自锁与自复位
从机械结构来说,按键分为自锁式和自复位式。两者的应用场景并不相同。
-
自锁式:按键按下和未按下的状态不同,对用户有引导作用。一般应用于没有明显的用户交互界面的场景,用户可以自行判断按键状态。但价格稍贵。
-
自复位式:按键按下和未按下的状态相同,仅从按键本身并不能判断是否(曾)被按下。一般应用于有明显的用户交互界面的场景,例如手机音量键,用户不用关注按键状态,只需观察屏幕反馈即可。
以上两种类型与按键的电气特性无关,仅仅是为了介绍全面,多啰嗦几句。本文研究的是自复位式独立按键。
按键响应事件
对于一个独立按键,它有两种状态:按下和释放。这就意味着每次按键动作都有两个状态可以被观测,称为按下响应和释放响应。
在有些场景下,按键响应我们只希望它执行一次,例如一次点击就打开一个应用(而不是按一次就弹出来许多);在其他场景下,我们希望按住按键可以使响应连续执行,例如调节音量(否则从 0 按到 100 将相当痛苦)。这即是单次响应与连续响应。
还有一种情况,我希望在按下某个按键时不会受其他按键的干扰。这即为按键屏蔽问题。
因此,联系手机或键盘的按键操作,大致可以将按键事件分为以下几类:
- 短按: 计算按下响应和释放响应的时间间隔 t,若小于设定的阈值,则视为短按事件。
- 按下立刻执行(无论释放与否)
- 释放立刻执行(按下没有反应)
- 按下执行,释放也执行
- 长按: 若大于设定的阈值,则视为长按事件。
- 单次执行:如手机关机
- 连续执行:如调节音量
- 双击: 若同个按键两次按下响应的时间间隔小于设定的阈值,则视为双击事件。
- 必是两个短按事件,如鼠标双击打开应用
- 组合键: 若两个不同的按键按下响应的时间间隔小于设定的阈值,则视为组合键事件。
- 短按:如截屏
- 长按:如强制重启
- 粘滞键:组合键触发方式比较苛刻,手速比较慢的人可能难以成功按出组合键,因此出现了粘滞键。它允许先按下一个键,再按下另一个键实现组合。
- 对按键顺序有所要求,首先按下的键不应有响应事件,而且只有等全部松开时才开始新一轮检测。比如键盘中的
Shift\Ctrl
键。
- 对按键顺序有所要求,首先按下的键不应有响应事件,而且只有等全部松开时才开始新一轮检测。比如键盘中的
按键抖动
另外,在真实按键中,由于按键物理结构的限制,必存在机械振动,俗称按键抖动。如何防抖,什么时候防抖是关键所在。一般可分为硬件防抖和软件防抖两种。
- 硬件防抖:在稳定性要求高的场景,应当优先考虑。比软件防抖稳定,但会增加成本。
-
低通滤波电路:在按键两端并联电容,由于电容两端电压不能突变,必定存在一个电容充放电的时间常数,从而滤去抖动。
-
RS触发器:利用RS触发器来吸收按键的抖动。一旦有键按下,触发器立即翻转,按键的抖动便不会再对输出产生影响,按键释放时也一样。
-
- 软件防抖:一般的按键抖动时间为5~10ms,而人不太可能以大于100Hz的频率去按键。所以通过延时可以消除抖动带来的影响,也不会过滤掉人的正常操作。
按键抖动是一种不可消除的机械振动形式,客观存在。其影响是,按一次可能会导致MCU执行多次。抖动时间依赖于按键的机械结构设计,越优质的结构和材料抖动时间越短。需要强调的是,无论是硬件还是软件防抖都不能过滤人为的误触(时间量级不一样)。
为防止误触,一般将比较重要的功能以长按、双击、组合键的形式设置。
在工业或是军事场景中,按键响应除了要满足高度实时的要求,还必须高度可靠,最有效的方法即检测多次。 例如,在10ms内检测10次按键状态,当全部有效时,才视为按键按下,而普通的软件延时仅仅检测了2次,显然可靠性不及前者。 在定时器章节,我们将利用这种思想实现最佳按键检测。
原理图分析
独立按键
观察独立按键,共有四个引脚,其中两组间距较短,而另两组间距较长。间距长的两组引脚之间是连接在一起的,而短间距引脚之间初始状态是断开的,当按键被按下时,四个引脚被接通,可视作一根导线。
一般情况下,独立按键只需要接两个脚即可使用。其中一个引脚接地,另一个引脚由单片机IO口控制。
开发板上独立按键的硬件原理图如上。
软件实现
注:本节暂不讨论长按和双击事件,在后续章节中通过定时器实现。
单键单次与连续响应
实验效果:四个独立按键,分别控制四个LED的开关,实现不同的按键效果。数码管显示按键编号。
- 按键1按下,LED6亮灭状态转变。按键1松开,不执行操作。(按下响应)
- 按键2按下,不执行操作。按键2松开,LED7亮灭状态转变。(松开响应)
- 按键3按下,LED8亮灭状态转变。按键3松开,LED8亮灭状态转变。(按下松开均响应)
- 按键4按下,数码管显示数值持续增加。按键4松开,数码管清零。(连续响应)
注:未展示的头文件、源文件可在《模块功能封装汇总》中找到。
key.h
#ifndef __KEY_H__
#define __KEY_H__
#include "delay.h"
// 按键单次响应(0)或连续响应(1)开关
#define KEY1_MODE 0
#define KEY2_MODE 0
#define KEY3_MODE 0
#define KEY4_MODE 1
// 引脚定义
sbit key1 = P3^1;
sbit key2 = P3^0;
sbit key3 = P3^2;
sbit key4 = P3^3;
// 按键状态枚举
typedef enum{
KEY_UNPRESS = 0,
KEY1_PRESS,
KEY2_PRESS,
KEY3_PRESS,
KEY4_PRESS,
KEY_NON_DEAL,
}Key_State;
void scan_key();
void check_key();
#endif
key.c
#include "key.h"
#include "led.h"
#include "smg.h"
#include <stdio.h>
/**
** @brief 独立按键的函数封装
** @author QIU
** @date 2023.08.23
**/
/*-------------------------------------------------------------------*/
// 按键状态、前状态
Key_State key_state, key_pre_state;
// 按键累积值,反映按键时长
u16 num = 0;
/**
** @brief 按键1按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_pressed(){
smg_showChar('1', 1, false);
led_turn(6);
}
/**
** @brief 按键2按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_pressed(){
;
}
/**
** @brief 按键3按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_pressed(){
smg_showChar('3', 1, false);
led_turn(8);
}
/**
** @brief 按键4按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key4_pressed(){
u8 str[8];
num++;
sprintf(str,"%d",num/10);
smg_showString(str, 1);
}
/**
** @brief 按键1松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_unpressed(){
;
}
/**
** @brief 按键2松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_unpressed(){
smg_showChar('2', 1, false);
led_turn(7);
}
/**
** @brief 按键3松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_unpressed(){
key3_pressed();
}
/**
** @brief 按键4松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key4_unpressed(){
smg_showChar('0', 1, false);
num = 0;
}
/*-------------------------------------------------------------------*/
/**
** @brief 按键松开响应
** @param 无
** @retval 无
**/
void key_unpress(){
switch(key_pre_state){
case KEY_UNPRESS: break;
case KEY1_PRESS: key1_unpressed();break;
case KEY2_PRESS: key2_unpressed();break;
case KEY3_PRESS: key3_unpressed();break;
case KEY4_PRESS: key4_unpressed();break;
}
}
/**
** @brief (轮询方式)扫描独立按键,判断哪个键按下
** @param 无
** @retval 无
**/
void scan_key(){
static u8 flag = 1; // 开关(按下至完全松开为一轮判断)
// 如果有按键按下
if(flag && (!key1||!key2||!key3||!key4)){
flag = 0; // 清零
delay_ms(10); // 延时10ms消抖
// key1往下屏蔽(屏蔽组合键)
if(!key1) key_state = KEY1_PRESS;
else if(!key2) key_state = KEY2_PRESS;
else if(!key3) key_state = KEY3_PRESS;
else if(!key4) key_state = KEY4_PRESS;
else key_state = KEY_UNPRESS;
// 如果按键松开
}else if(key1&&key2&&key3&&key4){
flag = 1;
delay_ms(10); // 延时10ms消抖,松开响应有逻辑判断时,需要加上消抖。否则可以省略。
if(key1&&key2&&key3&&key4)key_state = KEY_UNPRESS;
}
}
/**
** @brief (轮询方式)判断按键, 进行相应处理
** @param 无
** @retval 无
**/
void check_key(){
switch(key_state){
case KEY_UNPRESS:
// 松开响应
key_unpress();
// 记录当前按键状态
key_pre_state = KEY_UNPRESS;
break;
case KEY1_PRESS:
// 记录当前按键状态
key_pre_state = KEY1_PRESS;
// 如果是单次响应
if(!KEY1_MODE) key_state = KEY_NON_DEAL;
key1_pressed();
break;
case KEY2_PRESS:
// 记录当前按键状态
key_pre_state = KEY2_PRESS;
// 如果是单次响应
if(!KEY2_MODE) key_state = KEY_NON_DEAL;
break;
case KEY3_PRESS:
// 记录当前按键状态
key_pre_state = KEY3_PRESS;
// 如果是单次响应
if(!KEY3_MODE) key_state = KEY_NON_DEAL;
key3_pressed();
break;
case KEY4_PRESS:
// 记录当前按键状态
key_pre_state = KEY4_PRESS;
if(!KEY4_MODE) key_state = KEY_NON_DEAL;
key4_pressed();
break;
case KEY_NON_DEAL:
// 按下不处理
break;
}
}
main.c
#include "key.h"
#include "smg.h"
/**
** @brief 独立按键单键的单次响应与连续响应
** 1. 按键1按下,LED6亮灭状态转变。按键1松开,不执行操作。(按下响应)
** 2. 按键2按下,不执行操作。按键2松开,LED7亮灭状态转变。(松开响应)
** 3. 按键3按下,LED8亮灭状态转变。按键3松开,LED8亮灭状态转变。(按下松开均响应)
** 4. 按键4按下,数码管显示数值持续增加。按键4松开,数码管清零。(连续响应)
** @author QIU
** @date 2023.08.31
**/
/*-------------------------------------------------------------------*/
void main(){
smg_showChar('0', 1, false); // 初始为0
while(1){
// 扫描按键
scan_key();
// 检查按键
check_key();
}
}
本例联动了之前篇章的LED、数码管等元器件。核心代码是在key.h
和key.c
中,采用了状态机的思想,将按键事件分为6个状态
- KEY_UNPRESS:未有按键按下
- KEY1_PRESS:按键1按下
- KEY2_PRESS:按键2按下
- KEY3_PRESS:按键3按下
- KEY4_PRESS:按键4按下
- KEY_NON_DEAL:按下未松开状态
主要逻辑是:通过scan_key()
函数不断轮询四个独立按键状态,一旦发生改变,延时10ms消抖,再次检测,以确定是否存在按键按下或是松开。然后判断是哪一个按键被按下,记录当前状态。此后,用check_key()
轮询按键状态,并在该函数内实现各个按键所执行的逻辑代码。
对于一些需要松开响应的场景,应当用变量key_pre_state
记录前一个按键状态,以判断是哪一个键被松开。
对于一些需要连续响应的场景,应当用宏定义KEYx_MODE
来设置按键的响应模式。例如开灯的时候,我们只希望在按下时执行一次,而在调节音量时,我们希望在按下时能持续执行。
注意:
if .. else if
语句具有向下屏蔽的特点。在此例中,按键1会屏蔽按键2~4的响应,即按下按键1时,再按下其他的按键并不响应。- 按下和松开都存在抖动,但如果只需对按下事件做出响应时,松开消抖延时可以省略。
组合键单次与连续响应
实验效果:
- 按键1和按键2同时按下,LED切换为流水灯模式。
- 按键2和按键3同时按下,LED切换为跑马灯模式。
- 按键3和按键4同时按下,LED模式速度持续减慢(数字越大,时间间隔越长)。
key.h
#ifndef __KEY_H__
#define __KEY_H__
#include "delay.h"
// 按键单次响应(0)或连续响应(1)开关
#define KEY1_MODE 0
#define KEY2_MODE 0
#define KEY3_MODE 0
#define KEY4_MODE 1
#define KEY_1_2_MODE 0
#define KEY_2_3_MODE 0
#define KEY_3_4_MODE 1
// 引脚定义
sbit key1 = P3^1;
sbit key2 = P3^0;
sbit key3 = P3^2;
sbit key4 = P3^3;
// 按键状态枚举
typedef enum{
KEY_UNPRESS = 0x00,
KEY1_PRESS = 0x01,
KEY2_PRESS = 0x02,
KEY3_PRESS = 0x04,
KEY4_PRESS = 0x08,
KEY1_2_PRESS = 0x03,
KEY2_3_PRESS = 0x06,
KEY3_4_PRESS = 0x0C,
KEY_NON_DEAL = 0xff,
}Key_State;
void scan_key();
void check_key();
#endif
key.c
#include "key.h"
#include "led.h"
#include "smg.h"
#include <stdio.h>
/**
** @brief 独立按键的函数封装
** 1. 单键的单次或连续响应
** 2. 组合键的单次或连续响应
** @author QIU
** @date 2023.08.31
**/
/*-------------------------------------------------------------------*/
// 按键状态、前状态
Key_State key_now_state, key_pre_state;
// 按键累积
u16 num = 0;
// LED速度
u16 led_speed = 10000;
/**
** @brief 按键1按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_pressed(){
smg_showChar('1', 1, false);
led_turn(6);
}
/**
** @brief 按键2按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_pressed(){
;
}
/**
** @brief 按键3按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_pressed(){
smg_showChar('3', 1, false);
led_turn(8);
}
/**
** @brief 按键4按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key4_pressed(){
u8 str[8];
num++;
sprintf(str,"%d",num/10);
smg_showString(str, 1);
}
/**
** @brief 组合键1和2按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key_1_2_pressed(){
smg_showChar('a', 1, false);
led_stream(led_speed);
}
/**
** @brief 组合键2和3按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key_2_3_pressed(){
smg_showChar('b', 1, false);
led_run(led_speed);
}
/**
** @brief 组合键3和4按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key_3_4_pressed(){
u8 str[8];
led_speed += 100;
sprintf(str,"%u",led_speed); // 无符号数
smg_showString(str, 1);
}
/**
** @brief 按键1松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_unpressed(){
;
}
/**
** @brief 按键2松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_unpressed(){
smg_showChar('2', 1, false);
led_turn(7);
}
/**
** @brief 按键3松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_unpressed(){
key3_pressed();
}
/**
** @brief 按键4松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key4_unpressed(){
smg_showChar('0', 1, false);
num = 0;
}
/**
** @brief 按键1、2松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_2_unpressed(){
;
}
/**
** @brief 按键2、3松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_3_unpressed(){
;
}
/**
** @brief 按键3、4松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_4_unpressed(){
;
}
/*-------------------------------------------------------------------*/
/**
** @brief 按键松开响应
** @param 无
** @retval 无
**/
void key_unpress(){
switch(key_pre_state){
case KEY_UNPRESS: break;
case KEY1_PRESS: key1_unpressed();break;
case KEY2_PRESS: key2_unpressed();break;
case KEY3_PRESS: key3_unpressed();break;
case KEY4_PRESS: key4_unpressed();break;
case KEY1_2_PRESS: key1_2_unpressed();break;
case KEY2_3_PRESS: key2_3_unpressed();break;
case KEY3_4_PRESS: key3_4_unpressed();break;
}
}
/**
** @brief (轮询方式)扫描独立按键,判断哪个键按下
** @param 无
** @retval 无
**/
void scan_key(){
static u8 flag = 1;
// 如果有按键按下
if(flag && (!key1||!key2||!key3||!key4)){
flag = 0; // 清零
delay_ms(10); // 延时10ms消抖
delay_ms(50); // 延时50ms 容许间隔
// 获取当前所有按下的键
if(!key1) key_now_state |= KEY1_PRESS;
if(!key2) key_now_state |= KEY2_PRESS;
if(!key3) key_now_state |= KEY3_PRESS;
if(!key4) key_now_state |= KEY4_PRESS;
// 如果按键全部松开
}else if(key1&&key2&&key3&&key4){
flag = 1;
delay_ms(10); // 延时10ms消抖,松开响应有逻辑判断时,需要加上消抖。否则可以省略。
if(key1&&key2&&key3&&key4)key_now_state = 0;
}
}
/**
** @brief (轮询方式)判断按键, 进行相应处理
** @param 无
** @retval 无
**/
void check_key(){
switch(key_now_state){
case KEY_UNPRESS:
// 松开响应
key_unpress();
key_pre_state = KEY_UNPRESS;
break;
case KEY1_PRESS:
key_pre_state = KEY1_PRESS;
// 如果是单次响应
if(!KEY1_MODE) key_now_state = KEY_NON_DEAL;
key1_pressed();
break;
case KEY2_PRESS:
key_pre_state = KEY2_PRESS;
if(!KEY2_MODE) key_now_state = KEY_NON_DEAL;
break;
case KEY3_PRESS:
key_pre_state = KEY3_PRESS;
if(!KEY3_MODE) key_now_state = KEY_NON_DEAL;
key3_pressed();
break;
case KEY4_PRESS:
key_pre_state = KEY4_PRESS;
if(!KEY4_MODE) key_now_state = KEY_NON_DEAL;
key4_pressed();
break;
case KEY1_2_PRESS:
key_pre_state = KEY1_2_PRESS;
if(!KEY_1_2_MODE) key_now_state = KEY_NON_DEAL;
key_1_2_pressed();
break;
case KEY2_3_PRESS:
key_pre_state = KEY2_3_PRESS;
if(!KEY_2_3_MODE) key_now_state = KEY_NON_DEAL;
key_2_3_pressed();
break;
case KEY3_4_PRESS:
key_pre_state = KEY3_4_PRESS;
if(!KEY_3_4_MODE) key_now_state = KEY_NON_DEAL;
key_3_4_pressed();
break;
case KEY_NON_DEAL:
// 按下不处理
break;
}
}
在多键检测中,主要更改了scan_key()
的处理逻辑,只要在50ms
内同时按下两个键,就会被记录,否则将视为单键按下。同时,按键状态的枚举值设置为包含按键信息的值,通过或运算可以得到所有按键的状态信息。需要注意的是,按下过程中会屏蔽其他按键,只有全部松开之后才再次检测。
相似的,我们也可以很容易地拓展出三键组合键乃至更多组合键实现,这可以视为多个独立按键的总体封装。
总结
按键虽小,但细节很多,本节只是初步实现按键检测,事实上,代码和实现思路还有诸多不足。在学完中断和定时器后,我们将采用最佳的方式完成按键检测。继续加油吧!