-
新型的按键扫描程序
不过我在网上游逛了很
久,
也看过不少源程序了,
没有发现这种按键处理办法的
踪迹,所以,我将他共享出来,和广大同僚们共勉。我非常坚信这种按键处理办
法的便捷和高效,
你可以移植到任何一种嵌入式处理器上面,
< br>因为
C
语言强大的
可移植性。<
/p>
同时,
这里面用到了一些分层的思想
,
在单片机当中也是相当有用的,
也是本文
的另外一个重点。
对于老鸟,
我建议直接看那两个表达式,
然后自己想想就会懂的了,
也不需
要听
我后面的自吹自擂了,
我可没有班门弄斧的意思,
hoho
~~但是对于新手,
我建
议将全文看完。因为这是实际项目中总结出来的经验,学校里面学不到的东西。
以下假设你懂
C
语言,
因为纯粹的
C
语言描述,
所以和处理器平台无关,
你可以
在
M
CS-51
,
AVR
,
PIC
,甚至是
ARM
平台上
面测试这个程序性能。当然,我自己
也是在多个项目用过,效果非常好的。
好了,工程人员的习惯,废话就应该少说,开始吧。以下我以
AVR
的
MEGA8
作为
平台讲解,没有其它原因,因为我手头上只有
AVR
的板子而已没有
51
的。用
51
也可以,只是芯片初始化部分不同,还有寄存器名字不同而已。
< br>
核心算法:
unsigned char Trg;
unsigned
char Cont;
void KeyRead( void )
{
unsigned char
ReadData = PINB^0xff; // 1
Trg =
ReadData & (ReadData ^ Cont); // 2
Cont = ReadData;
// 3
}
完了。
有没有一种
不可思议的感觉?当然,
没有想懂之前会那样,
想懂之后就会<
/p>
惊叹于这算法的精妙!!
下面是程序解释:
Trg
(
triger
)
<
/p>
代表的是触发,
Cont
(
continue
)代表的是连续按下。
1
:读
PORTB
的
端口数据,取反,然后送到
ReadData
临时变量里面保存起来。
2
:算法
1
,用来计算触发变量的。一个位与操
作,一个异或操作,我想学过
C
语言都应该懂吧?
Trg
为全局变量,其它程序可以直接引用。
<
/p>
3
:算法
2
,用
来计算连续变量。
看到这里,有种“知其然,不知其所以然
”的感觉吧?代码很简单,但是它到底
是怎么样实现我们的目的的呢?好,下面就让我们
绕开云雾看青天吧。
我们最常用的按键接法如下:
AVR
是有内部上拉功能的,但是为了说明问题,我
是特意用外部上拉电阻。那么,按键没有按下的时候,读端口数据为
1
,如果按
键按下,
那么端口读到
0
。
下面就看看具体几种情况之下,
这算法是怎么一回事。
(
1
)
没有按键的时候
端口为
0xff
,
ReadData
读端口并且取反,很显然,就是
0x00
了。
Trg =
ReadData & (ReadData ^ Cont);
(初始状态下,
p>
Cont
也是为
0
的)很
简单的数学计算,因为
ReadData
为
0
,则它和任何数“相与”,结果也是为
0
的。
Cont =
ReadData;
保存
Cont
其实就是等于
ReadData
,为
0
;
结果就是:
ReadData
=
0
;
Trg
=
0
;
Cont
=
0
;
(
2
)
第一次
PB0
按下的情况
端口数据为
0xfe
,
ReadData
读端口并且取反,很显然,就是
0x01
了。
Trg = ReadData & (ReadData ^ Cont);
因为这是第一次按下,所以
Cont
是上
p>
次的值,应为为
0
。那么这个式子的值也不
难算,也就是
Trg = 0x01 &
(0x01^
0x00) = 0x01
Cont =
ReadData = 0x01
;
结果就是:
ReadData
=
0x01
;
Trg
=
0x01
;
Trg
只会在这个时候对应位的值为
1
,其它时候都为
0
Cont
=
0x01
;
(
3
)
PB0
按着不松(长按键)的情况
端口数据为
0xfe
,
ReadDat
a
读端口并且取反是
0x01
了。
Trg =
ReadData & (ReadData ^ Cont);
因为这是连续按下,
所以
Cont
是上次
的值,应为为
p>
0x01
。那么这个式子就变成了
Trg
= 0x01 & (0x01^0x01) = 0x0
0
Cont = ReadData =
0x01
;
结果就是:
ReadData
=
0x01
;
Trg
=
0x00
;
Cont
=
0x01
;
因为现在按键是长按着
,所以
MCU
会每个一定时间(
20m
s
左右)不断的执行这
个函数,那么下次执行的时候情况会是怎
么样的呢?
ReadData
=
0x01
;这个不会变,因为按键没有松开
Trg
=
ReadData &
(ReadData ^ Cont)
=
0x01 &
(0x01 ^ 0x01) = 0
,
只要
按键没有松开,这个
Trg
值永远为
< br> 0
!!!
Cont
=
0x01
;只要按键没有松开,这
个值永远是
0x01
!!
(
4
)
按键松开的情况
端口数据为
0xff
,
ReadData
读端口并且取反是
0x00
了。
Trg =
ReadData & (ReadData ^ Cont) = 0x00 & (0x00^0x01)
= 0x00
Cont = ReadData =
0x00
;
结果就是:
ReadData
=
0x00
;
Trg
=
0x00
;
Cont
=
0x00
;
很显然,这个回到了初始状态,也就是没有按键按下的状态。
总结一下,不知道想懂了没有?其实很简单,答案如下:
Trg
表示的就是触发的意思,
也就
是跳变,
只要有按键按下
(电平从
1<
/p>
到
0
的跳
变),
那么
Trg
在对应按键的位上面会置一,我们用了
PB0
则
Trg
的值为
p>
0x01
,
类似,
如果我们
PB7
按下的话,
Trg
的值就应该为
0x80
,
这个很好理解,
还有,
最关键的地方,
Trg
的值每次按下只会出现一次,
然后立刻
被清除,
完全不需要
人工去干预。所以按键功能处理程序不会重
复执行,省下了一大堆的条件判断,
这个可是精粹哦!!
Con
t
代表的是长按键,如果
PB0
按着不
放,那么
Cont
的值
就为
0x01
,
相对应,
P
B7
按着不放,
那么
Cont
的值应该为
0x80
,
同样很好理解。
如果还是想不懂的话,可以自己演算一下那
两个表达式,应该不难理解的。
因为有了这个支持,那么按键处理就变得很爽了,下面看应用:
应用一:一次触发的按键处理
假设
PB0
为蜂鸣器按键,按一下,蜂鸣器
beep
的响一声。这个很简单,但是大
家以前是怎么做的呢?
对比一下看谁的方便?
#define KEY_BEEP
0x01
void KeyProc(void)
{
if (Trg & KEY_BEEP) //
如果按下的是
KEY_BEEP
{
Beep(); //
执行蜂鸣器处理函数
}
}
怎么样?够和谐不?记得前面解释说
Trg
的精粹是什么?精粹就是只会出现一
次。所以你按下
按键的话,
Trg & KEY_BEEP
为“真”的情况只
会出现一次,所
以处理起来非常的方便,蜂鸣器也不会没事乱叫,
hoho
~~~
或者你会认为这个处理简单,没有问题,我们继续。
应用
2
:长按键的处理
项目中经常会遇到一些要求,例如:一个按键如果短按一下执行功能
A
,如果长
按
2
秒不放的话会执行功能
B
,又或者是要求
3
秒按着不放,计数连加什么什么
的功能,很实
际。不知道大家以前是怎么做的呢?我承认以前做的很郁闷。
但是看我们这里怎么处理吧,或许你会大吃一惊,原来程序可以这么简单
这里具个简单例子,为了只是说明原理,
PB0
是模式按键,短按则切换模式,
PB
1
就是加,如果长按的话则连加(玩过电子表吧?没错,就是那个!)
#define KEY_MODE 0x01 //
模式按键
#define
KEY_PLUS 0x02 //
加
void KeyProc(void)
{
if (Trg & KEY_MODE) //
如果按下的是
KEY_MODE
,而且你常按这按键
也没有用,
{
//
它是不会执行第二次的哦
,
必须先松开再按下
Mode++; //
模式寄存器加
1
p>
,当然,这里只是演示,你可以
执行你想
//
执行的任何代码
}
if (Cont & KEY_PLUS) //
如果“加”按键被按着不放
{
cnt_plus++; //
计时
if
(cnt_plus > 100) // 20ms*100 = 2S
如果时间到
{
Func(); //
你需要的执行的程序
}
}
}
不知道各位感觉如何?我觉得还是挺简单的完成了任务,
当然,
作为演示用代码。
应用
3
:点触型按键和开关型按键的混合使用
点触形按键估计用的最多,
特别是单片机。
开关型其实
也很常见,
例如家里的电
灯,那些按下就不松开,除非关。这是
两种按键形式的处理原理也没啥特别,但
是你有没有想过,
如果
一个系统里面这两种按键是怎么处理的?我想起了我以前
的处理,
分开两个非常类似的处理程序,
现在看起来真的是笨的不行了,
但是也
没有办法啊,结构决定了程序。不过现在好了,用上面介绍的办法,很轻松就可
以搞定。
原理么?可能你也会想到
,
对于点触开关,
按照上面的办法处理一次按下和长按,
对于开关型,
我们只需要处理
Cont
就
OK
了,
为什么?
很简单嘛,
把它当成是一
个长按键,这样就找到了共同点,屏蔽
了所有的细节。程序就不给了,完全就是
应用
2
的内容,在这里提为了就是说明原理~~
好了,<
/p>
这个好用的按键处理算是说完了。
可能会有朋友会问,
为什么不说延时消
抖问题?哈哈,被看穿了。果然不能偷懒。下面谈谈这个
问题,顺便也就非常简
单的谈谈我自己用时间片轮办法,以及是如何消抖的。
延时消抖的办法是非常传统,也就是
第一次判断有按键,延时一定的时间(一
般习惯是
20ms
)再读端口,如果两次读到的数据一样,说明了是真正的按键,
而不是抖动,则进入按键处理程序。
当然,不
要跟我说你
delay
(
20
)那样去死循环去,真是那样的话,我衷心的建
议你先放下手上所有的东
西,
好好的去了解一下操作系统的分时工作原理,
大概
知道思想就可以,不需要详细看原理,否则你永远逃不出“菜鸟”这个圈子。当
< br>然我也是菜鸟。我的意思是,真正的单片机入门,是从学会处理多任务开始的,
这
个也是学校程序跟公司程序的最大差别。
当然,
本文不是专门说
这个的,
所以
也不献丑了。
我的主程序架构是这样的:
volatile unsigned char Intrcnt;
void InterruptHandle() //
中断服务程序
{
Intrcnt++; // 1ms
中断
1
次,可变
}
void main(void)
{
SysInit();
while(1) //
每
20ms
执行一次大循环
{
KeyRead(); //
将每个子程序都扫描一遍
KeyProc();
Func1();
Funt2();
?
?
while(1)
{
if (Intrcnt>20) //
一直在等,直到
20ms
时间到
{
Intrcnt=
break;
//
返回主循环
}
}
}
}
貌似扯远了,
回到我们刚才的问
题,
也就是怎么做按键消抖处理。
我们将读按键
的程序放在了主循环,也就是说,每
20ms
我们会执
行一次
KeyRead()
函数来得
到
新的
Trg
和
Cont
值。好了,下面是我的消抖部分:很简单
< br>基本架构如上,我自己比较喜欢的,一直在用。当然,和这个配合,每个子程序
必
须执行时间不长,
更加不能死循环,
一般采用有限状态机的办法
来实现,
具体
参考其它资料咯。
<
/p>
懂得基本原理之后,
至于怎么用就大家慢慢思考了,
我想也难不到聪明的工程师
们。例如还有一些处理,
怎么判断按键释放?很简单,
Trg
和
Cont
都为
0
则肯定已经释放了。
这个需要有定时
(按键间隔)
调用函数,
完成去抖,
区别单次和长按,
好的思路。
我想矩阵键盘也可以处理,只有键盘返回的码是唯一的,把
PINB
换成
getkey
之类的函数。我想
这个可能用来分析脉冲信号,比如红外遥控信号
最简单矩阵键盘扫描程序
这是站长初
学者写的最简单、
最详细、
效率最高的矩阵键盘扫描程序,
p>
只用了四条常用命令
(
MOV/
送数、
JB/
高电平转移、
JMP/
直接转移、
RET/
子程序
返回)
,保证初学者一看就懂!
本程序已经在本站电子实验板上
验证通过,
占用CPU时间少,
效率高,
被选作
单片机
的测
试程序!
矩阵按键扫描程
序是一种节省
IO
口的方法
,
按键数目越多节省
IO
口就越可观,本程序
p>
的思路跟书上一样:先判断某一列(行)是否有按键按下,再判断该行(列)是那一只键按<
/p>
下。但是,在程序的写法上,站长采用了最简单的方法,使得程序效率最高。
本程序中,
如果检测到某键按下了,
就不再检测其它的按键,
这完全能满足绝大多数需
要,又能节省大量的
CPU
时间。另外,本人认为键盘用延时程序来消除抖动,完全是浪费
时间。试
想,如果不用中断执行(用中断执行需要更多的硬件资源)的方法来扫描键盘,每
秒钟扫
描20-100次,
每次都要延时10-20MS的话,
我们的
单片机
还有多少时间
做正事呢?
其实,延时的
这段时间,
CPU
可以做其它的事呀。所以,本键盘扫描程序的
前面后面
都可以加入少少代码,
既可以达到完美的消抖动效果,
又可以扩展其它的功能
(例如按键封
锁
、按键长按等按键功能复用!
)
字串
2
本键盘扫描子程序名叫
key
,每次要
扫描时用
call
key
调用即可。以下子程序内容:
key:mov
p0,#0000
1111b;
上四位和下四位分别为行和列
,
< br>所以送出高低电压检查有没有按键
按下
jmp
k10;
跳到
K10
处开始扫描,这里可以改成其它条件转移指令来决定本次扫描是否要继<
/p>
续,
例如减
1
为
0
转移或者位为
1
或
0
才转移,
这主要用来增加功能
,
确认上一按键功能是
否完成?是否相当于经过了延时?是否要
封锁键盘?
goend:jmp
k
end;
如果上面判断本次不执行键盘扫描程序,
则立即转到程
序尾部,
不要浪费
C
PU
的时间
k10:jb
p
0.0,k20;
扫描正式开始,先检查列
1
< br>四个键是否有键按下,如果没有,则跳到
K20
检查列<
/p>
2
k11:mov
p0,#1110
1111b;
列
1
有键按下时
,P0.0
变低
,
到底
是那一个键按下?现在分别输出各行
低电平
jb
p0.0,k12;
该行的键不
按下时,
p0.0
为高电平
,
跳到到
K12,
检查其它的行
< br>
mov
r1,#1;
如果正
好是这行的键按下,将寄存器
R0
写下
1
,表示
1
号键按下了
k12:mov
p0,#11011111b
jb
p0.0,k13
mov <
/p>
r1,#2;
如果正好是这行的键按下,将寄存器
R0
写下
2
,表示
2
号键按下了
k13:mov
p0,#10111111b
jb
p0.0,k14
mov <
/p>
r1,#3;
如果正好是这行的键按下,将寄存器
R0
写下
3
,表示
3
号键按下了
字串
3
k14:mov
p0,#01111111b
jb
p0.0,kend;
如果现在
四个键都没有按下,可能按键松开或干扰,退出扫描(以后相同)
mov
r1,#4
如果正好是这行的
键按下,将寄存器
R0
写下
4
,表示
4
号键按下了
jmp
kend;
已经找到按下的键
,跳到结尾吧
k20:jb
p>
p0.1,k30;
列
2
< br>检查为高电平再检查列
3
、
4
k21:mov
p0,#1110
1111b;
列
2
有健按下时,
P0.0
会变低,到底是那一行的键按下呢?分别输
< br>出行的低电平
jb
p0.1
,k22;
该行的键不按下时
p0.0
为高电平,跳到到
K22
,检查另外三行
mov
r1,#5;
如果正好是
这行的键按下,将寄存器
R0
写下
5<
/p>
,表示
5
号键按下了(以后相同,
不再重复了)
k22:mov
p0,#11011111b
-
-
-
-
-
-
-
-
-
上一篇:蓝桥杯ACM决赛经典试题及其详解
下一篇:return 0 和1的解释