衡阳刘小勇是做什么的:晨辉教你轻松学51

来源:百度文库 编辑:中财网 时间:2024/04/29 17:27:57
晨辉教你轻松学51--------按键篇
 
    对于一个由单片机为核心构成的系统而言。输入通道是相当重要的。可以看到几乎每一样基于单片机的产品都有人机交互的部分。如各种仪器设备上的各种按钮和开关,以及我们手机上的键盘,MP3上的按键等等。最常见的输入部分,莫非就是按键了。对于大多数初学者而言,编写一个好的按键程序是一件颇为头疼的事情。于是乎在网上乱搜一气,程序倒是找到了不少,但是看了半天依然是不明白。或者在某某论坛上面发帖“跪求XX按键程序,大虾帮忙……”如果你偶然间进了这个论坛,又偶然看到了这个帖子,而且恰好你对按键程序的写法也不是很清楚,那么我希望你能够静静的看完这个帖子。如果你觉得对你很有帮助,那么我希望你能够在以后的日子中能够坚持到这个论坛来,一起交流学习,分享自己学习过程中的喜悦或者一起探讨棘手的问题,这是我写这个帖子的最大的初衷了。OK,不能再说了,再说就变成水帖了。那么我们开始吧。
按键的种类很多。不过原理基本相似。下面我们以一种轻触开关为例讲解按键程序的写法。

这种轻触开关大家不陌生吧^_^

    一般情况下,按键与单片机的连接如下面这幅图所示。
  

(图中电阻值一般去4.7k~10k之间,对于内部端口有上拉电阻的单片机则可省略此电阻)
单片机对于按键的按下与否则是通过检测相应引脚上的电平来实现的。对于上图而言,当P17引脚上面的电平为低时,则表示按键已经按下。反之,则表明按键没有按下。我们在程序中只要检测到了P17引脚上面的电平为低了,就可以判断按键按下。呵呵,简单吧。等会,您先别乐呵,话还没说完呢。下面我们来看看,当按键按下时,P17引脚上面的波形是怎么变化的。


上图是一个理想波形图,当按键按下时,P17口的电平马上被拉低到0V了。当然理想的东西都是不现实的。所以我们还是看看现实的波形图吧。


看出什么区别来了没。呵呵,只要你不是傻子我相信都能看出其中的区别。由于按键的机械特性。当按键闭合时,并不能马上保存良好的接触,而是来回弹跳。这个时间很短,我们的手根本感觉不出来。但是对于一秒钟执行百万条指令的单片机而言,这个时间是相当的长了。那么在这段抖动的时间内,单片机可能读到多次高低电平的变化。如果不加任何处理的话,就会认为已经按下,或者松开很多次了。而事实上,我们的手一直按在按键上,并没有重复按动很多次。要想能够正确的判断按键是否按下就要避开这段抖动的时间。根据一般按键的机械特点,以及按键的新旧程度等而言,这段抖动的时间一般在5MS~20MS之间。
看到这里你明白了该如何做了吧。
    看看下面的这个流程图,你应该不陌生吧。


