三行代码按键消抖 独立按键 矩阵按键 长按 短按 双击
● 改编自国信长天蓝桥杯官方蓝皮书例程,按照自己的习惯进行了补充和修改
链接
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_Temp
与Key_Old
为相差10ms的临时按键值。
●三行代码第二行:Key_Down = Key_Temp & (Key_Old ^ Key_Temp);
两个位操作:按位与,按位异或。
○首先Key_Old ^ Key_Temp
位操作针对二进制,二进制与十进制一 一对应。按位异或:相同为0,不同为1。由下图可知:Key_Old
与Key_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_Old
与Key_Temp
的按键值相差10ms,不可能出现Key_Old=4
, Key_Temp=6
两个按键值的情况。
○Key_Old ^ Key_Temp
的运算结果再&
上Key_Temp
Key_Old | Key_Temp | 对应的按键过程 | Key_Old ^ Key_Temp | Key_Down |
---|---|---|---|---|
0 | 0 | 未按下 | 0 | 0 |
0 | 4 | 按下过程中 | 0100 (4) | 4 |
4 | 4 | 按下稳定期间 | 0 | 0 |
4 | 0 | 抬起过程中 | 0100 (4) | 0 |
所以由上表可知:三行代码第二行Key_Down = Key_Temp & (Key_Old ^ Key_Temp);
最后的运算结果Key_Down
只有在按键按下的过程中为按键值,持续时间大约10ms。
●可以在原有三行代码的基础上再增加一行,来判断按键抬起的过程。
Key_Up = ~Key_Temp & (Key_Old ^ Key_Temp); //松手为抬起前的按键值,其他为0
Key_Down
和Key_Up
识别的分别是按键按下和抬起时的下降沿和上升沿
3、相关变量值的变化
由以上分析可知:
按键过程 | Key_Down | Key_Up |
---|---|---|
未按下 | 0 | 0 |
按下过程中 | 相应的按键值 | 0 |
按下稳定期间 | 0 | 0 |
抬起过程中 | 0 | 抬起前的按键值 |
所以可以将Key_Down
与Key_Up
理解为临时值,只在按下或抬起过程中不为0,又 按键的扫描 Key_Read()
采用定时器进行扫描。10ms扫描一次,数据10ms更新一次。临时值Key_Down
与Key_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_Value
和uc_Key_state
来分别向全局传递按键值和按键状态。
●uc_Key_Value
和uc_Key_state
对比临时值Key_Down
与Key_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+C 和 Ctrl+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;
}