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 H
unsigned char Trg;
7 a9 i; V7 W- Z3 w: r
unsigned char Cont;
/ `* a# g( ^) N1 I9 X7 B$ Z4 S
void KeyRead( void )
8 T6 F9 ^0 i# b9 l7 p+ }
{
/ s1 y, O3 { k' T! Z
unsigned char ReadData = PINB^0xff; // 1
7 \! u5 t" x! D/ ~$ j
Trg = ReadData & (ReadData ^ Cont); // 2
1 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 u
Trg(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( w
Trg = ReadData & (ReadData ^ Cont); (初始状态下,Cont也是为0的)很简单的数学计算,因为ReadData为0,则它和任何数“相与”,结果也是为0的。
2 r: N, y5 B/ g) u; `% c
Cont = ReadData; 保存Cont 其实就是等于ReadData,为0;
; T" K, d1 D& _/ G6 m3 e) _7 I6 g, V/ t- L
结果就是:
4 d2 [4 g! N1 h" w; e
ReadData = 0;
9 R# G& u# L5 f+ R6 N
Trg = 0;
$ ]5 G, G' ~# ?% F2 G: q: }( I* Q
Cont = 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; v
Cont = 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 j
ReadData = 0x01;
. W# N7 I, T5 m5 H* H4 L. f
Trg = 0x00;
. ~. T1 J" P' K
Cont = 0x01;
! s4 ]* {+ v5 {' E( r
因为现在按键是长按着,所以MCU会每个一定时间(20ms左右)不断的执行这个函数,那么下次执行的时候情况会是怎么样的呢?
" T, N8 i8 a" M* z7 I" @1 \) h
ReadData = 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, v
Cont = 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 X
void 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. l
void 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 ^/ G
void 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