多按键状态机的实现

1. 简单按键检测

记得开始学习单片机的时候,写的按键扫描是这样的:

if(KEY1 == 0)
{
    delay_ms(20);
    if(KEY1 == 0)
    {
        while(KEY1 == 0);
        // 按键按下处理代码
    }
}

一看,有个20ms消除抖动时间,就是说我要在这里死等20ms,还有等待按键释放,我就是不放,你能怎么样?没办法只能做超时。那我想做长按1s呢?细思极恐,对于实际项目上的应用来说是很糟糕的事情,这不仅会拖慢你整个系统,还会出现,多个按键有时检测不到的问题。有没有更好的办法来实现呢?答案是肯定的,想想,如果这个20ms的延时用定时器来做,不就可以了吗!!!

2. 状态机

首先我们得了解什么是状态机?这个当然是问度娘了!!!
状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机的内在因果关系的考虑。"现态"和"条件"是,"动作"和"次态"是。详解如下:
现态:是指当前所处的状态。
条件:当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
次态:条件满足后要迁往的新状态。"次态"是相对于"现态"而言的,"次态"一旦被激活,就转变成新的"现态"了。
有了理论的支撑,有没有发现,状态机这种机制,其实可以运行到很多场景的啦,不仅仅局限于按键。

3. 按键枚举和结构体

按键的状态可以分为:未按、按下、长按、抬起四种。当然你也可以按自己需求去分。废话不多说,直接上代码分析。

typedef enum _KEY_STATUS_LIST
{
    KEY_NULL = 0x00,
    KEY_SURE = 0x01,
    KEY_UP   = 0x02,
    KEY_DOWN = 0x04,
    KEY_LONG = 0x08,
}KEY_STATUS_LIST;

我这里定义了一个按键状态的枚举,包含5个元素,KEY_NULL表示无动作,KEY_SURE表示确认状态,KEY_UP表示按键抬起,
KEY_DOWN表示按键按下,KEY_LONG表示长按。

typedef enum _KEY_LIST
{
    KEY0,
    KEY1,
    KEY2,
    KEY_NUM,
}KEY_LIST;

再来一个枚举,这里列出你的按键,KEY_NUM可以自动统计你按键个数,这里KEY_NUM的值为3,至于为什么我就不多说了,自己问度娘。为什么这里要用枚举?这里抛个砖,继续往下看。

typedef struct _KEY_COMPONENTS
{
    uint8_t KEY_SHIELD;       //按键屏蔽0:屏蔽,1:不屏蔽
    uint8_t KEY_COUNT;        //按键长按计数
    uint8_t KEY_LEVEL;        //虚拟当前IO电平,按下1,抬起0
    uint8_t KEY_DOWN_LEVEL;   //按下时IO实际的电平
    uint8_t KEY_STATUS;       //按键状态
    uint8_t KEY_EVENT;        //按键事件
    uint8_t (*READ_PIN)(void);//读IO电平函数
}KEY_COMPONENTS;
extern KEY_COMPONENTS Key_Buf[KEY_NUM];

KEY_SHIELD按键屏蔽用的,0表示按键不使用,1表示使用;KEY_COUNT长按计数器,好比秒表,按开始然后开始计数了;KEY_LEVEL虚拟按键按下的电平,KEY_DOWN_LEVEL,实际按键按下的IO电平,这两个变量,主要是为了函数封装进行统一,比如你一个按键按下高电平,一个按下低电平,我不管这么多,反正我就和你KEY_DOWN_LEVEL值进行比较,相等我就认为你按下,然后把KEY_LEVEL置位,相反就清零;KEY_STATUS就是我们说的按键状态了,它负责记录某一时刻按键状态;KEY_EVENT表示按键事件,我这里分了3个事件,有按下、抬起和长按。(*READ_PIN)是一个函数指针变量,需要把你读IO的函数接口给它。
最后别忘了,用这个结构体定义变量(这里只是声明哦),有几个按键就定义几个结构类型变量。发现没有我们这里用到了KEY_NUM好处一,按键增加也不需要改动。

4. 按键IO函数

首先得读IO口的电平吧。

static uint8_t KEY0_ReadPin(void)
{
    return _KEY0;
}

static uint8_t KEY1_ReadPin(void)
{
    return _KEY1;
}

static uint8_t KEY2_ReadPin(void)
{
    return _KEY2;
}

这个很简单,就是把你的IO口电平返回来给我就可以了。可以根据自己单片机去实现。有了这几个函数不就可以定义的结构体类型变量了吗。

KEY_COMPONENTS Key_Buf[KEY_NUM] = {
{1,0,0,0,KEY_NULL,KEY_NULL,KEY0_ReadPin},
{1,0,0,0,KEY_NULL,KEY_NULL,KEY1_ReadPin},
{1,0,0,0,KEY_NULL,KEY_NULL,KEY2_ReadPin},
};

这个就不多说了,对着上面结构体说明看就知道了,我这里按键按下的都是低电平。
真正的按键IO电平获取函数在这里

static void Get_Key_Level(void)
{
    uint8_t i;
    
    for(i = 0;i < KEY_NUM;i++)
    {
        if(Key_Buf[i].KEY_SHIELD == 0)
            continue;
        if(Key_Buf[i].READ_PIN() == Key_Buf[i].KEY_DOWN_LEVEL)
            Key_Buf[i].KEY_LEVEL = 1;
        else
            Key_Buf[i].KEY_LEVEL = 0;
    }
}

