Bootstrap

三行代码按键消抖 独立按键 矩阵按键 长按 短按 双击

九层妖塔 起于垒土

在这里插入图片描述


直接跳转到三行代码




改编自国信长天蓝桥杯官方蓝皮书例程,按照自己的习惯进行了补充和修改


链接
1、独立按键抖动产生原因;普通软件延时消抖;标志位软件延时消抖;标志位定时器延时消抖
2、矩阵键盘的识别与编码;查询扫描;定时扫描;中断扫描;使用定时器状态机的方法进行按键的扫描识别
3、定时器三行代码 矩阵键盘长短按的识别



一、基本理论

0、按键的常见名词:

 ①按键抖动

 当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动。抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。

在这里插入图片描述
尝试用示波器的触发捕获按键抖动波形,只捕获到了比较明显的松手抖动。
在这里插入图片描述
下图是串口返回的IO口内部数学电平(不知道是采样频率的问题还是按键的问题,并没有观察到方波)
在这里插入图片描述

 ②按键稳定闭合时间

按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为100ms 以上至数秒。

在这里插入图片描述

完整的按键识别分为两部分:按键的扫描按键的消抖

1、按键的扫描:

 按键的扫描就是通过程序判断与按键相连的IO口的电平状态。通过相关IO口的电平状态来判断按键的抬起和按下。
①查询扫描
  把按键扫描子程序和其他子程序并列在一起,单片机循环分时运行各个子程序。当按键按下并且单片机查询到的时候立即响应键盘输入操作。
②定时扫描
  定时器,灵活多样,方法较多。
③中断扫描
  依赖于硬件(如四路与门等),利用外部中断。

2、按键的消抖:

 由于上文所说的 按键抖动 ,需要对IO口电平进行消抖处理。消抖分为硬件消抖和软件消抖。常见的硬件消抖有 使用电容进行延时消抖 、S触发器进行硬件去抖等。
 在此旨在介绍全面的软件消抖算法。



二、常见的按键消抖算法

1、普通软件延时

 ●消抖:软件延时。CPU在按键按下抖动时进行10ms的软件延时消抖,在数百ms的稳定期以及松手抖动时停滞在松手检测while(!KEY)中,占用CPU资源过多;也就是在整个按键检测期间内,没有再执行其他指令。
 ●松手检查:while(!KEY)等待按键释放。 等按键确认释放后再去执行相应代码。防止重入。
 ●按键检测时是借助while(1)死循环进行反复检测。
 ●若是采用普通软件延时消抖10ms的软件延时和松手检测都会占用CPU资源,对其它同样借助while(1)死循环进行反复运行的模块代码产生了挤占影响,比如动态数码管。
 ●稳定期取决于是否松手,若不松手,则数码管将一直保持进入按键检测前最后一次对数码管的操作。

最低级的方法,没有实用性。
 ●流程图:
在这里插入图片描述
 ●示例代码

	while(1)
	{
		if(key==0)
		{
			Delay10ms();
			if(key==0)
			{
              //按键按下执行相关功能代码                   
			  while(!key);
			}
		}
	}

2、标志位软件延时

 ●消抖:软件延时
 ●通过引入按下标志位,省去了while(!KEY)的松手检测,将按下时稳定期的数百ms释放出来,只占用了按下和松手时的共20ms的消抖时间。

标志位的使用使算法具有了一定的实用性,优点是不借助硬件资源
 ●波形图:
在这里插入图片描述


3、标志位定时器延时

 ●消抖:定时器延时。使用定时器进行延时消抖。
 ●使用标志位代替松手检测。

文前链接中有解释和程序,写的不太成熟,与状态机类似

4、状态机

 也是一种利用标志位定时器延时按键算法,但比较成熟,逻辑严谨。

状态机进行按键的扫描识别

bug最少,算法成熟,但程序量比较大,不太方便移植。


5、三行代码

目前已知最好的按键算法,将按键的识别与扫描完全分开,便于移植,拓展性好。



三行代码

完整的按键程序可以包括:按键的扫描按键的消抖 两部分。
三行代码 属于 按键的消抖 部分的算法,与是独立按键还是矩阵键盘无关。
三行代码 其实就是一种时域滤波算法–消抖滤波法。

1、按键的扫描

按键的扫描 采用定时器进行扫描。10ms扫描一次IO口电平状态,即10ms读取一次键值。通过Key_Read()函数读取。

