OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 26711|回复: 27

推荐---按键扫描程序--经典 值得细细品

[复制链接]

14

主题

101

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
493
金钱
493
注册时间
2011-9-6
在线时间
3 小时
发表于 2011-10-16 00:08:50 | 显示全部楼层 |阅读模式
 转载《按键扫描程序 》
注:红色为自己评注   蓝色为重点部分

以下假设你懂C语言,因为纯粹的C语言描述,所以和处理器平台无关,你可以在MCS-51,AVR,PIC,甚至是ARM平台上面测试这个程序性能。 以下以AVR的MEGA8作为平台讲解,没有其它原因,因为我手头上只有AVR的板子而已没有51的。用51也可以,只是芯片初始化部分不同,还有寄存器名字不同而已。
核心算法:
unsigned char Trg;
unsigned char Cont;
void KeyRead( void )
{
    unsigned char ReadData = INB^0xff;   // 1
    Trg = ReadData & (ReadData ^ Cont);      // 2
    Cont = ReadData;                                // 3
}
下面是程序解释:   
Trg(triger) 代表的是触发,Cont(continue)代表的是连续按下。
1:读PORTB的端口数据,取反,然后送到ReadData 临时变量里面保存起来。(端口值与0XFF按位异或,有按键按下为0,异或后相应的位就为1,相当于将读取的端口值取反)
2:算法1,用来计算触发变量的。一个位与操作,一个异或操作,我想学过C语言都应该懂吧?Trg为全局变量,其它程序可以直接引用。
3:算法2,用来计算连续变量。
看到这里,有种“知其然,不知其所以然”的感觉吧?代码很简单,但是它到底是怎么样实现我们的目的的呢?好,下面就让我们绕开云雾看青天吧。

我们最常用的按键接法如下:AVR是有内部上拉功能的,但是为了说明问题,我是特意用外部上拉电阻。(STM32可以将端口设置为输入上拉模式)那么,按键没有按下的时候,读端口数据为1,如果按键按下,那么端口读到0。下面就看看具体几种情况之下,这算法是怎么一回事。
(1) 没有按键的时候
端口为0xff,ReadData读端口并且取反,很显然,就是 0x00 了。 (0XFF^0XFF=0X00)
Trg = ReadData & (ReadData ^ Cont); (初始状态下,Cont也是为0的)很简单的数学计算,因为ReadData为0,则它和任何数“相与”,结果也是为0的。
Cont = ReadData; 保存Cont 其实就是等于ReadData,为0;
结果就是:
ReadData = 0;
Trg = 0;
Cont = 0;

(2)第一次PB0按下的情况
端口数据为0xfe,ReadData读端口并且取反,很显然,就是 0x01 了。 (0XFE^0XFF=0X01)
Trg = ReadData & (ReadData ^ Cont); 因为这是第一次按下,所以Cont是上次的值,应为为0。那么这个式子的值也不难算,也就是 Trg = 0x01 & (0x01^0x00) = 0x01
Cont = ReadData = 0x01;
结果就是:
ReadData = 0x01;
Trg = 0x01;Trg只会在这个时候对应位的值为1,其它时候都为0
Cont = 0x01;