这个流程是好多教科书上的做法。可惜,误导了好多人。为什么呢。因为它根本就没有考虑实际情况。我们根据这幅流程图来写它的代码看看。
        unsigned char v_ReadKey_f( void )
    {
        unsigned char KeyPress ;
        if( P17 == 0)
        {
                Delay(20) ;      //延时20MS
                If( P17 == 0)
               {
                         KeyPress = 1 ;
                         While( !P17) ;  //等待释放
            }
               else
              KeyPress = 0 ;
            }
    }

     这样一个程序,相信对很多初学者而言都不陌生。因为好多书上基本都是这样的一个流程和写法。可是当有一天,我们想做一个数码管加按键调整的时钟,发现当我们按键按下去的时候,数码管就不亮了。为什么呢。原因就在这个键盘扫描函数。平常没有按键按下还好。一旦有键按下,它先是浪费了CPU的大部分时间(就是那个什么事情都没做的延时20MS函数)然后,又霸占CPU( 就是哪个死死等在那里的while(P17);语句)直到按键释放。对于这种情况我们是忍无可忍的,那么就让我们彻底的抛弃它吧。那么到底按键扫描函数改如何写呢……..所谓众里寻她千百度,蓦然回首,那人却在灯火阑珊处。如果我们把CPU延时的那20MS拿出来去做其它事情,那么不就充分利用CPU的时间了吗。而一般情况下我们只要前沿去抖动就可以了。也就是说了,我们只需在按键按下后去抖就可以了,对于按键的释放抖动可以不必要过于关注。当然这主要和应用的场合有关。一个能有效识别按键按下并支持连发功能的按键已经能够应用到大多数的场合了。

     下面以四个独立按键的处理程序为例来讲解(支持单击和连发)
    #include"regx52.h"
                     sbit KeyOne = P1^0 ;
    sbit KeyTwo = P1^1 ;
    sbit KeyThree = P1^2 ;
    sbit KeyFour = P1^3 ;
    #define uint16 unsigned int
    #define uint8 unsigned char
    #define NOKEY  0xff
    #define KEY_WOBBLE_TIME 500          //去抖动时间(待定)
    #define KEY_OVER_TIME 15000      //等待进入连击时间(待定),该常数要比正常 //按键时间要长,防止非目的性进入连击模式
        #define KEY_QUICK_TIME 1000   //等待按键抬起的连击时间(待定)
                    void v_KeyInit_f( void )
    {
        KeyOne = 1 ;    //按键初始化(相应端口写1)
        KeyTwo = 1 ;
        KeyThree = 1 ;
        KeyFour = 1 ;
    }
    uint8 u8_ReadKey_f(void)
    {
        static uint8 LastKey = NOKEY ;        //保存上一次的键值
        static uint16 KeyCount = 0 ;        //按键延时计数器
        static uint16 KeyOverTime = KEY_OVER_TIME ; //按键抬起时间
            uint8 KeyTemp = NOKEY ;            //临时保存读到的键值
        KeyTemp = P1 & 0x0f ;                //读键值
        if( KeyTemp == 0x0f )
        {
            KeyCount = 0 ;
            KeyOverTime = KEY_OVER_TIME ;
            return NOKEY ;            //无键按下返回NOKEY
        }
        else
        {
                    if( KeyTemp == LastKey )    //是否第一次按下
              {
                                               if( ++KeyCount == KEY_WOBBLE_TIME )    //不是第一次按下,则判断//抖动是否结束
            {
                return KeyTemp ;                //去抖动结束,返回键值
            }
            else
            {
                           if( KeyCount > KeyOverTime )
                          {
                KeyCount = 0 ;
                KeyOverTime = KEY_QUICK_TIME ;
                          }
                          return NOKEY ;
            }
                     }
                    else    //是第一次按下则保存键值,以便下次执行此函数时与读到的键值作比较
              {
            LastKey = KeyTemp ;            //保存第一次读到的键值
            KeyCount = 0 ;                //延时计数器清零
            KeyOverTime = KEY_OVER_TIME ;
            return NOKEY ;
                    }
                    }
    }
  
   下面是我测试用的主程序(相关头文件未列出,仅仅作测试演示用)
   void main(void)
{
    uint8 KeyValue ;
    int16 Count ;
    v_LcdInit_f() ;
    v_KeyInit_f() ;
    CLS
    LOCATE(3, 1)
    PRINT("Key Test")
    LOCATE(6, 2)
    SHOW_ICON
    while(1)
    {    
        
        KeyValue = u8_ReadKey_f() ;
        if( KeyValue != NOKEY )
        {    LOCATE(1, 2)
            if( KeyValue == 0x0e )Count++ ;
            if( KeyValue == 0x0d )Count-- ;
            if( KeyValue == 0x0b )Count = 0 ;
            if( KeyValue == 0x07 )Count = 0 ;
            HIDE_ICON
            PRINTD(Count, 5)
            LOCATE(6, 2)
        }
        else
        {
           //SHOW_ICON
         }        
    }        
}

