2024年4月13日发(作者:)

单片机按键处理技巧及编程方式

2010-10-23 15:01

从这一章开始,我们步入按键程序设计的殿堂。在基于单片机为核心构成

的应用系统中,用户输入是必不可少的一部分。输入可以分很多种情况,譬如有

的系统支持PS2键盘的接口,有的系统输入是基于编码器,有的系统输入是基于

串口或者USB或者其它输入通道等等。在各种输入途径中,更常见的是,基于单

个按键或者由单个键盘按照一定排列构成的矩阵键盘(行列键盘)。我们这一篇章

主要讨论的对象就是基于单个按键的程序设计,以及矩阵键盘的程序编写。

◎按键检测的原理

常见的独立按键的外观如下,相信大家并不陌生,各种常见的开发板学习板上随

处可以看到他们的身影。

(原文件名:)

引用图片

总共有四个引脚,一般情况下,处于同一边的两个引脚内部是连接在

一起的,如何分辨两个引脚是否处在同一边呢?可以将按键翻转过来,处于同一

边的两个引脚,有一条突起的线将他们连接一起,以标示它们俩是相连的。如果

无法观察得到,用数字万用表的二极管挡位检测一下即可。搞清楚这点非常重要,

对于我们画PCB的时候的封装很有益。

它们和我们的单片机系统的I/O口连接一般如下:

(原文件名:)

引用图片

对于单片机I/O内部有上拉电阻的微控制器而言,还可以省掉外部的

那个上拉电阻。简单分析一下按键检测的原理。当按键没有按下的时候,单片机

I/O通过上拉电阻R接到VCC,我们在程序中读取该I/O的电平的时候,其值为

1(高电平); 当按键S按下的时候,该I/O被短接到GND,在程序中读取该I/O

的电平的时候,其值为0(低电平) 。这样,按键的按下与否,就和与该按键相

连的I/O的电平的变化相对应起来。结论:我们在程序中通过检测到该I/O口电

平的变化与否,即可以知道按键是否被按下,从而做出相应的响应。一切看起来

很美好,是这样的吗?

◎现实并非理想

在我们通过上面的按键检测原理得出上述的结论的时候,其实忽略了一个重要的

问题,那就是现实中按键按下时候的电平变化状态。我们的结论是基于理想的情

况得出来的,就如同下面这幅按键按下时候对应电平变化的波形图一样:

(原文件名:)

引用图片

而实际中,由于按键的弹片接触的时候,并不是一接触就紧紧的闭合,

它还存在一定的抖动,尽管这个时间非常的短暂,但是对于我们执行时间以us

为计算单位的微控制器来说,

它太漫长了。因而,实际的波形图应该如下面这幅示意图一样。

(原文件名:)

引用图片

这样便存在这样一个问题。假设我们的系统有这样功能需求:在检测到按键按下

的时候,将某个I/O的状态取反。由于这种抖动的存在,使得我们的微控制器误

以为是多次按键的按下,从而将某个I/O的状态不断取反,这并不是我们想要的

效果,假如该I/O控制着系统中某个重要的执行的部件,那结果更不是我们所期

待的。于是乎有人便提出了软件消除抖动的思想,道理很简单:抖动的时间长度

是一定的,只要我们避开这段抖动时期,检测稳定的时候的电平不久可以了吗?

听起来确实不错,而且实际应用起来效果也还可以。于是,各种各样的书籍中,

在提到按键检测的时候,总也不忘说道软件消抖。就像下面的伪代码所描述的一

样。(假设按键按下时候,低电平有效)

If(0 == io_KeyEnter) //如果有键按下了

{

Delayms(20) ; //先延时20ms避开抖动时

If(0 == io_KeyEnter) //然后再检测,如果还是检

测到有键按下

{

return KeyValue ; //是真的按下了,

返回键值

}

else

{

return KEY_NULL //是抖动,返回空的键值

}

while(0 == io_KeyEnter) ; //等待按键释放

}

乍看上去,确实挺不错,实际中呢?在实际的系统中,一般是不允许这么样做的。