(3)PB0按着不松(长按键)的情况
端口数据为0xfe,ReadData读端口并且取反是 0x01 了。
Trg = ReadData & (ReadData ^ Cont); 因为这是连续按下,所以Cont是上次的值,应为为0x01。那么这个式子就变成了 Trg = 0x01 & (0x01^0x01) = 0x00
Cont = ReadData = 0x01;
结果就是:
ReadData = 0x01;
Trg = 0x00;
Cont = 0x01;
因为现在按键是长按着,所以MCU会每个一定时间(20ms左右)不断的执行这个函数,那么下次执行的时候情况会是怎么样的呢?
ReadData = 0x01;这个不会变,因为按键没有松开
Trg = ReadData & (ReadData ^ Cont) = 0x01 & (0x01 ^ 0x01) = 0 ,只要按键没有松开,这个Trg值永远为 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到0的跳变),那么Trg在对应按键的位上面会置一,我们用了PB0则Trg的值为0x01,类似,如果我们PB7按下的话,Trg 的值就应该为 0x80 ,这个很好理解,还有,最关键的地方,Trg 的值每次按下只会出现一次,然后立刻被清除,完全不需要人工去干预。所以按键功能处理程序不会重复执行,省下了一大堆的条件判断,这个可是精粹哦!!Cont代表的是长按键,如果PB0按着不放,那么Cont的值就为 0x01,相对应,PB7按着不放,那么Cont的值应该为0x80,同样很好理解。 

因为有了这个支持,那么按键处理就变得很爽了,下面看应用:
应用一:一次触发的按键处理
假设PB0为蜂鸣器按键,按一下,蜂鸣器beep的响一声。这个很简单,但是大家以前是怎么做的呢?对比一下看谁的方便?
#define KEY_BEEP 0x01
void KeyProc(void)
{
       if (Trg & KEY_BEEP) // 如果按下的是KEY_BEEP
    {
         Beep();            // 执行蜂鸣器处理函数
    }
}
当你按下按键的话,Trg & KEY_BEEP 为“真”的情况只会出现一次,所以处理起来非常的方便,蜂鸣器也不会没事乱叫,hoho~~~ 

