原创 基于状态机的单片机按键扫描程序

2011-4-5 15:45 10590 7 13 分类: MCU/ 嵌入式

 硬件连接很简单,四个独立按键分别接在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_u8KeyTimeCount > KEY_LONG_PERIOD)
                        {
                            s_u8KeyTimeCount = 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_u8KeyTimeCount > KEY_CONTINUE_PERIOD)
                        {
                            s_u8KeyTimeCount = 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这条语句的功能。

 

PARTNER CONTENT

文章评论6条评论)

登录后参与讨论

用户862916 2014-12-6 21:54

谢谢 学习之用

用户377235 2014-10-8 15:49

nice

用户377235 2013-12-1 04:47

写得不错,图看不到,能补上就最好了

用户1699654 2013-9-3 13:54

文中不是说有,状态流程转移图,在哪呢 ,没看见

用户377235 2013-1-19 11:26

肿么没看到博主的下一章啊,什么情况呢?这么好的帖子,咋就戛然而止了呢?

用户331262 2011-5-8 12:16

恩,不错!谢谢博主的分享

用户1609127 2009-10-23 20:38

PIAE大大这是一定的~ 因为时间的关系,目前俺只能以博客方式更新...见谅

用户393569 2009-10-23 19:24

写得非常好,就是图片看不到,非常期待博主能够把你的CAN学习的文章发表在我们的CAN学习小组中。
相关推荐阅读
用户226863 2010-04-06 21:28
I帧、P帧和B帧的特点
I帧:帧内编码帧I帧特点:1.它是一个全帧压缩编码帧。它将全帧图像信息进行JPEG压缩编码及传输;2.解码时仅用I帧的数据就可重构完整图像;3.I帧描述了图像背景和运动主体的详情;4.I帧不需要参考其...
用户226863 2009-09-13 21:40
H.264简介
1 H.264简介H.264是ITU-T和ISO/IEC联合制定的最新编码标准,它最先由ITU-T于1997年提出,目标是提出一种更高性能(相对于当时的H.263)的视频编码标准。相对于其它标准,H....
用户226863 2009-09-13 21:33
ARM7启动代码的分析与设计(转)
ARM7启动代码的分析与设计(转)感觉写的比较好,忍不住转过来。引言   随着生活水平的提高和IT技术的进步,8位处理器的处理能力已经不能满足嵌入式系统的需要了;而 16位处理器在性能和成本上都没有很...
用户226863 2009-08-02 12:54
ARM启动代码分析
ARM启动代码分析 收藏 理解启动代码(ADS) 所谓启动代码,就是处理器在启动的时候执行的一段代码,主要任务是初始化处理器模式,设置堆栈,初始化变量等等.由于以上的操作均与处理器体系结构和系统配置密...
用户226863 2009-06-08 22:48
avr的IO
每个IO口属性由DDRX,PORTX,PINX三个寄存器的对应位来控制,分别是方向,为端口赋值,读取引脚。SFIOR寄存器的PUD位用来禁止所有IO口的上拉电阻。在PUD没有被禁止的情况下,通过DDR...
EE直播间
更多
我要评论
6
7
关闭 站长推荐上一条 /3 下一条