unsigned char Key_Read(void)
{
  Key_Value = P01; //任意一个IO口的电平值保存在变量中
}

①独立按键的扫描

基于蓝桥杯单片机开发板

uchar Key4_Read(void)    //独立按键扫描函数,读取键值
{
    uchar Key_temp;
	uchar Key_Value;
	
	P3 |= 0x0f;
	Key_temp = P3&0x0f;
	
	switch(Key_temp)
	{
	    case 0x0e : Key_Value = 7; break;  //S7
		case 0x0d : Key_Value = 6; break;  //S6
		case 0x0b : Key_Value = 5; break;  //S5
		case 0x07 : Key_Value = 4; break;  //S4
		default: Key_Value = 0;
	}
	
	return Key_Value;
}

Notes:
 ●程序中P3 |= 0x0f; Key_temp = P3&0x0f;两行属于按键扫描的识别部分
 ●switch属于按键扫描的编码部分

 ●对程序中P3 |= 0x0f; Key_temp = P3&0x0f;两行的解释:
  ○P3 |= 0x0f;P3低四位即独立按键接口拉高置为1;Key_temp = P3&0x0f;读取P3低四位I/O口的电平。
  ○重点是读取I/O口外部电平状态前,需要先将I/O口置为1。 STC15的芯片手册中有介绍:STC15F2系列单片机I/O口上电默认为准双向口,在324页有相关的介绍和使用说明。截去一段关于读取外部状态的使用说明:
在这里插入图片描述

再来看国信长天给的电子版例程:
在这里插入图片描述
emmm……似乎不太严谨…………

①矩阵键盘的扫描

基于蓝桥杯单片机开发板

uchar Key16_Read(void)    //矩阵按键扫描函数,读取键值
{
    uint  Key_temp;
	uchar Key_Value;
	
	
	//按键扫描的识别部分
	P44=0; P42=1; P35=1; P34=1; P3|=0X0F; //第一列拉低,其余全为高
	Key_temp = P3;                        //读取P3低四位I/O状态
	
	P44=1; P42=0; P35=1; P34=1; P3|=0X0F;  //第二列拉低,其余全为高
	Key_temp = (Key_temp<<4) | (P3&0X0F);  //读取P3低四位I/O状态
	
	P44=1; P42=1; P35=0; P34=1; P3|=0X0F; //第三列拉低,其余全为高
	Key_temp = (Key_temp<<4) | (P3&0X0F); //读取P3低四位I/O状态
	
	P44=1; P42=1; P35=1; P34=0; P3|=0X0F; //第四列拉低,其余全为高
	Key_temp = (Key_temp<<4) | (P3&0X0F);  //读取P3低四位I/O状态
	
	//有按键按下Key_temp相应位为0	
	
	
	//按键扫描的编码部分
	switch(~Key_temp)   
	{
		case 0X8000: Key_Value = 4; break;   //S4
		case 0X4000: Key_Value = 5; break;   //S5
		case 0X2000: Key_Value = 6; break;   //S6
		case 0X1000: Key_Value = 7; break;   //S7
		
		case 0X0800: Key_Value = 8; break;   //S8
		case 0X0400: Key_Value = 9; break;   //S9
		case 0X0200: Key_Value = 10; break;   //S10
		case 0X0100: Key_Value = 11; break;   //S11
		
		case 0X0080: Key_Value = 12; break;   //S12
		case 0X0040: Key_Value = 13; break;   //S13
		case 0X0020: Key_Value = 14; break;   //S14
		case 0X0010: Key_Value = 15; break;   //S15
		
		case 0X0008: Key_Value = 16; break;   //S16
		case 0X0004: Key_Value = 17; break;   //S17
		case 0X0002: Key_Value = 18; break;   //S18
		case 0X0001: Key_Value = 19; break;   //S19
		
		default: Key_Value = 0;  //无按键按下
	}
	return Key_Value;
	
}

Notes:
 ●依旧是因为准双向口读取I/O口外部电平状态前,需要先将I/O口置为1,所以将国信长天蓝色指导书上的 按键扫描的识别部分改成了上边的样子。

指导书上的程序
在这里插入图片描述

  按键的扫描已经介绍完了,按键扫描函数Key_Read()放在定时器中进行扫描,10ms执行一次,即10ms读取一次I/O状态,进行一次键值编码。
  可以知道按键扫描函数Key_Read()10ms一次读取到的键值是 瞬态的,只能反映按键此刻的状态,无法反映按下按键抬起按键的稳定过程。