应用2:长按键的处理
项目中经常会遇到一些要求,例如:一个按键如果短按一下执行功能A,如果长按2秒不放的话会执行功能B,又或者是要求3秒按着不放,计数连加什么什么的功能,很实际。
这里具个简单例子,为了只是说明原理,PB0是模式按键,短按则切换模式,PB1就是加,如果长按的话则连加(玩过电子表吧?没错,就是那个!)
#define KEY_MODE 0x01    // 模式按键
#define KEY_PLUS 0x02     // 加
void KeyProc(void)
{
       if (Trg & KEY_MODE) // 如果按下的是KEY_MODE,而且你常按这按键也没有用,
    {                    //它是不会执行第二次的哦 , 必须先松开再按下
         Mode++;         // 模式寄存器加1,当然,这里只是演示,你可以执行你想
                         // 执行的任何代码
    }
    if (Cont & KEY_PLUS) // 如果“加”按键被按着不放
    {
         cnt_plus++;       // 计时
         if (cnt_plus > 100) // 20ms*100 = 2S 如果时间到
         {
              Func();      // 你需要的执行的程序
         }          
    }


应用3:点触型按键和开关型按键的混合使用
点触形按键估计用的最多,特别是单片机。开关型其实也很常见,例如家里的电灯,那些按下就不松开,除非关。这是两种按键形式的处理原理也没啥特别,但是你有没有想过,如果一个系统里面这两种按键是怎么处理的?我想起了我以前的处理,分开两个非常类似的处理程序,现在看起来真的是笨的不行了,但是也没有办法啊,结构决定了程序。
原理么?可能你也会想到,对于点触开关,按照上面的办法处理一次按下和长按,对于开关型,我们只需要处理Cont就OK了,为什么?很简单嘛,把它当成是一个长按键,这样就找到了共同点,屏蔽了所有的细节。程序就不给了,完全就是应用2的内容,在这里提为了就是说明原理~~
好了,这个好用的按键处理算是说完了。可能会有朋友会问,为什么不说延时消抖问题?哈哈,被看穿了。果然不能偷懒。下面谈谈这个问题,顺便也就非常简单的谈谈我自己用时间片轮办法,以及是如何消抖的。
延时消抖的办法是非常传统,也就是 第一次判断有按键,延时一定的时间(一般习惯是20ms)再读端口,如果两次读到的数据一样,说明了是真正的按键,而不是抖动,则进入按键处理程序。
当然,不要跟我说你delay(20)那样去死循环去,真是那样的话,我衷心的建议你先放下手上所有的东西,好好的去了解一下操作系统的分时工作原理,大概知道思想就可以,不需要详细看原理,否则你永远逃不出“菜鸟”这个圈子。
我的主程序架构是这样的:      
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="0";
                   break;       // 返回主循环
              }
        }
       }
}
貌似扯远了,回到我们刚才的问题,也就是怎么做按键消抖处理。我们将读按键的程序放在了主循环,也就是说,每20ms我们会执行一次KeyRead()函数来得到新的Trg 和 Cont 值。好了,下面是我的消抖部分:很简单       (第二个while(1)部分
当然,和这个配合,每个子程序必须执行时间不长,更加不能死循环,一般采用有限状态机的办法来实现。

懂得基本原理之后,至于怎么用就大家慢慢思考了,我想也难不到聪明的工程师们。例如还有一些处理, 怎么判断按键释放?很简单,Trg 和Cont都为0 则肯定已经释放了。
     
事无巨细 循序渐进
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

19

主题

121

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
218
金钱
218
注册时间
2011-8-8
在线时间
0 小时
发表于 2011-10-22 23:40:02 | 显示全部楼层
这段程序流传已久,就像是是点明方向的灯塔。
世界上有10种人,一种是懂二进制的,另一种是不懂二进制的。。。
回复 支持 反对

使用道具 举报

14

主题

101

帖子

2

精华

中级会员

Rank: 3Rank: 3

积分
493
金钱
493
注册时间
2011-9-6
在线时间
3 小时
 楼主| 发表于 2011-10-24 11:35:28 | 显示全部楼层
就是觉得经典才转载,也希望自己能够留下点痕迹,方面自己以后查阅!
事无巨细 循序渐进
回复 支持 反对

使用道具 举报

19

主题

248

帖子

2

精华

高级会员

Rank: 4

积分
842
金钱
842
注册时间
2012-2-8
在线时间
19 小时
发表于 2012-2-14 18:03:20 | 显示全部楼层
请教原子啊,这个算法如何移植到板子上,我试过了,还像不能一次读取几个口的状态,不知道哪里出了问题
回复 支持 反对

使用道具 举报

19

主题

248

帖子

2

精华

高级会员

Rank: 4

积分
842
金钱
842
注册时间
2012-2-8
在线时间
19 小时
发表于 2012-2-14 18:06:54 | 显示全部楼层
回复【4楼】寒寒:

---------------------------------
在本论坛找到了
回复 支持 反对

使用道具 举报

7

主题

74

帖子

0

精华

初级会员

Rank: 2

积分
129
金钱
129
注册时间
2011-9-3
在线时间
4 小时
发表于 2012-2-14 22:48:40 | 显示全部楼层
经典,留印子。
http://shop60782849.taobao.com/显示屏专卖
回复 支持 反对

使用道具 举报

19

主题

302

帖子

2

精华

高级会员

Rank: 4

积分
727
金钱
727
注册时间
2011-11-22
在线时间
10 小时
发表于 2012-2-15 10:04:35 | 显示全部楼层
这个程序是很经典,谁把抖动加上 ,在实际应用中,我的一个产品用不了这个。
世界如此美好,好好珍惜每一天吧!
回复 支持 反对

使用道具 举报

2

主题

26

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
227
金钱
227
注册时间
2011-12-28
在线时间
61 小时
发表于 2012-2-20 14:31:57 | 显示全部楼层
很不错啊!收藏!
回复 支持 反对

使用道具 举报

10

主题

46

帖子

0

精华

初级会员

Rank: 2

积分
106
金钱
106
注册时间
2012-2-15
在线时间
0 小时
发表于 2012-2-22 17:57:35 | 显示全部楼层
好算法,很实用啊!很巧妙~
回复 支持 反对

使用道具 举报

9

主题

68

帖子

0

精华

初级会员

Rank: 2

积分
131
金钱
131
注册时间
2012-2-28
在线时间
2 小时
发表于 2012-2-29 18:25:31 | 显示全部楼层
果断收藏
回复 支持 反对

使用道具 举报

4

主题

23

帖子

0

精华

初级会员

Rank: 2

积分
100
金钱
100
注册时间
2012-2-28
在线时间
12 小时
发表于 2012-3-1 10:06:15 | 显示全部楼层
这个不能当键盘扫描行列式的- -
回复 支持 反对

使用道具 举报

14

主题

173

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
256
金钱
256
注册时间
2011-10-14
在线时间
2 小时
发表于 2012-3-2 16:35:56 | 显示全部楼层
学习了,谢谢
回复 支持 反对

使用道具 举报

20

主题

562

帖子

0

精华

论坛大神

Rank: 7Rank: 7Rank: 7

积分
670
金钱
670
注册时间
2012-2-28
在线时间
0 小时
发表于 2012-3-3 14:46:32 | 显示全部楼层
很经典啊
回复 支持 反对

使用道具 举报

2

主题

23

帖子

0

精华

初级会员

Rank: 2

积分
74
金钱
74
注册时间
2011-12-4
在线时间
4 小时
发表于 2012-3-3 22:49:46 | 显示全部楼层
回复【11楼】xuzhenglim:
这个不能当键盘扫描行列式的- -
---------------------------------

在51上用的,可以用行列式


#include <reg52.h>
#include "sys.h"

uchar trgrf = 0; //第一次按键值,用于消抖处理
uchar trgr = 0; //消抖处理后的按键值
uchar contr = 0; //长按按键值

/**********************************************************
4*4矩阵键盘
trgr按键值,contr长按键键值
**********************************************************/
void KeyReadR(void) 

uchar keyrow = 0; //定义临时行
 uchar keycol = 0; //定义临时列,线反转读取P1的值
 uchar readdata = 0;
 1 = 0x0f;
 keyrow = 1;
 1 = 0xf0;
 keycol = 1;
 readdata = (keyrow|keycol)^0xff;
 trgr = readdata & (readdata ^ contr);
 contr = readdata;
 if(trgrf) //第一次如果有键按下,进行消抖处理,以下均为消抖
 {
if(contr == trgrf)
 {
 trgr = trgrf; 
}
 else 
{
 trgr = 0;
 contr = 0;
 }
 } 
}
回复 支持 反对