为什么呢?首先,这里的Delayms(20) , 让微控制器在这里白白等待了20 ms 的

时间,啥也没干,考虑我在《学会释放CPU》一章中所提及的几点,这是不可取

的。其次while(0 == io_KeyEnter) 所以合理的分配好微控制的处理时间,是

编写按键程序的基础。;更是程序设计中的大忌(极少的特殊情况例外)。任何

非极端情况下,都不要使用这样语句来堵塞微控制器的执行进程。原本是等待按

键释放,结果CPU就一直死死的盯住该按键,其它事情都不管了,那其它事情不

干了吗?你同意别人可不会同意

◎消除抖动有必要吗?

的确,软件上的消抖确实可以保证按键的有效检测。但是,这种消抖确实有必要

吗?有人提出了这样的疑问。抖动是按键按下的过程中产生的,如果按键没有按

下,抖动会产生吗?如果没有按键按下,抖动也会在I/O上出现,我会立刻把这

个微控制器锤了,永远不用这样一款微控制器。所以抖动的出现即意味着按键已

经按下,尽管这个电平还没有稳定。所以只要我们检测到按键按下,即可以返回

键值,问题的关键是,在你执行完其它任务的时候,再次执行我们的按键任务的

时候,抖动过程还没有结束,这样便有可能造成重复检测。所以,如何在返回键

值后,避免重复检测,或者在按键一按下就执行功能函数,当功能函数的执行时

间小于抖动时间时候,如何避免再次执行功能函数,就成为我们要考虑的问题了。

这是一个仁者见仁,智者见智的问题,就留给大家去思考吧。所以消除抖动的目

的是:防止按键一次按下,多次响应。

“从单片机初学者迈向单片机工程师”之KEY主题讨论

基于状态转移的独立按键程序设计

本章所描述的按键程序要达到的目的:检测按键按下,短按,长按,

释放。即通过按键的返回值我们可以获取到如下的信息:按键按下(短按),按键

长按,按键连_发,按键释放。不知道大家还记得小时候玩过的电子钟没有,就

是外形类似于CALL 机(CALL )的那种,有一个小液晶屏,还有四个按键,功能

是时钟,闹钟以及秒表。在调整时间的时候,短按+键每次调整值加一,长按的

时候调整值连续增加。小的时候很好奇,这样的功能到底是如何实现的呢,今天

就让我们来剖析它的原理吧。机,好像是很古老的东西了

状态在生活中随处可见。譬如早上的时候,闹钟把你叫醒了,这个时候,你便处

于清醒的状态,马上你就穿衣起床洗漱吃早餐,这一系列事情就是你在这个状态

做的事情。做完这些后你会去等车或者开车去上班,这个时候你就处在上班途中

的状态…..中午下班时间到了,你就处于中午下班的状态,诸如此类等等,在每

一个状态我们都会做一些不同的事情,而总会有外界条件促使我们转换到另外一

种状态,譬如闹钟叫醒我们了,下班时间到了等等。对于状态的定义出发点不同,

考虑的方向不同,或者会有些许细节上面的差异,但是大的状态总是相同的。生

活中的事物同样遵循同样的规律,譬如,用一个智能充电器给你的手机电池充电,

刚开始,它是处于快速充电状态,随着电量的增加,电压的升高,当达到规定的

电压时候,它会转换到恒压充电。总而言之,细心观察,你会发现生活中的总总

都可以归结为一个个的状态,而状态的变换或者转移总是由某些条件引起同时伴

随着一些动作的发生。我们的按键亦遵循同样的规律,下面让我们来简单的描绘

一下它的状态流程转移图。

(原文件名:)

引用图片

下面对上面的流程图进行简要的分析。

首先按键程序进入初始状态S1,在这个状态下,检测按键是否按下,如果有按

下,则进入按键消抖状态2,在下一次执行按键程序时候,直接由按键消抖状态

进入按键按下状态3,在此状态下检测按键是否按下,如果没有按键按下,则返

回初始状态S1,如果有则可以返回键值,同时进入长按状态S4,在长按状态下

