【51单片机实验笔记】开关篇(一) 独立按键


前言

本节内容,我们学习一下物理按键结构控制方式按键是一种典型的输入元器件,认识了按键,我们就可以和之前的章节所学的元器件们联动起来了。

本节涉及到的封装源文件可在《模块功能封装汇总》中找到。

本节完整工程文件已上传GitHub仓库地址,欢迎下载交流!


硬件介绍

在物理电路中,我们最熟知的一个词就是开关,那其实就是现实世界中按键(Key)理想模型

按键在我们日常生活中几乎无处不在:手机的音量键电脑键盘日光灯的开关电视机的电源按钮…


自锁与自复位

机械结构来说,按键分为自锁式自复位式。两者的应用场景并不相同。

  • 自锁式:按键按下未按下状态不同,对用户有引导作用。一般应用于没有明显的用户交互界面的场景,用户可以自行判断按键状态。但价格稍贵。

  • 自复位式:按键按下未按下状态相同,仅从按键本身并不能判断是否()被按下。一般应用于有明显的用户交互界面的场景,例如手机音量键,用户不用关注按键状态,只需观察屏幕反馈即可。

以上两种类型与按键电气特性无关,仅仅是为了介绍全面,多啰嗦几句。本文研究的是自复位式独立按键


按键响应事件

对于一个独立按键,它有两种状态:按下释放。这就意味着每次按键动作都有两个状态可以被观测,称为按下响应释放响应

在有些场景下,按键响应我们只希望它执行一次,例如一次点击就打开一个应用(而不是按一次就弹出来许多);在其他场景下,我们希望按住按键可以使响应连续执行,例如调节音量(否则从 0 按到 100 将相当痛苦)。这即是单次响应连续响应

还有一种情况,我希望在按下某个按键时不会受其他按键的干扰。这即为按键屏蔽问题。

因此,联系手机或键盘的按键操作,大致可以将按键事件分为以下几类:
在这里插入图片描述

  • 短按: 计算按下响应释放响应时间间隔 t,若小于设定的阈值,则视为短按事件
    1. 按下立刻执行(无论释放与否)
    2. 释放立刻执行(按下没有反应)
    3. 按下执行,释放也执行
  • 长按:大于设定的阈值,则视为长按事件
    • 单次执行:如手机关机
    • 连续执行:如调节音量
  • 双击:同个按键两次按下响应的时间间隔小于设定的阈值,则视为双击事件
    • 必是两个短按事件,如鼠标双击打开应用
  • 组合键:两个不同的按键按下响应的时间间隔小于设定的阈值,则视为组合键事件
    • 短按:如截屏
    • 长按:如强制重启
  • 粘滞键组合键触发方式比较苛刻手速比较慢的人可能难以成功按出组合键,因此出现了粘滞键。它允许先按下一个键,再按下另一个键实现组合。
    • 按键顺序有所要求,首先按下的键不应有响应事件,而且只有等全部松开时才开始新一轮检测。比如键盘中的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.hkey.c中,采用了状态机的思想,将按键事件分为6个状态

  1. KEY_UNPRESS:未有按键按下
  2. KEY1_PRESS:按键1按下
  3. KEY2_PRESS:按键2按下
  4. KEY3_PRESS:按键3按下
  5. KEY4_PRESS:按键4按下
  6. 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内同时按下两个键,就会被记录,否则将视为单键按下。同时,按键状态枚举值设置为包含按键信息的值,通过或运算可以得到所有按键状态信息。需要注意的是,按下过程中会屏蔽其他按键,只有全部松开之后才再次检测。

相似的,我们也可以很容易地拓展出三键组合键乃至更多组合键实现,这可以视为多个独立按键总体封装


总结

按键虽小,但细节很多,本节只是初步实现按键检测,事实上,代码实现思路还有诸多不足。在学完中断定时器后,我们将采用最佳的方式完成按键检测。继续加油吧!

  • 3
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬铃木下的青春

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值