专栏首页嵌入式开发圈独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)

独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)

鸿哥是我的一位单片机学习的启蒙老师,在此分享来自于他的一篇文章。

【92.1 独立按键的硬件电路简介。】

上图92.1.1 独立按键电路

按键有两种驱动方式,一种是独立按键,一种是矩阵按键。1个独立按键要占用1个IO口,IO口不能共用。而矩阵按键的IO口是分时片选复用的,用少量的IO口就可以驱动翻倍级别的按键数量。比如,用8个IO口只能驱动8个独立按键,但是却可以驱动16个矩阵按键(4x4)。因此,按键少的时候就用独立按键,按键多的时候就用矩阵按键。这两种按键的驱动本质是一样的,都是靠识别输入信号的下降沿(或上升沿)来识别按键的触发。 独立按键的硬件原理基础,如上图,P2.2这个IO口,在按键K1没有被按下的时候,P2.2口因为单片机内部自带上拉电阻把电平拉高,此时P2.2口是高电平的输入状态。当按键K1被按下的时候,按键K1左右像一根导线连接到电源的负极(GND),直接把原来P2.2口的电平拉低,此时P2.2口变成了低电平的输入状态。编写按键驱动程序,就是要识别这个电平从高到低的过程,这个过程也叫下降沿。多说一句,51单片机的P1,P2,P3口是内部自带上拉电阻的,而P0口是内部没有上拉电阻的,需要外接上拉电阻。除此之外,很多单片机内部其实都没有上拉电阻的,因此,建议大家在做独立按键电路的时候,养成一个习惯,凡是按键输入状态都外接上拉电阻。 识别按键的下降沿触发有四大要素:自锁,消抖,非阻塞,清零式滤波。 “自锁”,按键一旦进入到低电平,就要“自锁”起来,避免不断触发按键,只有当按键被松开变成高电平的时候,才及时“解锁”为下一次触发做准备。 “消抖”,按键是一个机械触点器件,在接触的瞬间必然存在微观上的机械抖动,反馈到电平的瞬间就是“高,低,高,低...”这种不稳定的电平状态是一种干扰,但是,按键一旦按下去稳定了之后,这种状态就消失,电平就一直保持稳定的低电平。消抖的本质就是滤波,要把这种接触的瞬间抖动过滤掉,避免按键的“一按多触发”。 “非阻塞”,在处理消抖的时候,必须用到延时,如果此时用阻塞的delay延时就会影响其它任务的运行效率,因此,用非阻塞的定时延时更加有优越性。 “清零式滤波”,在消抖的时候,有两种境界,第一种境界是判断两次电平的状态,中间插入“固定的时间”延时,这种方法前后一共判断了两次,第一次是识别到低电平就进入延时的状态,第二次是延时后再确认一次是否继续是低电平的状态,这种方法的不足是,“固定的时间”全凭经验值,但是不同的按键它们的抖动时间长度是不同的,除此之外,前后才判断了两次,在软件的抗干扰能力上也弱了很多,“密码等级”不够高。第二种境界就是“清零式滤波”,“清零式滤波”非常巧妙,抗扰能力超强,它能自动过滤不同按键的“抖动时间”,然后再进入一个“稳定时间”的“N次识别判断”,更加巧妙的是,在“抖动时间”和“稳定时间”两者时间内,只要发现一次是高电平的干扰,就马上自动清零计时器,重新开始计时。“稳定时间”一般取20ms到30ms之间,而“抖动时间”是隐藏的,在代码上并没有直接描写出来,但是却无形地融入了代码之中,只有慢慢体会才能发现它的存在。 具体的代码如下,实现的功能是按一次K1或者K2按键,就触发一次蜂鸣器鸣叫。

  1#include "REG52.H"  
  2
  3#define KEY_VOICE_TIME   50 //按键触发后发出的声音长度  
  4#define KEY_FILTER_TIME  25  //按键滤波的“稳定时间”25ms
  5
  6void T0_time();
  7void SystemInitial(void) ;
  8void Delay(unsigned long u32DelayTime) ;
  9void PeripheralInitial(void) ;
 10
 11void BeepOpen(void);   
 12void BeepClose(void); 
 13void VoiceScan(void);
 14void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
 15void KeyTask(void);    //按键任务函数,放在主函数内
 16
 17sbit P3_4=P3^4;  
 18sbit KEY_INPUT1=P2^2;  //K1按键识别的输入口。
 19sbit KEY_INPUT2=P2^1;  //K2按键识别的输入口。
 20
 21volatile unsigned char vGu8BeepTimerFlag=0;  
 22volatile unsigned int vGu16BeepTimerCnt=0;  
 23
 24volatile unsigned char vGu8KeySec=0;  //按键的触发序号,全局变量意味着是其它函数的接口。
 25
 26void main() 
 27{
 28SystemInitial();            
 29Delay(10000);               
 30PeripheralInitial();      
 31    while(1)  
 32{  
 33   KeyTask();    //按键任务函数
 34    }
 35}
 36
 37void T0_time() interrupt 1     
 38{
 39VoiceScan();  
 40KeyScan();    //按键识别的驱动函数
 41
 42TH0=0xfc;   
 43TL0=0x66;   
 44}
 45
 46
 47void SystemInitial(void) 
 48{
 49TMOD=0x01;  
 50TH0=0xfc;   
 51TL0=0x66;   
 52EA=1;       
 53ET0=1;      
 54TR0=1;      
 55}
 56
 57void Delay(unsigned long u32DelayTime) 
 58{
 59    for(;u32DelayTime>0;u32DelayTime--); 
 60}
 61
 62void PeripheralInitial(void) 
 63{
 64
 65}
 66
 67void BeepOpen(void)
 68{
 69P3_4=0;  
 70}
 71
 72void BeepClose(void)
 73{
 74P3_4=1;  
 75}
 76
 77void VoiceScan(void)
 78{
 79
 80          static unsigned char Su8Lock=0;  
 81
 82if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
 83          {
 84                  if(0==Su8Lock)
 85                  {
 86                   Su8Lock=1;  
 87BeepOpen(); 
 88     }
 89    else  
 90{     
 91
 92                       vGu16BeepTimerCnt--;         
 93
 94                   if(0==vGu16BeepTimerCnt)
 95                   {
 96                           Su8Lock=0;     
 97BeepClose();  
 98                   }
 99
100}
101          }         
102}
103
104/* 注释一:
105* 独立按键扫描的详细过程,以按键K1为例,如下:
106* 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。
107* 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到
108*         阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
109*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零了,这个过程
110*         非常巧妙,非常有效地去除瞬间的杂波干扰。以后凡是用到开关感应器的时候,
111*         都可以用类似这样的方法去干扰。
112* 第三步:如果按键按下的时间达到阀值KEY_FILTER_TIME时,则触发按键,把编号vGu8KeySec赋值。
113*         同时,马上把自锁标志Su8KeyLock1置1,防止按住按键不松手后一直触发。
114* 第四步:等按键松开后,自锁标志Su8KeyLock1及时清零(解锁),为下一次自锁做准备。
115* 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。
116*/
117void KeyScan(void)  //此函数放在定时中断里每1ms扫描一次
118{
119   static unsigned char Su8KeyLock1; //1号按键的自锁
120   static unsigned int  Su16KeyCnt1; //1号按键的计时器
121   static unsigned char Su8KeyLock2; //2号按键的自锁
122   static unsigned int  Su16KeyCnt2; //2号按键的计时器
123
124   //1号按键
125   if(0!=KEY_INPUT1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
126   {
127      Su8KeyLock1=0; //按键解锁
128      Su16KeyCnt1=0; //按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
129   }
130   else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。这行很多初学者有疑问,请看专题分析。
131   {
132      Su16KeyCnt1++; //累加定时中断次数
133      if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME,长度是25ms。
134      {
135         Su8KeyLock1=1;  //按键的自锁,避免一直触发
136         vGu8KeySec=1;    //触发1号键
137      }
138   }
139
140   //2号按键
141   if(0!=KEY_INPUT2)
142   {
143      Su8KeyLock2=0; 
144      Su16KeyCnt2=0;       
145   }
146   else if(0==Su8KeyLock2)
147   {
148      Su16KeyCnt2++; 
149      if(Su16KeyCnt2>=KEY_FILTER_TIME) 
150      {
151         Su8KeyLock2=1;  
152         vGu8KeySec=2;    //触发2号键
153      }
154   }
155
156
157}
158
159void KeyTask(void)    //按键任务函数,放在主函数内
160{
161if(0==vGu8KeySec)
162{
163return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
164}
165
166switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
167{
168   case 1:     //1号按键
169
170        vGu8BeepTimerFlag=0;  
171vGu16BeepTimerCnt=KEY_VOICE_TIME;  //触发按键后,发出固定长度的声音
172        vGu8BeepTimerFlag=1;  
173vGu8KeySec=0;  //响应按键服务处理程序后,按键编号必须清零,避免一致触发
174break;
175
176   case 2:     //2号按键
177
178        vGu8BeepTimerFlag=0;  
179vGu16BeepTimerCnt=KEY_VOICE_TIME;  //触发按键后,发出固定长度的声音
180        vGu8BeepTimerFlag=1;  
181vGu8KeySec=0;  //响应按键服务处理程序后,按键编号必须清零,避免一致触发
182break;
183
184}
185}

【92.2 专题分析:else if(0==Su8KeyLock1)。】

疑问:

 1if(0!=KEY_INPUT1)
 2   {
 3      Su8KeyLock1=0; 
 4      Su16KeyCnt1=0;      
 5   }
 6   else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。为什么?为什么?为什么?
 7   {
 8      Su16KeyCnt1++; 
 9      if(Su16KeyCnt1>KEY_FILTER_TIME)
10      {
11         Su8KeyLock1=1;  
12         vGu8KeySec=1;   
13      }
14   }

解答: 首先,我们要明白C语言的语法中,

1if(条件1)
2{
3
4}
5else if(条件2)
6{
7
8}

以上语句是一对组合语句,不能分开来看。当(条件1)成立的时候,它是绝对不会判断(条件2)的。当(条件1)不成立的时候,才会判断(条件2)。 回到刚才的问题,当程序执行到(条件2) else if(0==Su8KeyLock1)的时候,就已经默认了(条件1) if(0!=KEY_INPUT1)不成立,这个条件不成立,就意味着0==KEY_INPUT1,也就是有按键被按下,因此,这里的else if(0==Su8KeyLock1)等效于else if(0==Su8KeyLock1&&0==KEY_INPUT1),而Su8KeyLock1是一个自锁标志位,一旦按键被触发后,这个标志位会变1,防止按键按住不松手的时候不断触发按键。这样,按键只能按一次触发一次,松开手后再按一次,又触发一次。 【92.3 专题分析:if(0!=KEY_INPUT1)。】 疑问:为什么不用if(1==KEY_INPUT1)而用if(0!=KEY_INPUT1)? 解答:其实两者在功能上是完全等效的,在这里都可以用。之所以本教程优先选用后者if(0!=KEY_INPUT1),是因为考虑到了代码在不同单片机平台上的可移植性和兼容性。很多32位的单片机提供的是库函数,库函数返回的按键状态是一个字节变量来表示,当被按下的时候是0,但是,当没有按下的时候并不一定等于1,而是一个“非0”的数值。 【92.4 专题分析:把KeyScan函数放在定时器中断里。】 疑问:为什么把KeyScan函数放在定时器中断里? 解答:中断函数里放的函数或者代码越少越好,但是KeyScan函数是特殊的函数,是涉及到IO口输入信号的滤波,滤波就涉及到时间的及时性与均匀性,放在定时中断函数里更加能保证时间的一致性。比如,蜂鸣器驱动,动态数码管驱动,按键扫描驱动,我个人都习惯放在定时中断函数里。 【92.5 专题分析:if(0==vGu8KeySec)return。】 疑问:if(0==vGu8KeySec)return是不是多此一举? 解答:在KeyTask函数这里,if(0==vGu8KeySec)return这行代码删掉,对程序功能是没有影响的,这里之所以多插入这行判断语句,是因为,当按键多达几十个的时候,避免主函数每次进入KeyTask函数,都挨个扫描判断switch的状态进行多次判断,如果增加了这行if(0==vGu8KeySec)return代码,就可以直接退出省事,在理论上感觉更加运行高效。其实,不同单片机不同的C编译器可能对switch语句的翻译不一样,因此,这里的是不是更加高效我不敢保证。但是可以保证的是,加了这行代码也没有其它副作用。

本文分享自微信公众号 - 嵌入式开发圈(gh_d6ff851b4069)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-05-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Spring Boot整合Elasticsearch

    Elasticsearch是一个全文搜索引擎,专门用于处理大型数据集。根据描述,自然而然使用它来存储和搜索应用程序日志。与Logstash和Kibana一起,它...

    JAVA葵花宝典
  • liteos 异常接管(十五)

    异常接管是操作系统对在运行期间发生异常的情况进行处理的一系列动作,譬如打印异常发生时当前函数调用栈信息、 cpu现场信息、任务的堆栈情况等。

    233333
  • 基于MicroPython:TPYBoard心率监测器

    这几年智能穿戴设备大火,尤其是手环类,从Apple Watch到荣耀手环,再到不知名的某些品牌,智能穿戴设备是铺天盖地的来了。而其中心率监测基本上是所有穿戴设备...

    阿莉埃蒂
  • Linux 设备和驱动的相遇

    上一节的最后我们讲到设备树的三大作用,其最后一个作用也是最重要的作用:设备信息集合。这一节结合设备信息集合的详细讲解来认识一下设备和驱动是如何绑定的。所谓设备信...

    刘盼
  • JAVA、C、C++、Python同样是高级语言,为什么只有C和C++可以编写单片机程序?

    从事编程十几年,JAVA、C、C++、Python这四种编程语言都玩过,前三种玩的比较多,python做为兴趣爱好或者玩脚本的时候弄过,编程语言在使用的时候主要...

    程序员互动联盟
  • 2-STM32+W5500+GPRS物联网开发基础篇-基础篇学习的内容

    https://www.cnblogs.com/yangfengwu/p/10936553.html

    杨奉武
  • go语言调度器源代码情景分析之二:CPU寄存器

    寄存器是CPU内部的存储单元,用于存放从内存读取而来的数据(包括指令)和CPU运算的中间结果,之所以要使用寄存器来临时存放数据而不是直接操作内存,一是因为CPU...

    阿波张
  • 增长黑客死了!留存黑客又开始盛行!

    知乎盐选会员、京东Plus、阿里88VIP、美团外卖会员......忽如一夜春风来,互联网千家万户的梨花都开成了Costco。

    数据猿
  • go语言调度器源代码情景分析之三:内存

    内存由大量内存单元组成,内存单元大小为1个字节(1字节包含8个二进制位), 每个内存单元都有一个编号,更专业的说法是每一个内存单元都有一个地址,我们在编写汇编代...

    阿波张
  • 3-ESP8266 SDK开发基础入门篇--点亮一个灯

    https://www.cnblogs.com/yangfengwu/p/11072834.html

    杨奉武

扫码关注云+社区

领取腾讯云代金券