# 前言 首先,我先给大家说一下按键抖动的原因和原理是什么。 ## 原因 通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。形成以下的波形 ![1.png][1] ![2.png][2] ## 原理 按键抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。键抖动会引起一次按键被误读多次。为确保单片机对按键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到按键释放到稳定状态后再去作处理。 # 消除按键抖动的方法 一般的,我们主要的消除按键抖动的方法主要有1、软件消抖 2、硬件消抖,当然,我们一般都是采用软件消抖。而软件消抖又分为延迟消抖和状态机消抖,下面我给大家详细讲解一下这几种消抖方式,并且,也会把延迟消抖和状态机消抖做一个比较。 # 硬件消抖 利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。在按键的两端并联一个0.1uf的电容。如按键消抖硬件图 ![3.png][3] #软件延迟消抖 对于一下单片机的新手来说,延迟消抖是采用最多的。大致的原理就是当有按键按下的时候,我们加一段10ms的延迟,让他避开这个抖动的时间,然后我们再来读取键值就非常准确了。 ``` if(按键是否按下) { //按键按下的条件下 Deadly_1ms(5); // 延时消去抖动 if(按键是否按下) { //按键再次确认按下 //执行按键功能 while(按键是否松开); } } ``` 这种方法很容易理解,但是执行效率不高,比较占用单片机的资源,因为当你在做延迟处理的时候单片机没法去做其他的事情,所以,在实际开发的时候,我们一般不会采取这种方法,所以就有了状态机消抖这个方法。 # 状态机消抖 ## 何为状态机 关于状态机的一个极度确切的描述是它是一个有向图形,由一组节点和一组相应的转移函数组成,状态机通过响应一系列事件而“运行”。每个事件都在属于“当前”节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态,状态机停止。 状态机是一种概念性机器,它能采取某种操作来响应一个外部事件。具体采取的操作不仅能取决于接收到的事件,还能取决于各个事件的相对发生顺序。之所以能做到这一点,是因为机器能跟踪一个内部状态,它会在收到事件后进行更新。为一个事件而响应的行动不仅取决于事件本身,还取决于机器的内部状态。另外,采取 的行动还会决定并更新机器的状态。这样一来,任何逻辑都可建模成一系列事件/状态组合。 状态机是软件编程中的一个重要概念。比如在一个按键命令解析程序中,就可以看做状态机,其过程如下:本来在A状态下,触发一个按键后切换到B,再触发另一个键后就切换到C状态,或者返回A状态。这是最简单的例子。其他的很多的程序都可以当做状态机来处理。 状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机内在因果关系的考虑。“现态”和“条件”是因,“动作”和“次态”是果。详细如下: 现态:是指当前所处的状态。 条件:又称为“事件”。当一个条件满足,将会触发一个动作,或者执行一次状态的迁移。 动作:条件满足后执行动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变为新的“现态”了。 按键的状态机实现 一个按键从键按下到松开的过程如下如所示。从图中可以看出,按键的按下和松开的过程都有抖动的干扰问题,因此要将它们消除。 可将将按键抽象为4个状态: (1) 未按下,假定为S0 (2) 确认有键按下,假定为S1 (3) 键稳定按下状态,假定为S2 (4) 键释放状态,假定为S3。 (有时也可以抽象为3个状态S0,S1,S3)。 在一个系统中按键的操作是随机的,因此系统软件中要对按键进行循环查询。在按键检测过程中需要进行消抖处理,消抖的延时处理一般要10ms或20ms,因此取状态机的时间序列为10或20ms,这样不仅可以跳过按键消抖的影响,同事也远小于按键0.3-0.5S的稳定闭合其,不会将按键过程丢失。 假定键按下时端口电平为0,未按下时为1(或者相反)。通过状态机实现按键检测的过程如下: 首先,按键的初始态为S0,当检测到输入为1时,表示没有键按下,保持S0。当按键输入为0时,则有键按下,转入状态S1。 在S1状态时,如果输入的信号为1,则表示刚才的按键操作为干扰,则状态跳转到S0;如果输入信号为0,则表示确实有键按下,此时可以读取键状态,产生相应的按键标志或者将该事件存入消息队列。同时状态机切换到S2状态。 在S2状态,如果输入信号为1,则没有键按下,切换到S3;如果输入信号为0,则保持S2状态,并进行计数。如果计数值超过一定的门限值,则可以认为该按键为长按键事件或者键一直按下状态,如果未超过门限值,则认为是短按键事件,保持S2状态。 在S3状态,如果输入信号为高电平,则切换到S0. 代码如下: ``` #include #include #define uint unsigned int #define uchar unsigned char #define Key_state_0 0 //状态0 等待按键按下 #define Key_state_1 1 //状态1 判断是否为抖动,并且判断键值 #define Key_state_2 2 //状态3 等待按键松开 #define Key_input P3 //输出口 uchar code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e}; //数码管段码 /***********延迟函数***********/ void Delay1ms(uint timee) //@11.0592MHz { unsigned char i, j; for(;timee > 0; timee --) { _nop_(); _nop_(); _nop_(); i = 11; j = 190; do { while (--j); } while (--i); } } /***********定时器初始化函数****/ void Init_Timer0() { TMOD = 0x01; //定时器0设置模式为1 TH0 = (65535-10000)/256; //定时器0赋初值,定时时间为10000us TL0 = (65535-10000)%256; EA = 1; //开总中断 ET0 = 1; //开定时器0中断 TR0 = 1; //开始计时 } /***********138译码器选择******/ void HC_138(uchar flag,datt) //P0口输出选择 { switch(flag) { case 0:P2 = P2 & 0x1f;break; case 1:P2 = (P2&0x1f)|0x2f;break; case 2:P2 = (P2&0x1f)|0x4f;break; case 3:P2 = (P2&0x1f)|0x6f;break; case 4:P2 = (P2&0x1f)|0x8f;break; case 5:P2 = (P2&0x1f)|0xaf;break; case 6:P2 = (P2&0x1f)|0xcf;break; case 7:P2 = (P2&0x1f)|0xef;break; } P0 = datt; 送入数据 } /***********键盘识别***********/ uchar Key_value; //全局变量--存放键值 void Key_identify() { static uchar Key_state = 0; //存储当前状态 uchar Key_press1,Key_press2,Key_press; //存放从端口读取的值 Key_input = 0x0f; //拉低高4位 Key_press1 = Key_input&0x0f; //读取值 Key_input = 0xf0; //拉低低4位 Key_press2 = Key_input&0xf0; //读取值 Key_press = Key_press1 | Key_press2; //获取综合值 switch(Key_state) //判断状态 { case Key_state_0: if(Key_press != 0xff) //当有按键按下则跳转到状态1 { Key_state = Key_state_1; } break; case Key_state_1: if(Key_press != 0xff) //再次判断是否有按键按下 { switch(Key_press) //根据读取的数据判断是哪个按键按下 { case 0x7e:Key_value = 0;break; case 0xbe:Key_value = 1;break; case 0xde:Key_value = 2;break; case 0xee:Key_value = 3;break; case 0x7d:Key_value = 4;break; case 0xbd:Key_value = 5;break; case 0xdd:Key_value = 6;break; case 0xed:Key_value = 7;break; case 0x7b:Key_value = 8;break; case 0xbb:Key_value = 9;break; case 0xdb:Key_value = 10;break; case 0xeb:Key_value = 11;break; case 0x77:Key_value = 12;break; case 0xb7:Key_value = 13;break; case 0xd7:Key_value = 14;break; case 0xe7:Key_value = 15;break; } Key_state = Key_state_2; //判断完成后跳转到状态2 } else {Key_state = Key_state_0;break;} //如果没有按键按下,则跳转到状态0 break; case Key_state_2: if(Key_press == 0xff) //判断按键是否弹起,如果没有则退出,如果弹起则返回状态0 Key_state = Key_state_0; break; } } /***********数码管子程序*******/ void Digital_tube_dispaly(uchar x) { HC_138(6,0xff); //打开所有位选 Delay1ms(1); //延迟1ms HC_138(7,x); //送入段码 } /************主程序************/ void main() { Init_Timer0(); //初始化定时器0 HC_138(5,0x00); //关闭蜂鸣器和继电器 while(1) { Digital_tube_dispaly(*(table+Key_value)); //循环点亮数码管,并且送入键值 } } void timer0()interrupt 1 { TH0 = (65535-10000)/256; //定时器0重新赋初值 TL0 = (65535-10000)%256; Key_identify(); //键盘读取 } ``` 在定时器中,定时10ms,定时到后在中断服务程序中调用上述函数,每次执行的间隔10ms,可以有效的消除消抖,提高CPU的利用率。 同时可以将状态机应用于其他的程序中,一个串行通信的时序(不管它是遵循何种协议,标准串口也好、I2C也好;也不管它是有线的、还是红外的、无线的)也都可以看做由一系列有限的状态构成。显示扫描程序也是状态机;通信命令解析程序也是状态机;甚至连继电器的吸合/释放控制、发光管(LED)的亮/灭控制又何尝不是个状态机。 [1]: https://www.qingfengblog.com/usr/uploads/2020/01/3348968632.png [2]: https://www.qingfengblog.com/usr/uploads/2020/01/804648749.png [3]: https://www.qingfengblog.com/usr/uploads/2020/01/4261318898.png Last modification:January 8, 2020 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 1 如果觉得我的文章对你有用,请随意赞赏