每次执行读键盘函数时,只是对一些标志进行判断,然后退出。因此能够充分的利用CPU的资源。同时可以处理连发按键。此按键扫描按键函数可以直接放在主函数中。如果感觉按键太过灵敏或者迟钝则改一下相关消抖动的宏定义即可。此函数也可以通过中断标志位进行定时的扫描。此时,需要添加一个定时标志位,并将相关消抖动的和连击时间的宏定义改小即可。然后在主程序类似下面这样写即可
                if( KeyTime )       //定时扫描时间到
                {
        KeyValue = u8_ReadKey_f() ;
               }
具体的工作就交给您去完成啦。

看看效果:
按键单击

连发时候的截图    

至此,关于单个按键的学习就告一段落了,您是否已经明白了。如果您还不明白,那么把这个程序好好的看看,并画下流程图,分析分析。估计您就会恍然大悟。关键是思路要转换过来。
下面我们来看看多个按键的情况吧
一般情况下,如果多个按键每个都直接接在单片机的I/O上的话会占用很多的I/O资源。比较合理的一种做法是,按照行列接成矩阵的形式。按键接在每一个的行列的相交处。这样对于m行n列的矩阵,可以接的按键总数是m*n。这里我们以常见的4*4矩阵键盘来讲解矩阵键盘的编程。



上图就是矩阵键盘的一般接法。

这里我们要介绍一种快速的键盘扫描法:线反转法(或者称为行列翻转法)。具体流程如下。首先,让单片机的行全部输出0,列全部输出1,读取列的值(假设行接P3口的高四位,列接低四位)。即P3= 0x0f ; 此时读列的值,如果有键按下,则相应的列读回来的值应该为低。譬如此时读回来的值为 0x0e ; 即按键列的位置已经确定。这时反过来,把行作为输入,列作为输出,即P0 = 0xf0 ;此时再读行的值,如果按键仍然被按下,则相应的行的值应该为低,如果此时读回来的值为0xe0,则确定了行的位置 。说到这里,您应该笑了,知道了一个按键被按下的行和列的位置,那么就可以肯定确定它的位置了。我们把读回来的行值和列值进行或运算。即 0xe0 | 0x 0e 即 0xee。那么0xee就是我们按下的按键的键值了。怎么样。只需几步就可以判断所有的键值,简单吧。下面再结合一个例子具体看看。
/******************************************
* 此模块所需相关支持库                      *
******************************************/
#include"regx52.h"
#define uint8 unsigned char
#define uint16 unsigned int

/****************************************
* 与硬件连接相关的定义及宏定义和操作宏  *
*****************************************/
#define KEYBOARD     P3                             //键盘连接到单片机上的端口位置
#define READ_ROW_ENLABLE    KEYBOARD = 0x0f ;     //读端口之前先把相应口置位(由基本51单片机特性决定的)
#define READ_COL_ENLABLE    KEYBOARD = 0xf0 ;     // 根据实际硬件连接情况修改    

/*****************************************
* 模块内相关的宏定义及常数宏             *
******************************************/
#define NOKEY         0xff    //定义无键按下时的返回值
#define DELAY_COUNT  2        //消抖时间常数


/*****************************************
* 此模块所需的全局或者外部变量           *
*****************************************/
bit bdata StartScan = 0 ;//此变量需放在定时中断中置位

