本节内容,我们学习一下物理按键的结构与控制方式。按键是一种典型的输入元器件,认识了按键,我们就可以和之前的章节所学的元器件们联动起来了。
在物理电路中,我们最熟知的一个词就是开关,那其实就是现实世界中按键(Key)的理想模型。
按键在我们日常生活中几乎无处不在:手机的音量键、电脑键盘、日光灯的开关、电视机的电源按钮…
从机械结构来说,按键分为自锁式和自复位式。两者的应用场景并不相同。
自锁式:按键按下和未按下的状态不同,对用户有引导作用。一般应用于没有明显的用户交互界面的场景,用户可以自行判断按键状态。但价格稍贵。
自复位式:按键按下和未按下的状态相同,仅从按键本身并不能判断是否(曾)被按下。一般应用于有明显的用户交互界面的场景,例如手机音量键,用户不用关注按键状态,只需观察屏幕反馈即可。
以上两种类型与按键的电气特性无关,仅仅是为了介绍全面,多啰嗦几句。本文研究的是自复位式独立按键。
对于一个独立按键,它有两种状态:按下和释放。这就意味着每次按键动作都有两个状态可以被观测,称为按下响应和释放响应。
在有些场景下,按键响应我们只希望它执行一次,例如一次点击就打开一个应用(而不是按一次就弹出来许多);在其他场景下,我们希望按住按键可以使响应连续执行,例如调节音量(否则从 0 按到 100 将相当痛苦)。这即是单次响应与连续响应。
还有一种情况,我希望在按下某个按键时不会受其他按键的干扰。这即为按键屏蔽问题。
因此,联系手机或键盘的按键操作,大致可以将按键事件分为以下几类:
另外,在真实按键中,由于按键物理结构的限制,必存在机械振动,俗称按键抖动。如何防抖,什么时候防抖是关键所在。一般可分为硬件防抖和软件防抖两种。
按键抖动是一种不可消除的机械振动形式,客观存在。其影响是,按一次可能会导致MCU执行多次。抖动时间依赖于按键的机械结构设计,越优质的结构和材料抖动时间越短。需要强调的是,无论是硬件还是软件防抖都不能过滤人为的误触(时间量级不一样)。
为防止误触,一般将比较重要的功能以长按、双击、组合键的形式设置。
在工业或是军事场景中,按键响应除了要满足高度实时的要求,还必须高度可靠,最有效的方法即检测多次。 例如,在10ms内检测10次按键状态,当全部有效时,才视为按键按下,而普通的软件延时仅仅检测了2次,显然可靠性不及前者。 在定时器章节,我们将利用这种思想实现最佳按键检测。
观察独立按键,共有四个引脚,其中两组间距较短,而另两组间距较长。间距长的两组引脚之间是连接在一起的,而短间距引脚之间初始状态是断开的,当按键被按下时,四个引脚被接通,可视作一根导线。
一般情况下,独立按键只需要接两个脚即可使用。其中一个引脚接地,另一个引脚由单片机IO口控制。
注:本节暂不讨论长按和双击事件,在后续章节中通过定时器实现。
实验效果:四个独立按键,分别控制四个LED的开关,实现不同的按键效果。数码管显示按键编号。
delay.h
#ifndef _DELAY_H_
#define _DELAY_H_
#include
#define false 0
#define true 1
typedef unsigned char u8;
typedef unsigned int u16;
void delay_10us(u16);
void delay_ms(u16);
#endif
delay.c
#include "delay.h"
/**
** @brief 通用函数
** @author QIU
** @date 2023.08.23
**/
/*-------------------------------------------------------------------*/
/**
** @brief 延时函数(10us)
** @param t:0~65535,循环一次约10us
** @retval 无
**/
void delay_10us(u16 t){
while(t--);
}
/**
** @brief 延时函数(ms)
** @param t:0~65535,单位ms
** @retval 无
**/
void delay_ms(u16 t){
while(t--){
delay_10us(100);
}
}
led.h
#ifndef _LED_H_
#define _LED_H_
#include "delay.h"
#define LED_PORT P2
void led_on(u8);
void led_turn(u8);
void led_stream(u16);
void led_run(u16);
#endif
led.c
#include "led.h"
/**
** @brief LED控制程序
** @author QIU
** @date 2023.08.23
**/
/*-------------------------------------------------------------------*/
/**
** @brief 指定某个LED亮
** @param pos: 位置(1~8)
** @retval 无
**/
void led_on(u8 pos){
LED_PORT &= ~(0x01<<(pos-1));
}
/**
** @brief 指定某个LED灭
** @param pos: 位置(1~8)
** @retval 无
**/
void led_off(u8 pos){
LED_PORT |= 0x01<<(pos-1);
}
/**
** @brief 指定位置LED翻转
** @param pos:1—8
** @retval 无
**/
void led_turn(u8 pos){
u8 port;
port = (LED_PORT>>(pos-1))&0x01;
if(port){
led_on(pos);
}else{
led_off(pos);
}
}
/**
** @brief LED流水灯
** @param time 延时时间
** @retval 无
**/
void led_stream(u16 time){
u8 i;
for(i=0;i<8;i++){
led_on(i+1);
delay_10us(time);
}
// 全部熄灭
for(i=0;i<8;i++){
led_off(i+1);
}
}
/**
** @brief LED跑马灯
** @param time 延时时间
** @retval 无
**/
void led_run(u16 time){
u8 i;
for(i=0;i<8;i++){
led_on(i+1);
delay_10us(time);
led_off(i+1);
}
}
smg.h
#ifndef _SMG_H_
#define _SMG_H_
#include "delay.h"
#define SMG_PORT P0
// 位选引脚,与38译码器相连
sbit A1 = P2^2;
sbit A2 = P2^3;
sbit A3 = P2^4;
void smg_showChar(u8,u8);
void smg_showString(u8*, u8);
#endif
smg.c
#include "smg.h"
/**
** @brief 数码管功能封装
** @author QIU
** @date 2023.08.31
**/
/*-------------------------------------------------------------------*/
//共阴极数码管字形码编码
u8 code smgduan[] = {0x3f,0x06,0x5b,0x4f,0x66, //0 1 2 3 4
0x6d,0x7d,0x07,0x7f,0x6f, //5 6 7 8 9
0x77,0x7c,0x58,0x5e,0x79, //A b c d E
0x71,0x76,0x30,0x0e,0x38, //F H I J L
0x54,0x3f,0x73,0x67,0x50, //n o p q r
0x6d,0x3e,0x3e,0x6e}; //s U v y
/**
** @brief 指定第几个数码管点亮,38译码器控制位选(不对外声明)
** @param pos:从左至右,数码管位置 1~8
** @retval 无
**/
void select_38(u8 pos){
u8 temp_pos = 8 - pos; // 0~7
A1 = temp_pos % 2; //高位
temp_pos /= 2;
A2 = temp_pos % 2;
temp_pos /= 2;
A3 = temp_pos % 2; //低位
}
/**
** @brief 解析数据并取得相应数码管字形码编码
** @param dat:想要显示的字符
** @retval 对应字形码编码值
**/
u8 parse_data(u8 dat){
switch(dat){
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:return smgduan[dat];
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':return smgduan[dat-'0'];
case 'a':
case 'A':return smgduan[10];
case 'b':
case 'B':return smgduan[11];
case 'c':
case 'C':return smgduan[12];
case 'd':
case 'D':return smgduan[13];
case 'e':
case 'E':return smgduan[14];
case 'f':
case 'F':return smgduan[15];
case 'h':
case 'H':return smgduan[16];
case 'i':
case 'I':return smgduan[17];
case 'j':
case 'J':return smgduan[18];
case 'l':
case 'L':return smgduan[19];
case 'n':
case 'N':return smgduan[20];
case 'o':
case 'O':return smgduan[21];
case 'p':
case 'P':return smgduan[22];
case 'q':
case 'Q':return smgduan[23];
case 'r':
case 'R':return smgduan[24];
case 's':
case 'S':return smgduan[25];
case 'u':
case 'U':return smgduan[26];
case 'v':
case 'V':return smgduan[27];
case 'y':
case 'Y':return smgduan[28];
default:return 0x00; //不显示
}
}
/**
** @brief 显示一个字符(1字节)
** @param dat:字符数据
** @param pos:显示位置 1~8
** @retval 无
**/
void smg_showChar(u8 dat, u8 pos){
// 解析点亮哪一个数码管
select_38(pos);
// 解析数据
SMG_PORT = parse_data(dat);
}
/**
** @brief 显示字符串(动态显示)
** @param dat[]:字符串
** @param pos:显示位置 1~8
** @retval 无
**/
void smg_showString(u8 dat[], u8 pos){
u8 i;
// 超出部分直接截断
for(i=0;(i<9-pos )&&(dat[i]!='\0');i++){
smg_showChar(dat[i], pos+i);
delay_10us(100); //延时1ms
SMG_PORT = 0x00; //消影
}
}
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"
// 按键状态、前状态
Key_State key_state, key_pre_state;
// 按键累积值,反映按键时长
u16 num = 0;
/**
** @brief 独立按键的函数封装
** @author QIU
** @date 2023.08.31
**/
/*-------------------------------------------------------------------*/
// 包含的头文件(按需求修改)
#include "led.h"
#include "smg.h"
#include
/**
** @brief 按键1按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_pressed(){
smg_showChar(1, 1);
led_turn(6);
}
/**
** @brief 按键2按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_pressed(){
;
}
/**
** @brief 按键3按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_pressed(){
smg_showChar(3, 1);
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);
led_turn(7);
}
/**
** @brief 按键3松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_unpressed(){
key3_pressed();
}
/**
** @brief 按键4松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key4_unpressed(){
smg_showChar(0, 1);
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); // 初始为0
while(1){
// 扫描按键
scan_key();
// 检查按键
check_key();
}
}
本例联动了之前篇章的LED、数码管等元器件,所以代码文件略多。
核心代码是在key.h
和key.c
中,主要的逻辑是:通过scan_key()
函数不断轮询4个独立按键状态,一旦发生改变,延时10ms消抖,再次检测,以确定是否存在按键按下或是松开。然后判断是哪一个按键被按下,记录当前状态。此后,用check_key()
轮询按键状态,并在该函数内实现各个按键所执行的逻辑。
对于一些需要松开响应的场景,应当用变量key_pre_state
记录前一个按键状态,以判断时哪一个键被松开。
对于一些需要连续响应的场景,应当用宏定义KEYx_MODE
来设置按键的响应模式。例如开灯的时候,我们只希望在按下时执行一次,而在调节音量时,我们希望在按下时能持续执行。
注意:
if .. else if
语句具有向下屏蔽的特点。在此例中,按键1会屏蔽按键2~4的响应,即按下按键1时,按下其他的按键并不响应。实验效果:
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"
// 按键状态、前状态
Key_State key_now_state, key_pre_state;
// 按键累积
u16 num = 0;
// LED速度
u16 time = 10000;
/**
** @brief 独立按键的函数封装
** 1. 单键的单次或连续响应
** 2. 组合键的单次或连续响应
** @author QIU
** @date 2023.08.31
**/
/*-------------------------------------------------------------------*/
#include "led.h"
#include "smg.h"
#include
/**
** @brief 按键1按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_pressed(){
smg_showChar(1, 1);
led_turn(6);
}
/**
** @brief 按键2按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_pressed(){
;
}
/**
** @brief 按键3按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_pressed(){
smg_showChar(3, 1);
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(){
led_stream(time);
}
/**
** @brief 组合键2和3按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key_2_3_pressed(){
led_run(time);
}
/**
** @brief 组合键3和4按下函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key_3_4_pressed(){
u8 str[8];
time += 100;
sprintf(str,"%u",time); // 无符号数
smg_showString(str, 1);
}
/**
** @brief 按键1松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key1_unpressed(){
;
}
/**
** @brief 按键2松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key2_unpressed(){
smg_showChar(2, 1);
led_turn(7);
}
/**
** @brief 按键3松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key3_unpressed(){
key3_pressed();
}
/**
** @brief 按键4松开函数(可以按需求修改)
** @param 无
** @retval 无
**/
void key4_unpressed(){
smg_showChar(0, 1);
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()
的处理逻辑,可以处理组合键事件。
按键虽小,但细节很多,本节只是初步实现按键检测,事实上,代码和实现思路还有诸多不足。在学完中断和定时器后,我们将采用最佳的方式完成按键检测。继续加油吧!