这个函数主要是实现封装,两步走,先判断按键是否使能,每一个按键IO电平。如果我添加按键这里要改动吗?完全不需要动。

5. 按键状态机

重点来了,准备了那么多,终于可以上按键状态机代码实现了。

void ReadKeyStatus(void)
{
    uint8_t i;
    
    Get_Key_Level();
    
    for(i = 0;i < KEY_NUM;i++)
    {
        switch(Key_Buf[i].KEY_STATUS)
        {
            //状态0:没有按键按下
            case KEY_NULL:
                if(Key_Buf[i].KEY_LEVEL == 1)//有按键按下
                {
                    Key_Buf[i].KEY_STATUS = KEY_SURE;//转入状态1
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //状态1:按键按下确认
            case KEY_SURE:
                if(Key_Buf[i].KEY_LEVEL == 1)//确认和上次相同
                {
                    Key_Buf[i].KEY_STATUS = KEY_DOWN;//转入状态2
                    Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //状态2:按键按下
            case KEY_DOWN:
                if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].KEY_STATUS = KEY_LONG;//转入状态3
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //状态3:按键连续按下
            case KEY_LONG:
                if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
                    Key_Buf[i].KEY_EVENT = KEY_NULL;
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
        }
    }
}

这个函数就是获取当前所有按键状态,每20ms(用定时器定时)调用一次,就可以了。

            //状态0:没有按键按下
            case KEY_NULL:
                if(Key_Buf[i].KEY_LEVEL == 1)//有按键按下
                {
                    Key_Buf[i].KEY_STATUS = KEY_SURE;//转入状态1
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

这里拿一个按键做说明,首先进来先获取按键IO电平,按键开始状态从KEY_NULL开始(现态),此时按键按下(条件),转入KEY_SURE次态),第一步完成退出;

            //状态1:按键按下确认
            case KEY_SURE:
                if(Key_Buf[i].KEY_LEVEL == 1)//确认和上次相同
                {
                    Key_Buf[i].KEY_STATUS = KEY_DOWN;//转入状态2
                    Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

定时器到20ms,刚好去抖动,进来就从新的状态KEY_SURE开始了,再判断当前按键是否还是按下的,如果没有按下,那就返回KEY_NULL,说明上次是干扰,如果按键是按下的,那就进入真正按下的状态KEY_DOWN,同时我们给KEY_EVENT事件赋值,标识我触发了按下事件,KEY_COUNT清零为长按计数做准备,此时退出,你就可以在外面判断事件这个变量决定是否要执行什么任务(动作);

            //状态2:按键按下
            case KEY_DOWN:
                if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].KEY_STATUS = KEY_LONG;//转入状态3
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

又过20ms进来,这次是从状态KEY_DOWN开始,判断按键是否释放,如果释放就转入状态KEY_NULL,同时标记事件为KEY_UP,如果没被释放,我们会进行计数,同时清空数据标志,其它不变,因为我们的条件没有满足,不进行状态迁移,需要注意每次进来没有变化就清空事件,不然出去你判断的标记又触发动作了;

            //状态2:按键按下
            case KEY_DOWN:
                if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].KEY_STATUS = KEY_LONG;//转入状态3
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

同样20ms后进来,假设长按,还是从KEY_DOWN开始,计数值累加,当累加到设定值,比如25时,也就是500ms,满足长按条件,迁移到长按状态KEY_LONG,标记事件为KEY_LONG

            //状态3:按键连续按下
            case KEY_LONG:
                if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
                    Key_Buf[i].KEY_EVENT = KEY_NULL;
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
                    Key_Buf[i].KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

20ms后,进入状态KEY_LONG,一样判断是否释放,释放就进入KEY_NULL状态,标记松开事件,否则继续判断是否为长按。我这里做的是一直按下,每500ms就返回一个长按事件。
这就是整状态机实现,这个函数也不需要我们修改,按键增加或减少根本不影响我这个函数的实现,这就是我前面做一堆枚举、结构体、函数封装的好处,好处二

6. 按键处理函数

前面我们做了一堆标记事件,就是给我们主函数做处理的,下面是我的按键处理,放在主循环里就可以了。

void Task_KEY_Scan(void)
{
    ReadKeyStatus();
    
    if(Key_Buf[KEY0].KEY_EVENT == KEY_UP)
    {
        printf("KEY0 Down\n");
    }
    else if(Key_Buf[KEY0].KEY_EVENT == KEY_LONG)
    {
        printf("KEY0 Long Down\n");
    }
    
    if(Key_Buf[KEY1].KEY_EVENT == KEY_UP)
    {
        printf("KEY1 Down\n");
    }
    else if(Key_Buf[KEY1].KEY_EVENT == KEY_LONG)
    {
        printf("KEY1 Long Down\n");
    }
    
    if(Key_Buf[KEY2].KEY_EVENT == KEY_UP)
    {
        printf("KEY2 Down\n");
    }
    else if(Key_Buf[KEY2].KEY_EVENT == KEY_LONG)
    {
        printf("KEY2 Long Down\n");
    }
}

这个就比较简单,就一直循环判断每个事件的标记,你需要哪种事件就做对比,出现这个标记就执行你的代码。有没有发现,我这里可以清楚的知道是哪个按键标记的事件,这就是那个枚举的终极用处,好处三
到这里就结束了,有什么不足的欢迎大家指出留言下方,有好的建议也可以提出,大家共同学习。好东西分享给大家,后面也会更新一些实用的东西给大家,喜欢就点关注哦!!!^_^

猜你喜欢

转载自www.cnblogs.com/ZzJan/p/11334869.html