/*****************************************
* 按键扫描函数,按下去后经去抖,确定按下 *
* 则返回键值0~15;无键按下则返回0xff ;   *
* 此函数需要定时器的支持(去抖....)         *
*****************************************/
uint8 u8_KeyBoardScan_f()
{
    static uint8 DelayCount = 0 ;
    uint8 KeyValueRow = 0 ;
    uint8 KeyValueCol = 0 ;
    uint8 KeyValue = 0 ;
    if( StartScan )         //开始扫描,StartScan在定时中断中置位
    {
        StartScan = 0 ;     //清除开始扫描标志位,避免多次重复执行扫描程序
        //读入按键状态前先向相应端口写1(由基本51单片机硬件结构决定)
                        READ_ROW_ENLABLE  
        if( ( KEYBOARD & 0x0f ) != 0x0f ) //判断是否有键按下
        {
            DelayCount++;
             if( DelayCount <= DELAY_COUNT )    //有键按下则判断延时去抖的时间是否达到
            {
                return NOKEY ;                
            }
            else                 //消除了抖动
            {
                if( ( KEYBOARD & 0x0f ) != 0x0f )    //再次判断是否按键真的按下
                {
                     DelayCount = 0 ;                 //确定按下后,延时去抖计时器清0
                     KeyValueRow = KEYBOARD & 0x0f ; //取得行码
                    //准备读列,先向相应端口写1(由基本51单片机硬件结构决定)
                                                                                          READ_COL_ENLABLE    
                     if ( (KEYBOARD & 0xf0) != 0xf0 ) //反转,读列码
                     {
                         KeyValueCol = KEYBOARD & 0xf0 ;    //取得列码
                                                               //合并取得的行码和列码,即是相应按键的键值
                                                         switch( KeyValueCol | KeyValueRow)    
                        {
                            case 0x77 : KeyValue = 0 ; break ;
                            case 0xb7 : KeyValue = 1 ; break ;
                            case 0xd7 : KeyValue = 2 ; break ;
                            case 0xe7 : KeyValue = 3 ; break ;
                            case 0x7b : KeyValue = 4 ; break ;
                            case 0xbb : KeyValue = 5 ; break ;
                            case 0xdb : KeyValue = 6 ; break ;
                            case 0xeb : KeyValue = 7 ; break ;
                            case 0x7d : KeyValue = 8 ; break ;
                            case 0xbd : KeyValue = 9 ; break ;
                            case 0xdd : KeyValue = 10 ;break ;
                            case 0xed : KeyValue = 11 ;break ;
                            case 0x7e : KeyValue = 12 ;break ;
                            case 0xbe : KeyValue = 13 ;break ;
                            case 0xde : KeyValue = 14 ;break ;
                            case 0xee : KeyValue = 15 ;break ;
                            default :     return NOKEY ;
                        }
                    
                        return KeyValue ;
                     }
                     else
                     {
                         DelayCount = 0 ;
                         return NOKEY ;
                     }
                 }
                 else
                {
                    DelayCount = 0 ;
                    return NOKEY ;
                }
            }
        }
        else
        {
            DelayCount = 0 ;
            return NOKEY ;
        }
    }    
}


void v_T0_Isr_f( void ) interrupt INTERRUPT_TIMER2_OVERFLOW
{

    StartScan = 1 ;
}

/***************************************************
*模块调试                                           *
***************************************************/

//主函数仅作演示用,主函数除按键扫描外的函数并没在这里给出
void v_Init_T2_f( void )
{
    T2CON    = 0x04 ;
    T2MOD     = 0x00 ;
    TH2      = 0xd8 ;
    RCAP2H     = 0xd8 ;
    TL2       = 0xf0 ;
    RCAP2L    = 0xf0 ;
    ET2     = 1 ;
    TR2    = 1 ;
}



void main( void )
{
    uint8 readkey = 0 ;
    v_Init_T2_f( ) ;
    v_LcdInit_f( );
    LOCATE( 1, 1)
    PRINT("4*4KeyBoard Test")
    EA = 1 ;

    
    
       LOCATE( 3, 2)
    while( 1 )
    {    
        
        SHOW_ICON
        readkey =  u8_KeyBoardScan_f()    ;
        if( readkey != NOKEY)
        {    
            PRINTN( readkey , 2)
            
            LOCATE( 3, 2)
            continue ;
        }
        else
        {
            continue ;
        }
    }
}




呵呵,按键扫描程序已经注释的很详细了。我就不多费嘴舌了。如果有不清楚的地方,欢迎跟帖讨论。
下面是按键测试的截图




我的自己搭建的实验板


OK,Enioy it !自此按键检测告一段落。下次如果再讲按键。将会讨论另外一种按键的写法:基于状态机的按键程序设计。欢迎讨论。