每次进入按键程序时候对按键时间计数,当计数值超过设定阈值时候,则表明长

按事件发生,同时进入按键连_发状态S5。如果按键键值为空键,则返回按键释

放状态S6,否则继续停留在本状态。在按键连_发状态下,如果按键键值为空键

则返回按键释放状态S6,如果按键时间计数超过连_发阈值,则返回连_发按键

值,清零时间计数后继续停留在本状态。

看了这么多,也许你已经有一个模糊的概念了,下面让我们趁热打铁,一起来动

手编写按键驱动程序吧。

下面是我使用的硬件的连接图。

(原文件名:)

引用图片

硬件连接很简单,四个独立按键分别接在P3^0------P3^3四个I/O上面。

因为51单片机I/O口内部结构的限制,在读取外部引脚状态的时候,需要向端

口写1.在51单片机复位后,不需要进行此操作也可以进行读取外部引脚的操作。

因此,在按键的端口没有复用的情况下,可以省略此步骤。而对于其它一些真正

双向I/O口的单片机来说,将引脚设置成输入状态,是必不可少的一个步骤。

下面的程序代码初始化引脚为输入。

void KeyInit(void)

{

io_key_1 = 1 ;

io_key_2 = 1 ;

io_key_3 = 1 ;

io_key_4 = 1 ;

}

根据按键硬件连接定义按键键值

#define KEY_VALUE_1 0x0e

#define KEY_VALUE_2 0x0d

#define KEY_VALUE_3 0x0b

#define KEY_VALUE_4 0x07

#define KEY_NULL 0x0f

下面我们来编写按键的硬件驱动程序。

根据第一章所描述的按键检测原理,我们可以很容易的得出如下的代码:

static uint8 KeyScan(void)

{

if(io_key_1 == 0)return KEY_VALUE_1 ;

if(io_key_2 == 0)return KEY_VALUE_2 ;

if(io_key_3 == 0)return KEY_VALUE_3 ;

if(io_key_4 == 0)return KEY_VALUE_4 ;

return KEY_NULL ;

}

其中io_key_1等是我们按键端口的定义,如下所示:

sbit io_key_1 = P3^0 ;

sbit io_key_2 = P3^1 ;

sbit io_key_3 = P3^2 ;

sbit io_key_4 = P3^3 ;

KeyScan()作为底层按键的驱动程序,为上层按键扫描提供一个接口,这样我们

编写的上层按键扫描函数可以几乎不用修改就可以拿到我们的其它程序中去使

用,使得程序复用性大大提高。同时,通过有意识的将与底层硬件连接紧密的程

序和与硬件无关的代码分开写,使得程序结构层次清晰,可移植性也更好。对于

单片机类的程序而言,能够做到函数级别的代码重用已经足够了。

在编写我们的上层按键扫描函数之前,需要先完成一些宏定义。

//定义长按键的TICK数,以及连_发间隔的TICK数

#define KEY_LONG_PERIOD 100

#define KEY_CONTINUE_PERIOD 25

//定义按键返回值状态(按下,长按,连_发,释放)

#define KEY_DOWN 0x80

#define KEY_LONG 0x40

#define KEY_CONTINUE 0x20

#define KEY_UP 0x10

//定义按键状态

#define KEY_STATE_INIT 0

#define KEY_STATE_WOBBLE 1

#define KEY_STATE_PRESS 2

#define KEY_STATE_LONG 3

#define KEY_STATE_CONTINUE 4

#define KEY_STATE_RELEASE 5

接着我们开始编写完整的上层按键扫描函数,按键的短按,长按,连按,释放等

等状态的判断均是在此函数中完成。对照状态流程转移图,然后再看下面的函数

代码,可以更容易的去理解函数的执行流程。完整的函数代码如下:

void GetKey(uint8 *pKeyValue)