使用道具 举报

头像被屏蔽

82

主题

191

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
465
金钱
465
注册时间
2011-11-14
在线时间
33 小时
发表于 2012-8-7 02:04:43 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复 支持 反对

使用道具 举报

25

主题

683

帖子

0

精华

论坛大神

Rank: 7Rank: 7Rank: 7

积分
1351
金钱
1351
注册时间
2012-4-25
在线时间
195 小时
发表于 2012-8-7 09:13:58 | 显示全部楼层
这段代码只能用在单个,或是多个特定按键的处理中, 如果是那种矩阵键盘处理就不行了,用这种还更麻烦
1-1
回复 支持 反对

使用道具 举报

3

主题

45

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
317
金钱
317
注册时间
2012-8-11
在线时间
27 小时
发表于 2012-8-11 22:09:19 | 显示全部楼层
回复 支持 反对

使用道具 举报

27

主题

308

帖子

1

精华

高级会员

Rank: 4

积分
774
金钱
774
注册时间
2012-6-19
在线时间
19 小时
发表于 2012-8-16 16:48:37 | 显示全部楼层
学习了
回复 支持 反对

使用道具 举报

5

主题

45

帖子

0

精华

初级会员

Rank: 2

积分
161
金钱
161
注册时间
2012-8-31
在线时间
16 小时
发表于 2013-6-29 22:27:21 | 显示全部楼层
回复【楼主位】葱花鱼:
---------------------------------
学习中,点按键自行清除会不会响应不到啊?
回复 支持 反对