2、按键的消抖

直接上代码(三行代码):

uchar Key_Temp,Key_Down,Key_Up;
static uchar Key_Old = 0;

Key_Temp = Key_Read();
Key_Down = Key_Temp & (Key_Old ^ Key_Temp);   //按下为按键值,其它为0
Key_Old = Key_Temp;
	

Notes:
 ●三行代码第一行:Key_Temp = Key_Read(); 读取10ms更新一次的I/O电平状态,并存储在变量Key_Temp,可以理解为临时按键值
 ●三行代码第三行:Key_Old = Key_Temp;Key_Old为静态局部变量,离开函数,值仍保留:数据存储在静态存储区,在程序整个运行期间都不释放,且只能在该函数中调用。将这次读取到的临时按键值Key_Temp更新到Key_Old中,作为下一次的旧的按键值;概括说Key_TempKey_Old为相差10ms的临时按键值。
 ●三行代码第二行:Key_Down = Key_Temp & (Key_Old ^ Key_Temp);两个位操作:按位与,按位异或。
  ○首先Key_Old ^ Key_Temp位操作针对二进制,二进制与十进制一 一对应。按位异或:相同为0,不同为1。由下图可知:Key_OldKey_Temp可能出现的情况:(假设按下的是按键4)
  Key_Old=0, Key_Temp=0 未按下 。Key_Old ^ Key_Temp=0
  Key_Old=0, Key_Temp=4 按下过程中。Key_Old ^ Key_Temp=0100=4
  Key_Old=4, Key_Temp=4 按下稳定期间。 Key_Old ^ Key_Temp=0000
  Key_Old=4, Key_Temp=0 抬起过程中。 Key_Old ^ Key_Temp=0100=4
再次说明由于是用定时器扫描,Key_OldKey_Temp的按键值相差10ms,不可能出现Key_Old=4, Key_Temp=6 两个按键值的情况。

  ○Key_Old ^ Key_Temp的运算结果再&Key_Temp

Key_OldKey_Temp对应的按键过程Key_Old ^ Key_TempKey_Down
00未按下00
04按下过程中0100 (4)4
44按下稳定期间00
40抬起过程中0100 (4)0

所以由上表可知:三行代码第二行Key_Down = Key_Temp & (Key_Old ^ Key_Temp); 最后的运算结果Key_Down只有在按键按下的过程中为按键值,持续时间大约10ms。

 ●可以在原有三行代码的基础上再增加一行,来判断按键抬起的过程。

Key_Up  = ~Key_Temp & (Key_Old ^ Key_Temp);  //松手为抬起前的按键值,其他为0 

Key_DownKey_Up识别的分别是按键按下和抬起时的下降沿和上升沿

在这里插入图片描述


3、相关变量值的变化

 由以上分析可知:

按键过程Key_DownKey_Up
未按下00
按下过程中相应的按键值0
按下稳定期间00
抬起过程中0抬起前的按键值

 所以可以将Key_DownKey_Up理解为临时值,只在按下或抬起过程中不为0,又 按键的扫描 Key_Read() 采用定时器进行扫描。10ms扫描一次,数据10ms更新一次。临时值Key_DownKey_Up为局部变量,生存期只在子函数中,只能在按键处理函数Key_Proc()内部使用。


4、按键处理函数

①独立按键的处理函数

//-----------------------------------------独立按键处理函数---------------//
void Key4_Proc(void)     //独立按键处理函数
{
    uchar Key_Temp,Key_Down,Key_Up;
    static uchar Key_Old = 0;
	
	if(uc_Key_flag) return;  //10ms
	uc_Key_flag = 1;
	
	Key_Temp = Key4_Read();
	Key_Down = Key_Temp & (Key_Old ^ Key_Temp);   //按下为按键值,其它为0
	Key_Up  = ~Key_Temp & (Key_Old ^ Key_Temp);  //松手为抬起前的按键值,其他为0 
	Key_Old = Key_Temp;
	
		
	if(Key_Down)
	{
		uc_Key_state = 'd';
        uc_Key_Value = Key_Down;
	}
 
	if(Key_Up)
	{
		uc_Key_state = 'U';
	}
}

