|
EDA365欢迎您!
您需要 登录 才可以下载或查看,没有帐号?注册
x
新型的按键扫描程序
) T, _8 V& d+ E% q" M+ ~3 X0 J不过我在网上游逛了很久,也看过不少源程序了,没有发现这种按键处理办法的踪迹,所以,我将他共享出来,和广大同僚们共勉。我非常坚信这种按键处理办法的便捷和高效,你可以移植到任何一种嵌入式处理器上面,因为C语言强大的可移植性。
* y* ^( w8 P2 ?% @6 P$ C同时,这里面用到了一些分层的思想,在单片机当中也是相当有用的,也是本文的另外一个重点。6 P8 E. Z8 ?) {, ?4 n/ b. _
对于老鸟,我建议直接看那两个表达式,然后自己想想就会懂的了,也不需要听我后面的自吹自擂了,我可没有班门弄斧的意思,hoho~~但是对于新手,我建议将全文看完。因为这是实际项目中总结出来的经验,学校里面学不到的东西。* E! a2 ?- I0 }& t0 W. p8 G
以下假设你懂C语言,因为纯粹的C语言描述,所以和处理器平台无关,你可以在MCS-51,AVR,PIC,甚至是ARM平台上面测试这个程序性能。当然,我自己也是在多个项目用过,效果非常好的。/ c0 j) P9 k8 U5 [. x$ A
好了,工程人员的习惯,废话就应该少说,开始吧。以下我以AVR的MEGA8作为平台讲解,没有其它原因,因为我手头上只有AVR的板子而已没有51的。用51也可以,只是芯片初始化部分不同,还有寄存器名字不同而已。& B. B* E' u' X# O. m) p
核心算法:9 l8 I% n+ I) }3 _# X
unsigned char Trg;* Q$ L( e( {- V# i/ n5 z8 a) ]
unsigned char Cont;; ?6 ]' _2 J: q0 i; \$ E5 \) T( w: e
void KeyRead( void )
2 B5 r' x" h/ ]{
D4 p# Z* m7 Z6 M) k; N unsigned char ReadData = PINB^0xff; // 1
7 |8 S2 M* e2 A. f9 Q: V Q Trg = ReadData & (ReadData ^ Cont); // 2
7 I+ Y9 c6 ?: Q3 v9 ]& h: } Cont = ReadData; // 3$ x/ ?" X& K+ D# \
}. {2 i/ M. |3 Z+ m3 \
完了。有没有一种不可思议的感觉?当然,没有想懂之前会那样,想懂之后就会惊叹于这算法的精妙!!
; \! Z# f, L5 r& F- N% N* @下面是程序解释:
! X1 a. l- v# L9 d7 vTrg(triger) 代表的是触发,Cont(continue)代表的是连续按下。
4 d# U* b& h$ n2 u; U# o1:读PORTB的端口数据,取反,然后送到ReadData 临时变量里面保存起来。
! p l! j: B0 Q' |" O2:算法1,用来计算触发变量的。一个位与操作,一个异或操作,我想学过C语言都应该懂吧?Trg为全局变量,其它程序可以直接引用。
! d6 n( f3 K. g/ G& u+ c* n/ W3:算法2,用来计算连续变量。- h, E2 O) e' x i
看到这里,有种“知其然,不知其所以然”的感觉吧?代码很简单,但是它到底是怎么样实现我们的目的的呢?好,下面就让我们绕开云雾看青天吧。& s0 T, f( a) D/ `! p
我们最常用的按键接法如下:AVR是有内部上拉功能的,但是为了说明问题,我是特意用外部上拉电阻。那么,按键没有按下的时候,读端口数据为1,如果按键按下,那么端口读到0。下面就看看具体几种情况之下,这算法是怎么一回事。
: j( M3 g0 c6 n9 J, E' K(1) 没有按键的时候1 O9 S+ u' U9 F+ @, q
端口为0xff,ReadData读端口并且取反,很显然,就是 0x00 了。
, r2 P+ W3 ~5 g' D WTrg = ReadData & (ReadData ^ Cont); (初始状态下,Cont也是为0的)很简单的数学计算,因为ReadData为0,则它和任何数“相与”,结果也是为0的。: n) j) |3 z* }/ g( X" _
Cont = ReadData; 保存Cont 其实就是等于ReadData,为0;
) M8 h+ m' ?! ^( D3 [- `' J结果就是:2 [/ F/ r- `, t& e4 A* C6 F, I
ReadData = 0;6 C& A; o/ c$ P( D6 Q& W5 t
Trg = 0;
! R0 S" {7 U9 a E6 TCont = 0;% [& d. J# Y6 o( x+ L) i
(2) 第一次PB0按下的情况- b' P+ x, K d8 ]/ G( s; F
端口数据为0xfe,ReadData读端口并且取反,很显然,就是 0x01 了。$ \7 s3 e# x* _8 G: Z6 a
Trg = ReadData & (ReadData ^ Cont); 因为这是第一次按下,所以Cont是上次的值,应为为0。那么这个式子的值也不难算,也就是 Trg = 0x01 & (0x01^0x00) = 0x01
) v% h8 S! \# u' G2 _' q7 _8 J( F7 oCont = ReadData = 0x01;* m3 X( r) r) C' l7 b/ I" h/ _
结果就是:
& i3 w/ J6 F' R7 _/ o* sReadData = 0x01;
! K0 M# o) Z1 x% n2 PTrg = 0x01;Trg只会在这个时候对应位的值为1,其它时候都为0
9 ~* j- y) N/ F5 g, ]Cont = 0x01;
$ l, p1 D8 M2 i& O! H5 F; C(3) PB0按着不松(长按键)的情况+ b' P0 b" s( M
端口数据为0xfe,ReadData读端口并且取反是 0x01 了。
5 l& ]! q! V1 o1 TTrg = ReadData & (ReadData ^ Cont); 因为这是连续按下,所以Cont是上次的值,应为为0x01。那么这个式子就变成了 Trg = 0x01 & (0x01^0x01) = 0x00
a& g; N; R k; [6 [8 cCont = ReadData = 0x01;
# [" C) B, {' C0 I结果就是:
& X6 v* J5 y+ CReadData = 0x01;
, K8 u# n2 G3 {: |: y+ @; [, O/ v wTrg = 0x00;3 b5 Q+ O/ b& E; W% g
Cont = 0x01;; Y; s- x2 t" ?8 Z# L7 N+ v% U
因为现在按键是长按着,所以MCU会每个一定时间(20ms左右)不断的执行这个函数,那么下次执行的时候情况会是怎么样的呢?
; }& ^# x3 X% b7 P3 z- @# |7 G2 dReadData = 0x01;这个不会变,因为按键没有松开
. `2 n: U( a8 O5 a9 F& T! R4 FTrg = ReadData & (ReadData ^ Cont) = 0x01 & (0x01 ^ 0x01) = 0 ,只要按键没有松开,这个Trg值永远为 0 !!!
* t3 p2 a* E6 U0 }% V% |0 mCont = 0x01;只要按键没有松开,这个值永远是0x01!!) t6 i1 [! X, f2 \/ G2 M
(4) 按键松开的情况2 |0 q7 Z& f. O. L1 v
端口数据为0xff,ReadData读端口并且取反是 0x00 了。* |3 a- P# G5 x8 z: {
Trg = ReadData & (ReadData ^ Cont) = 0x00 & (0x00^0x01) = 0x00
B- |# d- F' R$ RCont = ReadData = 0x00;+ |$ w( X# N8 n9 L9 F1 z# k
结果就是:
$ b. r: P. R6 K% {; e" _, jReadData = 0x00;
" a1 x" @: Z5 A9 e+ E# m! K1 JTrg = 0x00;
! m: D" Q2 H! U) a M7 VCont = 0x00;
# \% e- U; @! M$ Z0 p) d很显然,这个回到了初始状态,也就是没有按键按下的状态。2 {* j+ k: J \5 u+ _4 E* W
总结一下,不知道想懂了没有?其实很简单,答案如下:
, w/ Q. U0 W0 I, U8 t+ fTrg 表示的就是触发的意思,也就是跳变,只要有按键按下(电平从1到0的跳变),那么Trg在对应按键的位上面会置一,我们用了PB0则Trg的值为0x01,类似,如果我们PB7按下的话,Trg 的值就应该为 0x80 ,这个很好理解,还有,最关键的地方,Trg 的值每次按下只会出现一次,然后立刻被清除,完全不需要人工去干预。所以按键功能处理程序不会重复执行,省下了一大堆的条件判断,这个可是精粹哦!!Cont代表的是长按键,如果PB0按着不放,那么Cont的值就为 0x01,相对应,PB7按着不放,那么Cont的值应该为0x80,同样很好理解。* E1 _8 @! b6 U9 T; X+ V" h
如果还是想不懂的话,可以自己演算一下那两个表达式,应该不难理解的。, M; T$ z H7 `% _' p0 F
因为有了这个支持,那么按键处理就变得很爽了,下面看应用:
- }4 l5 ]8 h7 }应用一:一次触发的按键处理' f+ K. @- r5 u/ a# c
假设PB0为蜂鸣器按键,按一下,蜂鸣器beep的响一声。这个很简单,但是大家以前是怎么做的呢?对比一下看谁的方便?& C4 D$ N7 M; x g* A& r
#define KEY_BEEP 0x017 d1 q" X; c0 h/ o$ ~+ l% @) N
void KeyProc(void)( i8 T; \; }9 B! d \
{0 J; G6 z: J7 O
if (Trg & KEY_BEEP) // 如果按下的是KEY_BEEP
* P: Z. i0 T+ x* V1 i {- T0 s4 }+ e/ n: x9 U( l. s8 n, o: w
Beep(); // 执行蜂鸣器处理函数6 A( l& S& }! m0 Y/ l# @ L
}, Z B. L; s) F+ C- b
}1 ?# p, b4 x3 K- W$ Z
怎么样?够和谐不?记得前面解释说Trg的精粹是什么?精粹就是只会出现一次。所以你按下按键的话,Trg & KEY_BEEP 为“真”的情况只会出现一次,所以处理起来非常的方便,蜂鸣器也不会没事乱叫,hoho~~~
8 _( b6 U# i; R& ?! u. F. g或者你会认为这个处理简单,没有问题,我们继续。
* z( B& d4 F& V3 L8 y应用2:长按键的处理/ d) P8 d1 o8 n2 Z
项目中经常会遇到一些要求,例如:一个按键如果短按一下执行功能A,如果长按2秒不放的话会执行功能B,又或者是要求3秒按着不放,计数连加什么什么的功能,很实际。不知道大家以前是怎么做的呢?我承认以前做的很郁闷。, L; a4 _* G6 q( {0 k6 J
但是看我们这里怎么处理吧,或许你会大吃一惊,原来程序可以这么简单
x3 M8 ^. e5 E ?# g$ K这里具个简单例子,为了只是说明原理,PB0是模式按键,短按则切换模式,PB1就是加,如果长按的话则连加(玩过电子表吧?没错,就是那个!)( R1 b% c2 q* d
#define KEY_MODE 0x01 // 模式按键
; _! L: B+ R* W#define KEY_PLUS 0x02 // 加
! Z' b+ G1 q* h. d% N' Ivoid KeyProc(void)' v9 {6 W% k/ S' I9 m% l1 v* g
{+ H- H5 x$ n' c; K) ]4 {, ^
if (Trg & KEY_MODE) // 如果按下的是KEY_MODE,而且你常按这按键也没有用,0 F3 O" O C2 d1 f
{ //它是不会执行第二次的哦 , 必须先松开再按下
& A6 _7 e2 Y$ n, Y7 N Mode++; // 模式寄存器加1,当然,这里只是演示,你可以执行你想
5 n- a6 \5 T3 l6 }. R // 执行的任何代码
- x& I: |# k' x$ U2 ]& A" c }8 w, z% L9 x d! W q
if (Cont & KEY_PLUS) // 如果“加”按键被按着不放; o8 r, v1 U2 c4 K% B
{8 J0 k; x2 r7 u y" q/ |
cnt_plus++; // 计时
) O8 Y) I; M+ z* E8 v7 { if (cnt_plus > 100) // 20ms*100 = 2S 如果时间到
( w i/ H0 X X4 F# `3 f4 t. Y {0 B! H" t+ V9 W, q# m/ R
Func(); // 你需要的执行的程序' A. w+ e- S6 s# d# S
}
' ]7 O5 Z: W4 {* j9 _8 ]2 \/ r }
2 p2 t1 y4 n5 n7 X}! U' A" L6 ~8 u( |9 n% k/ @0 X
不知道各位感觉如何?我觉得还是挺简单的完成了任务,当然,作为演示用代码。
% c% o8 m* r# N* P+ v5 x j应用3:点触型按键和开关型按键的混合使用
& N& N: M. ] }; |4 s* o点触形按键估计用的最多,特别是单片机。开关型其实也很常见,例如家里的电灯,那些按下就不松开,除非关。这是两种按键形式的处理原理也没啥特别,但是你有没有想过,如果一个系统里面这两种按键是怎么处理的?我想起了我以前的处理,分开两个非常类似的处理程序,现在看起来真的是笨的不行了,但是也没有办法啊,结构决定了程序。不过现在好了,用上面介绍的办法,很轻松就可以搞定。
7 E: I$ P: L4 V+ M& e1 U+ M2 I$ N原理么?可能你也会想到,对于点触开关,按照上面的办法处理一次按下和长按,对于开关型,我们只需要处理Cont就OK了,为什么?很简单嘛,把它当成是一个长按键,这样就找到了共同点,屏蔽了所有的细节。程序就不给了,完全就是应用2的内容,在这里提为了就是说明原理~~) Y+ {6 P) C# q% |! X2 M& W
好了,这个好用的按键处理算是说完了。可能会有朋友会问,为什么不说延时消抖问题?哈哈,被看穿了。果然不能偷懒。下面谈谈这个问题,顺便也就非常简单的谈谈我自己用时间片轮办法,以及是如何消抖的。* E7 n& ^# x: w
延时消抖的办法是非常传统,也就是 第一次判断有按键,延时一定的时间(一般习惯是20ms)再读端口,如果两次读到的数据一样,说明了是真正的按键,而不是抖动,则进入按键处理程序。
. [+ Q/ N$ ?, J6 \! l当然,不要跟我说你delay(20)那样去死循环去,真是那样的话,我衷心的建议你先放下手上所有的东西,好好的去了解一下操作系统的分时工作原理,大概知道思想就可以,不需要详细看原理,否则你永远逃不出“菜鸟”这个圈子。当然我也是菜鸟。我的意思是,真正的单片机入门,是从学会处理多任务开始的,这个也是学校程序跟公司程序的最大差别。当然,本文不是专门说这个的,所以也不献丑了。/ a; J( {/ ?* v9 ~' K
我的主程序架构是这样的:
( y9 b |. ?' m1 ]* R$ hvolatile unsigned char Intrcnt;* U6 g: \" r' v7 G
void InterruptHandle() // 中断服务程序
' _: J% s# y9 U- a4 Z% x( A% M{# q4 D d9 f2 ~; j5 A- j) U' |
Intrcnt++; // 1ms 中断1次,可变
3 w7 e6 r) _5 {- ~}/ u8 c" f9 q. l2 \
void main(void)- U8 ^0 i c. F8 V
{
- u* _" M$ s) E9 A0 Y6 Y& { SysInit();8 l) _/ }; p4 c7 E$ H3 e5 y
while(1) // 每20ms 执行一次大循环
. N% w. \0 n, ~/ M7 [/ ^% e6 z {, {$ N% I0 b" d: H
KeyRead(); // 将每个子程序都扫描一遍% V7 {: G7 L3 C6 s# R" r2 F: j9 r
KeyProc();5 p5 u7 ^7 ~+ A
Func1();
6 b8 M# p- U3 o* V1 \2 w/ e- | Funt2();
) a$ a. Q8 g; E( q …! v- c h, d& Z- j
…
/ E3 m. d! n7 E9 c* K2 K j, ? while(1): H1 c# j" L# z# F
{
; T9 u; D8 L7 @7 ^( l if (Intrcnt>20) // 一直在等,直到20ms时间到
- S) G( |2 S. A4 ] {( I4 N( u1 N- ^5 \4 m8 H( C
Intrcnt="0";
+ g, ?1 Q2 o9 u2 r/ l ~$ `& Q5 { break; // 返回主循环
2 H. ~9 B+ P1 u' ^ }
% N* G+ {7 ]' s& g/ k7 F7 g4 n _ }
% V. D( }% B# A) {1 V2 a3 j# F: { }
$ M2 M- V" N" ~) G3 o. T2 E}
w, e9 v) K' M) y: t( R: W貌似扯远了,回到我们刚才的问题,也就是怎么做按键消抖处理。我们将读按键的程序放在了主循环,也就是说,每20ms我们会执行一次KeyRead()函数来得到新的Trg 和 Cont 值。好了,下面是我的消抖部分:很简单 j" }' |, T+ ]7 w1 k
基本架构如上,我自己比较喜欢的,一直在用。当然,和这个配合,每个子程序必须执行时间不长,更加不能死循环,一般采用有限状态机的办法来实现,具体参考其它资料咯。
! ^" I1 Q6 W0 j0 e6 t懂得基本原理之后,至于怎么用就大家慢慢思考了,我想也难不到聪明的工程师们。例如还有一些处理,
/ b, S/ q: v8 N. o- U怎么判断按键释放?很简单,Trg 和Cont都为0 则肯定已经释放了。
3 |# H! ]: W1 u
9 s0 T2 o6 r1 c1 m' Z0 T8 @ |
|