打印

一种基于定时器的按键检测程序

[复制链接]
10805|21
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
3htech|  楼主 | 2012-11-15 16:18 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 3htech 于 2012-11-15 16:19 编辑

一种基于定时器的按键检测程序
By—小白菜(3htech
该程序产生的背景

话说2012年,小白菜要做一个三相电压电流组合表,这个仪表需要进行数据输入(小白菜以前的项目也有输入,但是小白菜没有仔细的研究过),并且给出的时间很长,小白菜有时间来做一些研究处理。拿着以前写的按键检测程序,感觉漏洞百出,于是想着趁着有时间把这部分做出来,于是便用了一个星期(实际是5天,双休思密达)专门写了这部分程序。
小白菜的应用需求
小白菜的仪表仅需要单短击(简称单击,短击,短按等)和单长击(简称长击,长按等),单短击要在按键松开后才进行识别,单长击要在达到设定的时间阀值时进行识别(这时按键未松开)。
不需要考虑的情况如下,不需要连击(可以做为多次短击)、不需要多键同时击、暂时不需要考虑输入数字时长按某键,数字快速自加或自减。
该检测程序要满目不依赖于任何一种单片机,也不依赖按键连接方式,如独立式,矩阵式(当然你要用按键扫描芯片那就……你要用AD式键盘,我无语……好吧,你赢了~),能够独立存在。
按键过程分析
1 按键小思考
正常的按键过程(不考虑非法的按键状态)如 2.1.1
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image002.jpg
2.1.1正常按键状态示意图
单击和长击只是时间上的区分而已,但是其识别时稍有区别,单击是在按键松开时进行识别的,长击是在按键闭合时进行识别的。见2.1.2
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image004.jpg
2.1.2长短击按键状态示意图
2 各种可能出现的按键情况
合法情况不再赘述。下面把非法(仅在本应用中非法)的情况列一下。
(1) 人为或干扰引起的单击时间过短(主要为防干扰)。
(2) 单击时间过长(与(1)对应,凡事有短就有长,要有度嘛~)。
(3) 按下了多个键(与我的应用需要相悖,所以非法)。这里有可能是同时按的,也有可能是异步按下的。
(4) 快速多次按同一个键。这种情况可以归结到(1)
(5) 我觉得没有了,元芳,你怎么看??
3 小综合(综合?理综还是文综?
综合12进行考虑,得出以下几条。
(1)
正常的单击和长击示意图。如图2.3.1和图2.3.2所示。
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image006.jpg  file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image008.jpg
2.3.1短击识别示意图图                     2.3.2长击识别示意图
(2)
按键时间过短,应丢弃。如图2.3.3所示。
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image010.jpg
2.3.3按键时间过短示意图
(3)
多键同时按下时,不响应。如图2.3.4所示。
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image012.jpg    file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image014.jpg
2.3.4多键同时按下示意图
2.3.5 多键异步按下示意图
(4)
多键异步按下
这种情况要考虑一下了。如图2.3.5所示。这种情况不会影响到单击。看2.3.1可知,单击是在按键松开后才进行识别的;但这种情况对长击有影响,如果t1大于长击的阀值,那么键1是否应该识别为一次有效长击呢?这里小白菜认为键1有效,后面的程序也是按键1有效进行的处理。
既然认为有效,那么这个过程可以分解为两个部分,键1闭合到键2闭合这部分认为是一次合法的长击行为,键2闭合以后认为是多键同时按下(图2.3.4中所示),按出错处理。
(5)
多键前仆后继地按下
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image016.jpg
2.3.6多键前仆后继地按下示意图
这里仍然存在(4)中存在的长击问题。这里小白菜依旧认为长击有效。并且不再响应后面的按键。


(6)
按键因为受到干扰而出现了假松开。如图2.3.7中的尖峰所示。
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image018.jpg
2.3.7“假松开干扰示意图
小白菜并不知道这种假松开在实际发生的概率有多大。这种情况是小白菜捏造出来的。实际中,如果按键完全闭合后还口线电平还到达能够识别的高度,那么这个干扰也够强烈的(此时口线实际上是接地的)!
对付这种假松开,小白菜想到的是设定按键松开计时器,只有计时器到一定的值,并且在此期间,读取到的状态都是键松开,才真正认为是键松开。否则,认为是一次干扰而已。
还有其他错误情况吗,元芳??


一种基于定时器按键检测程序.pdf

450.26 KB

流程图.pdf

60.45 KB

状态转换图.pdf

39.05 KB

相关帖子

沙发
3htech|  楼主 | 2012-11-15 16:18 | 只看该作者
三 “伪状态”的抽象
为什么要加上“伪”字呢,因为小白菜对于状态机这个东西,认识的不够清楚,为了避免老鸟们的拍砖,加上伪字。嘎嘎~
根据第二部分,小白菜大致能抽象出一些东西了。
1 确定所使用的变量
        前言,在程序中,小白菜会加上前缀u8,i8,u16等,代表着无符号8位、有符号8位、无符号16位等,具体请参见TypeRedefine.H中的说明。这里为了叙述方便,去掉了u8、i8等前缀。
(1) 首先要有一个按键状态是否正确合法的标志 KeyErr标志寄存器。
(2) 按键时长计时器KeyTimer。用这个变量来确定键按下了多长时间。
单击时间范围 TS < t < TL 。这里Ts代表短按最短时间,TL 代表长按最短时间。
长击时间范围 t ≥ TL 。
(3) 为了防止【多键“前仆后继”地按下】(图2.3.6中所示)这种情况的出现,小白菜自以为是的增加了两个变量,当前按键值KeyCurrent和上次按键值KeyLast。通过两个变量的比较来防止二(5)的出现。
(4) 键松开计时器KeyOffTimer。
设置本变量主要是为了防止【假松开】(图2.3.7中所示)情况的出现。KeyOffTimer必须大于某个值才算完全松开。
这里为何不对键按下时做处理呢(假如由于干扰而出现假按下的情况)?因为假按下的延续时间比较短,达不到我们所设置的Ts时间,所以可以忽略,即由Ts来保证当由于干扰而出现假按下时,程序能够不响应。如果假按下的时间超过了Ts,让你来处理的话,你还能认为是假按下吗?
(5) 键值寄存器 KeyValue和KeyReg,用户可以直接查询变量KeyValue,最重要的是不需要用户清零;而KeyReg则由检测程序和键值读取函数使用,用户不可使用。

2 所有可能的情况
这里列了一个表。在表中利用组合可以看到所有可能情况,最重要的是,不会漏掉任何一种可能。
表中变量说明:
KeyErr标志寄存器 :         TRUE或OK = 无错误,ERR = 有错误。
KeyCurrent和KeyLast:NULL = 无键按下,OK = 键值合法,ERR = 键值非法。
KeyErr标志寄存器        KeyCurrent        KeyLast        此时的操作        合法性
TRUE        NULL        NULL        无键按下或完全松开        √
                OK        按键刚松开        √
                ERR        防错处理        ×
TRUE        OK        NULL        键刚按下        √
                OK        (与KeyCurrent相等)键正在按下        √
                OK        (与KeyCurrent不等) 二(5)        ×
                ERR                ×
TRUE        ERR        忽略        错误按键        ×
ERR        NULL        忽略        错误按键松开        ×
        OK
/ ERR        忽略        错误按键未松开        ×
表3.2.1 变量组合表
3 “伪”状态转换图
根据表3.2.1,画出“伪”状态转换图。这里要注意几点:
(1)        为了在一张图上能画的下,这里面把Key都省略了。
(2)        图中的粗线是正常按键时的状态转换。
(3)        小白菜在长按键识**,把Err标志寄存器置为了ERR,所以之后的状态就到了最下方的【Err标志寄存器 = ERR, Curr = OK或ERR, Last = 忽略】。

图 3.3.1 状态转换图


4 程序传统流程图
这里面需要注意一点,在长按键的识别时,有两种方法:
一是检查 KeyTimer == TL 这样可以保证只识别一次。
二是检查KeyTimer >= TL 然后把 KeyErr标志寄存器 置为Err,这样也能保证只识别一次,并且可以转换其状态,使其不再识别按键。
第一种方法的好处是清晰明了,但是却仅检查了TL 这一个点,对于大于TL情况没有做处理,小白菜很担心,也许你会说,正常运行时没问题,但是,我就怕不正常运行;基于安全的考虑,小白菜选用了方法二。流程图请参见图3.4.1。
图3.4.1 流程图
四 代码编写(略)
五 应用说明
说了半天,还没有中断的事。下面说一下,同学们,注意听哈。
1 while(1)式检测和中断式检测的对比
(1) 在网上也看过一些人发的按键检测,自己以前也写过按键检测,但是无论这些检测程序多么美妙,绝大部分是在while(1)里执行按键检测程序(先管它叫while(1)式吧),这其实有一个很大的隐患。
当 MCU 负担不重时,while(1)式检测程序也许能好好的工作,这是由于while(1)的平均执行时间(不是最大执行时间)与按键检测程序的最长调用间隔时间相差无几或更小。但当MCU 负担很重时,while(1)式反应就会明显变慢,甚至会丢失按键信息;这是由于while(1)的平均执行时间过大所造成的。举个极端例子,while(1)平均执行时间为1000ms,短按时间为300ms,显然按键检测程序会丢失按键信息。
一句话总结 while(1)式的按键检测程序:时间不可控,耗时长,易丢失信息。
(2) 下面说一下在定时中断服务函数中进行按键检测(管它叫中断式吧)的优缺点。
首先说缺点:
由于是在中断中执行检测程序,增加的中断执行时间。但由于仅仅是几个8位变量的大小判断,所以耗时很短,小白菜曾在自己所用的平台上测试过,不超过50us。
下面说一下优点。
优点一:中断式检测的调用时间是可预测,是可控的。中断的发生和while(1)的执行可以看成是并行的(老鸟不要拍砖啊),这显然为按键检测程序按我们设定的间隔执行。
优点二:几乎每个系统中都会出现定时中断(这里不用RTOS,因为小白菜不懂RTOS,只会裸了个奔),并且小白菜接触到的系统都是以10ms,20ms 左右这样的定时中断。
优点三:一但把按键检测程序放在中断处理函数中执行,那我们就不需要再考虑之了,只需要在需要使用按键值的地方(比如菜单)调用一个获取键值函数就可以了,而获取键值
函数也仅仅是把一个变量的值返回。
优点四:巧妙的躲过了抖动区。小白菜的系统中用的 10ms 中断。如图5.1.1 所示。

图 5.1.1 抖动区示意图
大家知道单片机IO口对高低电平识别是有范围的,如图5.1.1中所示,所以,虽然aa’、dd’段存在抖动,但是这一段的电平仍然在“松开”范围内,同理,bb’、cc’段的电平仍在“闭合”的范围内。实际中肯定有ab 段时长大于a’b’段时长。而我们真正要躲避的是a’b’和c’d’段,这才是真正的抖动区,因为其处于单片机不能识别的电平范围。
一般地,抖动区在10ms左右(我是听说的,这个我真没测试过),而我的中断时间是10ms,由图5.1.1可以,正好可以完美地跨过ab段和cd段,也必然跨过了a’b’和c’d’,从而实现了利用中断间隔达到普通延时去抖动的目的。
小白在实验定时间隔时发现,10-20ms 的定时时间对按键响应比较好。中断时间过短,会因中断太频繁而降低系统性能;中断时间过长又不能及时地响应短按键。万能的亲们,你们自己试试吧。
2 函数说明。
(1) extern void App_Init_Key(void);按键检测初始化函数。必须在中断服务程序执行前调用,使变量一个合法的初始值开始运行,以防止因为变量随机值而出现误动作的情况!!!
(2) extern void App_Detect_Key(void); 核心函数,好吧,直接放到定时中断服务程序中就行了。不需要做什么工作。
(3) extern uint8 App_Get_Key_Value(void);读取按键函数。返回值是当前的键值,一定要在使用键值前先调用本函数!在一个循环中本函数只能调用一次!
extern uint8 App_Get_Key_Value(void)
{
    // 这段代码不允许被打断,以防止定时中断函数对键值寄存器做赋值操作。
    EA = 0;
    KeyScan.u8KeyValue = KeyScan.u8KeyReg;
    KeyScan.u8KeyReg = KEY_NULL;
    EA = 1;

    return KeyScan.u8KeyValue;
}
看到该函数的实现了吧,如果你连着调用两次,第二次读到的就是无键按下了,所以在使用时,一个循环体中只能调用一次本函数。

3 简单的应用举例
void main(void) 主函数中
{
App_Init_Key(); // 初始化按键
xxx();     // 初始化定时器,定时时间10ms

while(1)
{
App_Get_Key_Value();

if(KEY_NULL == KeyScan.u8KeyValue)
{// 什么都不做
}
else if(KEY_1 == KeyScan.u8KeyValue)
{ // 处理1
}
}
}        定时中断ISR
10ms执行一次
Tn_ISR()
{
App_Detect_Key();
}

使用特权

评论回复
板凳
3htech|  楼主 | 2012-11-15 16:20 | 只看该作者
欢迎各位前辈的拍砖和骂声。
:lol

使用特权

评论回复
地板
InTo_JuMp| | 2012-11-15 16:50 | 只看该作者
支持一下,留个记号回家看

使用特权

评论回复
5
jasoncsx| | 2012-12-5 15:49 | 只看该作者
正需要着,研究一下

使用特权

评论回复
6
hanyz123| | 2012-12-15 11:10 | 只看该作者
留下自己的脚印, 感觉不错 正学习中

使用特权

评论回复
7
wdliming| | 2012-12-15 18:16 | 只看该作者
谢谢楼主分享

使用特权

评论回复
8
zxsa_001| | 2012-12-15 18:56 | 只看该作者
本帖最后由 zxsa_001 于 2012-12-15 19:00 编辑

初学者路过,不过当前我这里遇到个一个类似与楼主的按键扫描问题,自己琢磨了好几天,截止目前为止又琢磨出一个思路,不过还没试,把问题简单描述一下,希望楼主能给个思路建议  ;命题是这样的:使用三个按键进行一项参数设置,同时将设置的过程在数码管上实时显示出来(数码管正常显示的是当前设置的参数1);具体的实现过程如下:
1、当点按设置按键时,数码管开始闪烁(闪烁标志程序进入了设置状态,如果此时没有任何按键按动,则数码管闪烁5次后自动退出,回到正常显示),点按设置键1次,对参数1进行设置(参数1在数码管上闪烁显示),点按2次,设置参数2;参数2设置完毕后再点按设置键第三次则数码管退出闪烁,转入正常显示;
2、在数码管闪烁显示的时候,例如设置参数1时,点按其他两个键(一个+,一个-)可以对参数1进行加减操作;
3、在对参数设置完毕后(没有按键再按下),数码管闪烁5次后,同样也退出设置状态;

其实说的再简单些,就是用数码管闪烁显示的方式表明程序正在运行在设置状态;并在设置状态下的所有数码管显示的参数都是以闪烁的形式表现
我最终是给困在的第三步的实现上,因为参数进行完一次设置,在没有任何按键点按下的情况自己会闪烁5次退出设置状态,可是如果就在这闪烁的
5次过程当中,再次有按键按下,或是对设置参数的选择,或是对参数加减操作,都会使闪烁重新开始,再闪烁5次;我曾经用两个个外部中断口作为两个按键的捕捉口实现过上述命题,不过总觉得用外部中断的方式实现按键操作 1是有点大才小用,2是单片机上的外部中断口也就那么几个,一旦按键多了,那该怎么控制;
另外数码管显示我是用了8个共阴极数码管扫描方式即实现的(将数码管显示子程序放在定时器中断服务子程序中,每2ms调用一次);

使用特权

评论回复
9
渤海三叠浪| | 2012-12-15 22:23 | 只看该作者
按键大家研究的很火爆啊   匠人也研究啊

但是我平时用不上啊 哈哈

使用特权

评论回复
10
ytfdhb| | 2012-12-16 12:25 | 只看该作者
来学习一下

使用特权

评论回复
11
WXJPCY888| | 2012-12-18 16:51 | 只看该作者
:lol经典的很!!!

使用特权

评论回复
12
如何RH| | 2013-4-10 18:09 | 只看该作者
很受教。
也遇到按键检测问题,用While 判断检测,紧接着的中断函数不能执行,感觉是进了中断,马上就跳出来。一直没有弄明白那里出错。

使用特权

评论回复
13
如何RH| | 2013-4-10 18:14 | 只看该作者
int main ()                       {
            EA=1;                             //     TR0=1;                             //
          init_timer();
     
          while(1)                                   
        loop: if(DA==0)                        
         {                                 
           delay10ms(5);                     //延时20ms去抖动,
                 if(DA==0)                 //读取按键状态
                 {                              //执行以下程序

                  sc ();            //执行中断函数
                  SW=0;
                                     sd_s2();                 //led 闪烁                     
                                while(DA==0);                     //等待按键松开
   
                              goto loop;                  //
                  }   

       }   
            
            
                               

        return 0;                             //主函数返回值
        }
            
          ;                           //; “结束程序

以上的SC(),不能执行,很纠结

使用特权

评论回复
14
juxun001| | 2013-8-1 15:06 | 只看该作者
路过学习中,好东西啊!

使用特权

评论回复
15
yylfcxpx168| | 2013-8-1 18:10 | 只看该作者
学习了

使用特权

评论回复
16
zdhlixiang2006| | 2013-8-2 22:28 | 只看该作者
不知道为什么,只要看到有软件延时这种强制CPU原地空转的代码,我都很不爽

使用特权

评论回复
17
AFCA| | 2013-8-8 18:45 | 只看该作者
谢谢楼主

使用特权

评论回复
18
lytmkai| | 2014-1-11 21:35 | 只看该作者
学习了......

使用特权

评论回复
19
xianglin| | 2016-11-23 11:35 | 只看该作者
谢谢楼主分享

使用特权

评论回复
20
沧海一瞬| | 2016-11-23 20:50 | 只看该作者
zdhlixiang2006 发表于 2013-8-2 22:28
不知道为什么,只要看到有软件延时这种强制CPU原地空转的代码,我都很不爽 ...

同感,可以在定时器设个标志位,检测到按键不一样时,定时器开启计数,10ms置位标志位,再回头按键值。金沙滩也介绍过另一种方法。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:我是一颗小白菜~!

20

主题

416

帖子

3

粉丝