②矩阵键盘的处理函数

void Key16_Proc(void)     //矩阵按键处理函数	
{
    uchar Key_Value,Key_Down,Key_Up;
    static uchar Key_Old = 0;
	
	if(uc_Key_flag) return;  //10ms
	uc_Key_flag = 1;
	
	Key_Value = Key16_Read();
	Key_Down = Key_Value & (Key_Old ^ Key_Value);   //按下为按键值,其它为0
	Key_Up  = ~Key_Value & (Key_Old ^ Key_Value);  //松手为抬起前的按键值,其他为0 
	Key_Old = Key_Value;

	if(Key_Down)
	{
	  uc_Key_state = 'd';
	  uc_Key_Value = Key_Down;

	}
 
	if(Key_Up)
	{
	  uc_Key_state = 'U';
      uc_Key_Value = Key_Up;
	}
}

Notes:
 ●观察独立按键/矩阵键盘的处理函数可知,定义了两个全局变量uc_Key_Valueuc_Key_state来分别向全局传递按键值和按键状态。
 ●uc_Key_Valueuc_Key_state对比临时值Key_DownKey_Up,可以理解为状态值,10ms刷新一次,可以在全局中传递。


5、长按

长按一

 顾名思义,长按 区别于 短按 在于时间,又因为STC15没有实时时钟,只能通过定时器来搭建全局时间。用定时器1作为1ms定时器,用作程序内的ms计时。

创建全局变量ul_ms,用作ms计时。

unsigned long ul_ms;

在定时器1的中断服务函数或者自定义的查询服务函数中:

ul_ms++;  //ms计时

按键长按短按的实现只需在按键处理函数中增加代码:

//-----------------------------------------独立按键处理函数---------------//
void Key4_Proc(void)     //独立按键处理函数
{
    uchar Key_Temp,Key_Down,Key_Up;
    static uchar Key_Old = 0;
	static ulong ul_Key_Time_Down = 0;
	static ulong ul_Key_Time_Up = 0;
	
	if(uc_Key_flag) return;  //10ms
	uc_Key_flag = 1;
	
	Key_Temp = Key4_Read();
	Key_Down = Key_Temp & (Key_Old ^ Key_Temp);   //按下为按键值,其它为0
	Key_Up  = ~Key_Temp & (Key_Old ^ Key_Temp);  //松手为抬起前的按键值,其他为0 
	Key_Old = Key_Temp;
	
		
	if(Key_Down)
	{				
		uc_Key_state = 'd';
		
		ul_Key_Time_Down = ul_ms;  //记录按键按下时的时刻
		
	}
 
	if(Key_Up)
	{
		uc_Key_state = 'U';

		ul_Key_Time_Up = ul_ms;  //记录按键抬起时的时刻
		
		switch(Key_Up)
		{
			case 7 : uc_Key_Value = 7; break;
			case 6 : uc_Key_Value = 6; break;
			case 5 : uc_Key_Value = 5; break;
			case 4 : uc_Key_Value = 4; break;
		}
		
		//长按判断  
		if(ul_Key_Time_Up - ul_Key_Time_Down >1000)
		{
		  uc_Key_Value = uc_Key_Value +10;
		}
	}

}

Notes:
 ●有长按的要求,按键的功能最好放在按键抬起时刻。即在按键抬起时刻相应事件再响应。

