EDA365电子论坛网

标题: 堪称一绝按键扫描 [打印本页]

作者: fish1352    时间: 2015-11-12 16:41
标题: 堪称一绝按键扫描
新型的按键扫描程序
& h% ?) K& M6 e, _: v: Q: t不过我在网上游逛了很久,也看过不少源程序了,没有发现这种按键处理办法的踪迹,所以,我将他共享出来,和广大同僚们共勉。我非常坚信这种按键处理办法的便捷和高效,你可以移植到任何一种嵌入式处理器上面,因为C语言强大的可移植性。
# u, b) o' M6 Z; j. z$ q/ e- ^同时,这里面用到了一些分层的思想,在单片机当中也是相当有用的,也是本文的另外一个重点。
5 K  M( H# G3 V4 L; `( _% \对于老鸟,我建议直接看那两个表达式,然后自己想想就会懂的了,也不需要听我后面的自吹自擂了,我可没有班门弄斧的意思,hoho~~但是对于新手,我建议将全文看完。因为这是实际项目中总结出来的经验,学校里面学不到的东西。) u. ^. y! E& r( R5 @4 m- @8 y
以下假设你懂C语言,因为纯粹的C语言描述,所以和处理器平台无关,你可以在MCS-51,AVR,PIC,甚至是ARM平台上面测试这个程序性能。当然,我自己也是在多个项目用过,效果非常好的。. u9 g7 ?: R/ m2 e4 L
好了,工程人员的习惯,废话就应该少说,开始吧。以下我以AVR的MEGA8作为平台讲解,没有其它原因,因为我手头上只有AVR的板子而已没有51的。用51也可以,只是芯片初始化部分不同,还有寄存器名字不同而已。
4 B! `" ~! w" w7 Z核心算法:
( w; @1 f- ~& d( P3 Hunsigned char Trg;7 a9 i; V7 W- Z3 w: r
unsigned char Cont;
/ `* a# g( ^) N1 I9 X7 B$ Z4 Svoid KeyRead( void )8 T6 F9 ^0 i# b9 l7 p+ }
{
/ s1 y, O3 {  k' T! Z    unsigned char ReadData = PINB^0xff;   // 17 \! u5 t" x! D/ ~$ j
    Trg = ReadData & (ReadData ^ Cont);      // 21 T) g7 U8 [8 V) p) ~& F% j( {
    Cont = ReadData;                                // 3
9 q" i4 h0 w) a7 C$ T3 u) [3 \6 V- a}5 b1 X6 P7 S7 S7 n0 K* K0 u
完了。有没有一种不可思议的感觉?当然,没有想懂之前会那样,想懂之后就会惊叹于这算法的精妙!!4 i+ y# M( U4 L# T
下面是程序解释:
) e6 m, x, O4 n, P5 C7 uTrg(triger) 代表的是触发,Cont(continue)代表的是连续按下。# t& G/ G0 I3 I1 z5 r
1:读PORTB的端口数据,取反,然后送到ReadData 临时变量里面保存起来。- w, L# w/ j  M2 _2 Y/ E& m
2:算法1,用来计算触发变量的。一个位与操作,一个异或操作,我想学过C语言都应该懂吧?Trg为全局变量,其它程序可以直接引用。' V( L( ?) N$ C0 k7 J
3:算法2,用来计算连续变量。: n; p& I/ O' X* N2 ~
看到这里,有种“知其然,不知其所以然”的感觉吧?代码很简单,但是它到底是怎么样实现我们的目的的呢?好,下面就让我们绕开云雾看青天吧。8 J1 F8 [" @: h9 [& D/ [
我们最常用的按键接法如下:AVR是有内部上拉功能的,但是为了说明问题,我是特意用外部上拉电阻。那么,按键没有按下的时候,读端口数据为1,如果按键按下,那么端口读到0。下面就看看具体几种情况之下,这算法是怎么一回事。
0 l& q; M: d# |' C9 q8 g; I6 @(1)       没有按键的时候) m+ p" C+ M! e3 u( X$ ^
端口为0xff,ReadData读端口并且取反,很显然,就是 0x00 了。
+ o8 O. \# q& p( wTrg = ReadData & (ReadData ^ Cont); (初始状态下,Cont也是为0的)很简单的数学计算,因为ReadData为0,则它和任何数“相与”,结果也是为0的。
2 r: N, y5 B/ g) u; `% cCont = ReadData; 保存Cont 其实就是等于ReadData,为0;; T" K, d1 D& _/ G6 m3 e) _7 I6 g, V/ t- L
结果就是:
4 d2 [4 g! N1 h" w; eReadData = 0;9 R# G& u# L5 f+ R6 N
Trg = 0;
$ ]5 G, G' ~# ?% F2 G: q: }( I* QCont = 0;
: B" v/ [& B- L: J1 }(2)       第一次PB0按下的情况
1 k) H3 F& Y5 G2 g端口数据为0xfe,ReadData读端口并且取反,很显然,就是 0x01 了。* J9 d  i' d) x1 {
Trg = ReadData & (ReadData ^ Cont); 因为这是第一次按下,所以Cont是上次的值,应为为0。那么这个式子的值也不难算,也就是 Trg = 0x01 & (0x01^0x00) = 0x01% S! k! T4 {$ L+ o
Cont = ReadData = 0x01;9 ~, m  ?7 f. ]+ a
结果就是:' T2 A( }, n* @
ReadData = 0x01;
1 n3 D: }: \1 Q' c. G+ @Trg = 0x01;Trg只会在这个时候对应位的值为1,其它时候都为0
4 ]6 k3 u6 ~# x# L; vCont = 0x01;/ ~* k% K* j& L. {2 L+ {
(3)       PB0按着不松(长按键)的情况
( n8 t4 P4 z6 g, E端口数据为0xfe,ReadData读端口并且取反是 0x01 了。
/ e" H/ w. j8 S  b: e9 `Trg = ReadData & (ReadData ^ Cont); 因为这是连续按下,所以Cont是上次的值,应为为0x01。那么这个式子就变成了 Trg = 0x01 & (0x01^0x01) = 0x00( A9 R* s  v: o' u4 [' K
Cont = ReadData = 0x01;
, y$ f2 O! ?  V* e& I结果就是:
' g5 Q: p# F) a  I0 jReadData = 0x01;
. W# N7 I, T5 m5 H* H4 L. fTrg = 0x00;. ~. T1 J" P' K
Cont = 0x01;! s4 ]* {+ v5 {' E( r
因为现在按键是长按着,所以MCU会每个一定时间(20ms左右)不断的执行这个函数,那么下次执行的时候情况会是怎么样的呢?
" T, N8 i8 a" M* z7 I" @1 \) hReadData = 0x01;这个不会变,因为按键没有松开. e  N* I  y2 H8 [- C( }1 y$ U
Trg = ReadData & (ReadData ^ Cont) = 0x01 & (0x01 ^ 0x01) = 0 ,只要按键没有松开,这个Trg值永远为 0 !!!9 O3 T- `" F, {  R4 r
Cont = 0x01;只要按键没有松开,这个值永远是0x01!!
' R; l& L+ J5 N(4)       按键松开的情况4 w! V) H/ X* g( G% d9 ^& ]  j5 x+ F
端口数据为0xff,ReadData读端口并且取反是 0x00 了。' q6 a8 z" }# R8 M. M# ?
Trg = ReadData & (ReadData ^ Cont) = 0x00 & (0x00^0x01) = 0x00
+ c% _# U3 d, a7 [! @+ r, vCont = ReadData = 0x00;: P8 c! ^" S* L; |6 E( ~' Q
结果就是:& ~# ?3 J' w4 [! }, a+ I! h& p( v. d
ReadData = 0x00;& J7 N+ @* i0 D* i
Trg = 0x00;$ A& i/ F) K8 O) e% U
Cont = 0x00;( h* K0 K" d+ e0 w' b, V4 \
很显然,这个回到了初始状态,也就是没有按键按下的状态。
- i1 q; u. L3 `( J( `( ]/ }, m总结一下,不知道想懂了没有?其实很简单,答案如下:+ N' R0 N* F5 ?9 l$ l
Trg 表示的就是触发的意思,也就是跳变,只要有按键按下(电平从1到0的跳变),那么Trg在对应按键的位上面会置一,我们用了PB0则Trg的值为0x01,类似,如果我们PB7按下的话,Trg 的值就应该为 0x80 ,这个很好理解,还有,最关键的地方,Trg 的值每次按下只会出现一次,然后立刻被清除,完全不需要人工去干预。所以按键功能处理程序不会重复执行,省下了一大堆的条件判断,这个可是精粹哦!!Cont代表的是长按键,如果PB0按着不放,那么Cont的值就为 0x01,相对应,PB7按着不放,那么Cont的值应该为0x80,同样很好理解。
2 X( a$ K' ~4 q8 K; V* m, a如果还是想不懂的话,可以自己演算一下那两个表达式,应该不难理解的。$ v% E8 P1 J" t4 [! F
因为有了这个支持,那么按键处理就变得很爽了,下面看应用:! p4 B$ ?  e' t1 l4 B8 ?/ u. p
应用一:一次触发的按键处理
7 b% o* b) e4 }! \( Z; N9 O, K假设PB0为蜂鸣器按键,按一下,蜂鸣器beep的响一声。这个很简单,但是大家以前是怎么做的呢?对比一下看谁的方便?
. \5 u( e1 w3 |6 I% X8 q& v#define KEY_BEEP 0x01
" \; Y1 z' h- I7 Xvoid KeyProc(void)
' ?7 ]7 \' V8 D$ p) l{: d+ `+ u9 r; x* z3 ^
       if (Trg & KEY_BEEP) // 如果按下的是KEY_BEEP
, `! G6 s4 X- p    {) J1 _$ d* p" m+ N# g
         Beep();            // 执行蜂鸣器处理函数
* Z% {- y# O' _  U; I    }' Z, [/ j- x4 H1 M
}3 H* s, N8 k6 x; B2 l$ u, I
怎么样?够和谐不?记得前面解释说Trg的精粹是什么?精粹就是只会出现一次。所以你按下按键的话,Trg & KEY_BEEP 为“真”的情况只会出现一次,所以处理起来非常的方便,蜂鸣器也不会没事乱叫,hoho~~~
4 i0 i$ w0 s4 n% t或者你会认为这个处理简单,没有问题,我们继续。
: v! \6 A4 x: K4 D* b( C应用2:长按键的处理1 O0 `* A% F5 a8 j
项目中经常会遇到一些要求,例如:一个按键如果短按一下执行功能A,如果长按2秒不放的话会执行功能B,又或者是要求3秒按着不放,计数连加什么什么的功能,很实际。不知道大家以前是怎么做的呢?我承认以前做的很郁闷。5 ]7 @3 t4 ~. ~- W
但是看我们这里怎么处理吧,或许你会大吃一惊,原来程序可以这么简单3 u1 `. B7 m0 y' r! _' J
这里具个简单例子,为了只是说明原理,PB0是模式按键,短按则切换模式,PB1就是加,如果长按的话则连加(玩过电子表吧?没错,就是那个!). Z: [, m  r& r% I4 H. T
#define KEY_MODE 0x01    // 模式按键
$ {; f6 o6 `  Q, V. Q#define KEY_PLUS 0x02     // 加
5 g' b6 D7 S/ i. lvoid KeyProc(void)" v; W2 M8 J" f* r8 X
{
4 }' D; z$ s2 z6 W9 E* ]8 z       if (Trg & KEY_MODE) // 如果按下的是KEY_MODE,而且你常按这按键也没有用,( _0 A( V4 \1 y* t4 `" c: X) N
    {                    //它是不会执行第二次的哦 , 必须先松开再按下
( E# ]+ ?- @- K. M+ a& Q* K0 Y         Mode++;         // 模式寄存器加1,当然,这里只是演示,你可以执行你想
1 X, `3 b& K# C+ B5 L$ }  C                         // 执行的任何代码
# u& r7 F" o/ I: G  ^6 f5 V1 k6 `$ u" Z    }
4 B. p% M, Y0 L5 ~- ?& K    if (Cont & KEY_PLUS) // 如果“加”按键被按着不放
) m: F- B) c3 o! w7 d3 y. }    {
; {$ T! s4 l/ |9 t' N         cnt_plus++;       // 计时
$ K* g$ i0 ]" s/ w" ^% R- }         if (cnt_plus > 100) // 20ms*100 = 2S 如果时间到
# E0 D* d6 e2 p7 b4 ^# w         {
7 v) g4 N6 \0 Q1 f8 G              Func();      // 你需要的执行的程序4 \/ }4 A& s: f
         }          - F' u, z& D* |. D7 s( Y, f% {; r
    }
: w9 Q8 n$ e9 }- s" ]: @! L}. B2 Y; I% S0 N$ X1 w
不知道各位感觉如何?我觉得还是挺简单的完成了任务,当然,作为演示用代码。
# H% Z) {# v& `2 Y9 U应用3:点触型按键和开关型按键的混合使用
* I4 _& x" ^( j点触形按键估计用的最多,特别是单片机。开关型其实也很常见,例如家里的电灯,那些按下就不松开,除非关。这是两种按键形式的处理原理也没啥特别,但是你有没有想过,如果一个系统里面这两种按键是怎么处理的?我想起了我以前的处理,分开两个非常类似的处理程序,现在看起来真的是笨的不行了,但是也没有办法啊,结构决定了程序。不过现在好了,用上面介绍的办法,很轻松就可以搞定。' @" y0 z7 v( ]7 Y) N
原理么?可能你也会想到,对于点触开关,按照上面的办法处理一次按下和长按,对于开关型,我们只需要处理Cont就OK了,为什么?很简单嘛,把它当成是一个长按键,这样就找到了共同点,屏蔽了所有的细节。程序就不给了,完全就是应用2的内容,在这里提为了就是说明原理~~" [5 _( h8 H& U  e, J) o
好了,这个好用的按键处理算是说完了。可能会有朋友会问,为什么不说延时消抖问题?哈哈,被看穿了。果然不能偷懒。下面谈谈这个问题,顺便也就非常简单的谈谈我自己用时间片轮办法,以及是如何消抖的。
6 K2 ~+ Q/ @# i6 \延时消抖的办法是非常传统,也就是 第一次判断有按键,延时一定的时间(一般习惯是20ms)再读端口,如果两次读到的数据一样,说明了是真正的按键,而不是抖动,则进入按键处理程序。
$ H' D  p! j4 ^/ ~% t  F7 T当然,不要跟我说你delay(20)那样去死循环去,真是那样的话,我衷心的建议你先放下手上所有的东西,好好的去了解一下操作系统的分时工作原理,大概知道思想就可以,不需要详细看原理,否则你永远逃不出“菜鸟”这个圈子。当然我也是菜鸟。我的意思是,真正的单片机入门,是从学会处理多任务开始的,这个也是学校程序跟公司程序的最大差别。当然,本文不是专门说这个的,所以也不献丑了。# F& M. t/ v# n: E% Q
我的主程序架构是这样的:
5 J$ g( o8 i0 l* ?volatile unsigned char Intrcnt;
; h" K# R& n4 E5 `7 K9 X4 ^/ Gvoid InterruptHandle()    // 中断服务程序
& c8 [, J( s2 A" Y{
. r8 ]6 q: r! \       Intrcnt++;          // 1ms 中断1次,可变" H5 E0 `. v9 m; s0 P( ?, D
}; C- e+ m) K7 v
void main(void)6 Q" |3 ?" q: s, }5 c% E& `
{+ Q# |: W7 D% N; ?" w& P$ I9 M0 _
       SysInit();: k3 P. }4 t- B) E+ H
    while(1)           // 每20ms 执行一次大循环- X# a% T  l* U) l
    {
# |# s" b, k# ^1 A, T        KeyRead();             // 将每个子程序都扫描一遍. ]1 ^& w: O$ Q! S1 F$ _" A
        KeyProc();" F- W4 y, f& E4 a% }
        Func1();0 M5 s1 K% D4 Y2 [: d
        Funt2();! r, j" q- L: a
        …
; H+ P) d6 B) f5 X- n1 [  r9 d        …3 e( j( @; u' J0 q* E) O
           while(1)
. A3 ~8 F+ J. @1 E        {
" W" q4 E) c+ P% a4 X4 y' d$ P              if (Intrcnt>20)     // 一直在等,直到20ms时间到
1 g' l- T7 Z5 h6 w/ O) j: v              {
% G. Y; m# t0 o" G  \! v                   Intrcnt="0";
( d1 R. S# H8 t6 }0 r                   break;       // 返回主循环
* [+ H+ S' p3 @2 L9 Y5 ^1 x7 f3 o4 p              }
; J& s0 S9 a2 A4 I! p+ ^8 n( @        }3 {- N: i2 B+ x5 ~. h; {% T7 i
       }3 d( q+ ]9 U2 b# k
}+ D5 a; Y3 t% M4 _5 G
貌似扯远了,回到我们刚才的问题,也就是怎么做按键消抖处理。我们将读按键的程序放在了主循环,也就是说,每20ms我们会执行一次KeyRead()函数来得到新的Trg 和 Cont 值。好了,下面是我的消抖部分:很简单
" v. t8 R! P* L基本架构如上,我自己比较喜欢的,一直在用。当然,和这个配合,每个子程序必须执行时间不长,更加不能死循环,一般采用有限状态机的办法来实现,具体参考其它资料咯。
( r  P9 E- S/ ?3 p懂得基本原理之后,至于怎么用就大家慢慢思考了,我想也难不到聪明的工程师们。例如还有一些处理,  A* N( H1 C# k6 S6 F/ d' h0 y
怎么判断按键释放?很简单,Trg 和Cont都为0 则肯定已经释放了。4 F2 A2 y: K0 c
+ \. t. a: H! L5 G

作者: 终南孤魂    时间: 2015-11-17 19:13
涨姿势
作者: CSL    时间: 2015-12-5 17:46
草鸟的我感觉压力大。那个cont初始化放在哪里?
作者: 天马行空6704    时间: 2015-12-22 12:39
没空看,先收藏网址再说




欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/) Powered by Discuz! X3.2