{

static uint8 s_u8KeyState = KEY_STATE_INIT ;

static uint8 s_u8KeyTimeCount = 0 ;

static uint8 s_u8LastKey = KEY_NULL ; //保存按键释放时候

的键值

uint8 KeyTemp = KEY_NULL ;

KeyTemp = KeyScan() ; //获取键值

switch(s_u8KeyState)

{

case KEY_STATE_INIT :

{

if(KEY_NULL != (KeyTemp))

{

s_u8KeyState =

KEY_STATE_WOBBLE ;

}

}

break ;

case KEY_STATE_WOBBLE : //消抖

{

s_u8KeyState =

KEY_STATE_PRESS ;

}

break ;

case KEY_STATE_PRESS :

{

if(KEY_NULL != (KeyTemp))

{

s_u8LastKey =

KeyTemp ; //保存键值,以便在释放按键状态返回键值

KeyTemp |=

KEY_DOWN ; //按键按下

s_u8KeyState =

KEY_STATE_LONG ;

}

else

{

s_u8KeyState =

KEY_STATE_INIT ;

}

}

break ;

case KEY_STATE_LONG :

{

if(KEY_NULL != (KeyTemp))

{

if(++s_u8KeyTimeCoun

t > KEY_LONG_PERIOD)

{

s_u8KeyTimeC

ount = 0 ;

KeyTemp |=

KEY_LONG ; //长按键事件发生

s_u8KeyState

= KEY_STATE_CONTINUE ;

}

}

else

{

s_u8KeyState =

KEY_STATE_RELEASE ;

}

}

break ;

case KEY_STATE_CONTINUE :

{

if(KEY_NULL != (KeyTemp))

{

if(++s_u8KeyTimeCoun

t > KEY_CONTINUE_PERIOD)

{

s_u8KeyTimeC

ount = 0 ;

KeyTemp |=

KEY_CONTINUE ;

}

}

else

{

s_u8KeyState =

KEY_STATE_RELEASE ;

}

}

break ;

case KEY_STATE_RELEASE :

{

s_u8LastKey |= KEY_UP ;

KeyTemp = s_u8LastKey ;

s_u8KeyState =

KEY_STATE_INIT ;

}

break ;

default : break ;

}

*pKeyValue = KeyTemp ; //返回键值

}

关于这个函数内部的细节我并不打算花过多笔墨去讲解。对照着按键状态流程转

移图,然后去看程序代码,你会发现其实思路非常清晰。最能让人理解透彻的,

莫非就是将整个程序自己看懂,然后想象为什么这个地方要这样写,抱着思考的

态度去阅读程序,你会发现自己的程序水平会慢慢的提高。所以我更希望的是你

能够认认真真的看完,然后思考。也许你会收获更多。

不管怎么样,这样的一个程序已经完成了本章开始时候要求的功能:按下,长按,

连按,释放。事实上,如果掌握了这种基于状态转移的思想,你会发现要求实现

其它按键功能,譬如,多键按下,功能键等等,亦相当简单,在下一章,我们就

去实现它。

在主程序中我编写了这样的一段代码,来演示我实现的按键功能。

void main(void)

{

uint8 KeyValue = KEY_NULL;

uint8 temp = 0 ;

LED_CS11 = 1 ; //流水灯输出允许

LED_SEG = 0 ;

LED_DIG = 0 ;

Timer0Init() ;

KeyInit() ;

EA = 1 ;

while(1)

{

Timer0MainLoop() ;

KeyMainLoop(&KeyValue) ;

if(KeyValue == (KEY_VALUE_1 | KEY_DOWN)) P0 = ~1 ;

if(KeyValue == (KEY_VALUE_1 | KEY_LONG)) P0 = ~2 ;

if(KeyValue == (KEY_VALUE_1 | KEY_CONTINUE)) { P0 ^=

0xf0;}

if(KeyValue == (KEY_VALUE_1 | KEY_UP)) P0 = 0xa5 ;

}

}

按住第一个键,可以清晰的看到P0口所接的LED的状态的变化。当按

键按下时候,第一个LED灯亮,等待2 S后第二个LED亮,第一个熄灭,表示长

按事件发生。再过500 ms 第5~8个LED闪烁,表示连按事件发生。当释放按键

时候,P0口所接的LED的状态为:

灭亮灭亮亮灭亮灭,这也正是P0 = 0xa5这条语句的功能。