长按二

  static ulong ul_Key4_Presstime = 0;   //计时按键按下状态的时间
  static bit Key4_Down_flag = 0;        //按按下标志位
  static bit Key4_Up_Enable = 0;        //抬起触发使能标志位

	if(Key_Down) //下降沿检测
	{	
		switch(Key_Down)
		{
		  case 4:
			{
			  ul_Key4_Presstime = ul_time;  //在按键按下的时刻 开始记录时间
				Key4_Down_flag = 1;   //按下标志位 置1
				Key4_Up_Enable = 1;   //使能抬起标志位
			}break;
			…………
	   }
	 }

	if(Key_Up)  //上升沿检测
	{
		switch(Key_Up)
		{
		  case 4:
			{
				if(Key4_Up_Enable == 1)  //抬起标志位已经使能
				{
					Key4_Down_flag = 0;  //清除按下标志位
					Key4_Up_Enable = 0;  //清除抬起标志位
					
					uc_Key_Value = 40;  //短按功能
				}
		    }
    }  


	if( (( ul_time - ul_Key4_Presstime)>1000) && (Key4_Down_flag == 1) && (Key_Old == 4) && (ul_Key4_Presstime != 0))
	{
	    ul_Key4_Presstime = ul_time;
		//ul_Key4_Presstime = 0;
		Key4_Up_Enable = 0;
		
	    uc_Key_Value += 1;
	}

6、双击

//-----------------------------------------独立按键处理函数---------------//
void Key4_Proc(void)     //独立按键处理函数
{
    uchar Key_Temp,Key_Down,Key_Up;
    static uchar Key_Old = 0;
	
	static ulong ul_Key_Time_Down_First = 0;  //记录按键 第一次 按下时的时刻
	static ulong ul_Key_Time_Down_Second = 0; //记录按键 第二次 按下时的时刻
	static uchar Key_First = 0;   //记录按键 第一次 按键值
    static uchar Key_Second = 0;  //记录按键 第二次 按键值
	
	if(uc_Key_flag) return;  //10ms
	uc_Key_flag = 1;
	
	Key_Temp = Key4_Read();
	Key_Down = Key_Temp & (Key_Old ^ Key_Temp);   //按下为按键值,其它为0
	Key_Up  = ~Key_Temp & (Key_Old ^ Key_Temp);  //松手为抬起前的按键值,其他为0 
	Key_Old = Key_Temp;
	
		
	if(Key_Down)
	{				
		uc_Key_state = 'd';
		
		if( (Key_First == 0) && (Key_Second == 0) )  //未有按键按下
		{
		  ul_Key_Time_Down_First = ul_ms;  //记录按键 第一次 按下时的时刻
		  Key_First = Key_Down;  //记录按键 第一次 按键值
			
		}
		else if( (Key_First != 0) && (Key_Second == 0) )  //已有一次按键按下,此次是第二次
		{
		  ul_Key_Time_Down_Second = ul_ms;  //记录按键 第二次 按下时的时刻
		  Key_Second = Key_Down;  //记录按键 第二次 按键值
				
		}
		
	}
 
	if(Key_Up)
	{
		uc_Key_state = 'U';
	}
   
	
	//双击、单击的判断
	 if( (Key_First != 0) && (ul_ms - ul_Key_Time_Down_First >500) )  //有第一次按键按下,并且时间>500ms
		{
		  if(Key_Second == 0)    //第二次没有按下, 单击
			{
			 		switch(Key_First)
							{
								case 7 : uc_Key_Value = 7; break;
								case 6 : uc_Key_Value = 6; break;
								case 5 : uc_Key_Value = 5; break;
								case 4 : uc_Key_Value = 4; break;
							}
				 
							Key_First = 0;Key_Second = 0;  //清零
			}
			else if(Key_Second == Key_First)  //第二次有按下,且和第一次相等 ,双击
			{
              uc_Key_Value = Key_Second +10;
			  Key_First = 0;Key_Second = 0; //清零
			}
			else if( (Key_Second != 0) && (Key_Second != Key_First) )  //第二次有按下,但和第一次不相等 ,错误
			{
			    uc_Key_Value = 'E';
				Key_First = 0;Key_Second = 0; //清零
			}
			
		}
	
}

Notes:
 ●按键双击的判断周期设为500ms,即按键500ms响应一次,判断是单击、双击还是错误。


7、复合按键

 类似于Ctrl+CCtrl+V 在长按一个的情况下,再单击另一个。
复合按键 是在按键扫描函数中进行修改。

uchar Key4_Read(void)    //独立按键扫描函数,读取键值
{
    uchar Key_temp;
	uchar Key_Value;
	
	P3 |= 0x0f;
	Key_temp = P3&0x0f;
	
	switch(Key_temp)
	{
	    case 0x0e : Key_Value = 7; break;  //S7
		case 0x0d : Key_Value = 6; break;  //S6
		case 0x0b : Key_Value = 5; break;  //S5
		case 0x07 : Key_Value = 4; break;  //S4
		
		 //S4的复合按键部分
	    case 0x06 : Key_Value = 47; break;  //S4 + S7
		case 0x05 : Key_Value = 46; break;  //S4 + S6
		case 0x03 : Key_Value = 45; break;  //S4 + S5
		
		default: Key_Value = 0;
	}
	
	return Key_Value;
}

  
  
  
  
彩 蛋

;