使用道具 举报

0

主题

1

帖子

0

精华

新手入门

积分
21
金钱
21
注册时间
2013-8-20
在线时间
0 小时
发表于 2013-8-20 10:39:59 | 显示全部楼层
回复【楼主位】葱花鱼:
---------------------------------
大神,来个4*4扫描键盘的经典程序呗,我问一下,扫描键盘时,一边引脚输出值,但一边又要读入值,我该怎么设置端口的模式呢,我现在全部设置为PP了,还有如果设置成INFLOATING的话,那么输出怎么办
希望多认识些朋友,多学习些知识
回复 支持 反对

使用道具 举报

5

主题

69

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
374
金钱
374
注册时间
2014-2-13
在线时间
98 小时
发表于 2014-5-23 11:53:16 | 显示全部楼层
回复【20楼】KKKK:
---------------------------------
没结果,顶
回复 支持 反对

使用道具 举报

4

主题

84

帖子

0

精华

初级会员

Rank: 2

积分
155
金钱
155
注册时间
2013-10-17
在线时间
9 小时
发表于 2014-5-27 09:32:51 | 显示全部楼层
不错!顶!!
回复 支持 反对

使用道具 举报

头像被屏蔽

65

主题

277

帖子

0

精华

高级会员

Rank: 4

积分
674
金钱
674
注册时间
2013-8-11
在线时间
29 小时
发表于 2015-4-24 11:50:07 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复 支持 反对

使用道具 举报

7

主题

50

帖子

0

精华

初级会员

Rank: 2

积分
100
金钱
100
注册时间
2014-6-14
在线时间
1 小时
发表于 2015-6-13 09:47:24 | 显示全部楼层
收藏,以后备用,不错
回复 支持 反对

使用道具 举报

4

主题

24

帖子

0

精华

初级会员

Rank: 2

积分
60
金钱
60
注册时间
2015-8-11
在线时间
0 小时
发表于 2015-9-14 20:29:06 | 显示全部楼层
版主,我有一个问题想请教:就是,在按键的时候,不管是长按还是短按,Trig都会被触发一下(只不过长按的时候Trig会在下一次判断时清零,只留下Count),这样的话不会误触发短按键的函数吗?
回复 支持 反对

使用道具 举报

22

主题

103

帖子

0

精华

高级会员

Rank: 4

积分
950
金钱
950
注册时间
2017-2-23
在线时间
205 小时
发表于 2017-5-4 09:17:39 | 显示全部楼层
很是不错的例子,有一个实际的例子就更加完美了
回复 支持 反对

使用道具 举报

0

主题

4

帖子

0

精华

新手上路

积分
20
金钱
20
注册时间
2017-3-5
在线时间
3 小时
发表于 2017-5-17 00:30:27 | 显示全部楼层
这个程序实在经典,赞一个,要是有人把消抖部分加上就更好了
回复 支持 反对

使用道具 举报

17

主题

587

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4467
金钱
4467
注册时间
2013-6-27
在线时间
565 小时
发表于 2017-5-17 09:02:56 | 显示全部楼层
机械输入的最关键是去斗的处理,比如按键输入,行程输入判断等这些都是有机械抖动,程序去斗处理不好直接影响产品的可靠性。
而不同机械抖动是有差别的,所以设计去斗也是要根据实际调整去斗参数。
去斗的方法:
1 对状态值的进行定时计数,状态一旦变化清零重新开始计数,直到计数个数到达预期的个数,确定有按键输入。
2 对输入状态进行定时移位处理(这个是看STM32仿PLC输入状态处理的参考代码)。

输入确定后,再对事件进行分类;
按键事件主要分为:按下处理,按键持续处理,释放处理。
让我们的思维驾驭在电的速度之上!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-8-21 04:40

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表