【GD32F3x0应用笔记】----- 多按键状态机

第五章 多按键状态机



前言

之前有在博客园写过这篇文章,修改一下搬到这里来了

单片机开发经常会用到按键,而我们写按键驱动的时候有没有这样写过:

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

一看好像没什么问题,但是细心的同学就会发现,这里有个bug,而且效率不高。首先是这个while循环,如果按键一直不释放,那么那么程序就会卡死在这里。有人可能会说,我可以加超时呀。超时好像可以解决这一问题,但是如果我想做长按检测,难道我要在这里等它1秒?同时你会看到这里还有一个delay_ms(20)延时消斗程序,这些在程序运行过程中加延时函数,都会造成系统拖沓,效率低下。多个按键的时候甚至出现按键检测不到的问题,这对于实际项目来说,是很糟糕的事情。想要解决这个问题,就可以用到我们今天的主角——状态机。


一、状态机是什么?

  • 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。(解释的很专业,下次不用解释了)
  • 我们只需要弄明白它的四要素就简单了,哪四要素?现态、条件、动作、次态"现态""条件"是因,"动作""次态"是果。
  • 现态:即当前所处的状态;
  • 条件:即满足某个条件就会触发某一动作;
  • 动作:即满足条件时要执行的动作;
  • 次态:执行动作后的状态。
  • “次态”是相对于“现态”而言的,“次态”被激活就会变成新的“现态”。举个简单栗子,比如你当前状态口渴,满足条件有水,你会执行喝水动作,喝完会变成次态不渴了,当你喝水前不渴是次态,喝完水不渴就是现态。
  • 对于我们今天要写的程序来说,“现态”和“次态”就是按键可能所处的状态,有“未按”、“按下”、“长按”、“释放”等,“条件”就是按键“按下”、“长按”、“释放”等,“动作”就是满足条件后要执行的代码,比如点亮LED。

二、状态机编程

  • 我们在上一章工程中进行修改,新建两个文件bsp_key.c和bsp_key.h,保存到我们的APPS目录下,并添加到工程中。

2.1 按键驱动

  • 先来写一下GD32的按键初始化函数,我这里用到了3个按键,分别接单片机的PA5,PA6,PA7,初始化代码如下。
void KEY_Init(void)
{
	//开启时钟
	rcu_periph_clock_enable(RCU_GPIOA);
	//配置IO上拉输入
	gpio_mode_set(GPIOA,GPIO_MODE_INPUT,GPIO_PUPD_PULLUP,GPIO_PIN_5);//KEY1
	gpio_output_options_set(GPIOA,GPIO_OTYPE_OD,GPIO_OSPEED_2MHZ,GPIO_PIN_5);
	
	gpio_mode_set(GPIOA,GPIO_MODE_INPUT,GPIO_PUPD_PULLUP,GPIO_PIN_6);//KEY2
	gpio_output_options_set(GPIOA,GPIO_OTYPE_OD,GPIO_OSPEED_2MHZ,GPIO_PIN_6);
	
	gpio_mode_set(GPIOA,GPIO_MODE_INPUT,GPIO_PUPD_PULLUP,GPIO_PIN_7);//KEY3
	gpio_output_options_set(GPIOA,GPIO_OTYPE_OD,GPIO_OSPEED_2MHZ,GPIO_PIN_7);
}

2.2 按键枚举和结构体

  • 按键的状态我这里定义了四种:未按、按下、长按、抬起。将它们定义成枚举类型如下
typedef enum _KEY_STATUS_LIST
{
	KEY_NULL = 0x00,
	KEY_SURE = 0x01,
	KEY_DOWN = 0x02,
	KEY_LONG = 0x04,
	KEY_UP   = 0x08,
}KEY_STATUS_LIST;
  • 这个枚举包含了五个元素,KEY_NULL表示未按下,KEY_SURE表示确认用了消除抖动的,KEY_DOWN表示按键按下,KEY_LONG表示按键长按,KEY_UP表示按键抬起。

  • 下来是按键的枚举,相当于按键列表,列出所有按键,KEY_NUM刚好就是按键的数量 KEY_NUM = 3,你往里面增加按键,这个数字自动增加。这里数很重要,后面用到你就知道这里用枚举的巧妙之处了。
typedef enum _KEY_LIST
{
	KEY1,
	KEY2,
	KEY3,
	KEY_NUM,
}KEY_LIST;
  • 再来就是按键结构体,因为C语言不是面向对象的,这里将按键所有变量和函数做成结构体,达到类似于面向对象编程的效果。
typedef struct _KEY_OBJECT
{
    uint8_t KEY_SHIELD;       //按键屏蔽 0:屏蔽 1:不屏蔽
	uint8_t KEY_COUNT;        //按键长按计数
    uint8_t KEY_LEVEL;        //虚拟当前IO电平 0:按下 1:抬起
    uint8_t KEY_DOWN_LEVEL;   //按下时IO实际的电平
    uint8_t KEY_STATUS;       //按键状态
    uint8_t KEY_EVENT;        //按键事件
    uint8_t (*READ_PIN)(void);//获取IO状态的函数指针
}KEY_OBJECT;

extern KEY_OBJECT KEY_OBJ[KEY_NUM];
  • KEY_SHIELD 用来做按键屏蔽的,0表示这个按键无效,1这表示有效;
  • KEY_COUNT 长按计数器,统计长按的时间;
  • KEY_LEVEL 虚拟一下按键按下的电平,加这个是为了后面函数封装统一,如果一个按键按下是高电平,一个按下是低电平就不好做代码统一了;
  • KEY_DOWN_LEVEL 按键按下时,单片机IO上面的电平值;
  • KEY_STATUS 按键状态,记录按键当前状态;
  • KEY_EVENT 按键事件,记录事件是否要触发某个动作;
  • (*READ_PIN) 这是一个函数指针变量,指向读IO状态的函数接口。
  • 最后是按键对象的变量声明,如果你仔细看就会发现我这里用了KEY_NUM,发现一个用处就是即使按键增加,表里声明也自动增加。

2.3 按键状态获取

做好上述准备,开始步入正轨了。要想知道按键状态,那肯定是要去获取IO电平的,代码如下

static uint8_t KEY1_ReadPin(void)
{
	return gpio_input_bit_get(GPIOA,GPIO_PIN_5);
}

static uint8_t KEY2_ReadPin(void)
{
	return gpio_input_bit_get(GPIOA,GPIO_PIN_6);
}

static uint8_t KEY3_ReadPin(void)
{
	return gpio_input_bit_get(GPIOA,GPIO_PIN_7);
}
  • 我这里做了一层封装,虽然看起来有点多余,但是却能换来后期的可移植性,后面就会看到,即使换了一个单片机,也只是修改这里,就能跑起来。
  • 有了几个函数,我们就可以来创建我们按键的结构体了
KEY_OBJECT KEY_OBJ[KEY_NUM] = {
	{1,0,0,0,KEY_NULL,KEY_NULL,KEY1_ReadPin},
	{1,0,0,0,KEY_NULL,KEY_NULL,KEY2_ReadPin},
	{1,0,0,0,KEY_NULL,KEY_NULL,KEY3_ReadPin},
};
  • 以上是按键结构体的初始化,具体为什么这样赋值,可以看结构体说明,就知道了。到这里我们还没有真正去调用,我们单独封装一个函数,对所有按键进行状态的获取。
static void Get_Key_Level(void)
{
    uint8_t i;
    
    for(i = 0;i < KEY_NUM;i++)
    {
        if(KEY_OBJ[i].KEY_SHIELD == 0)
            continue;
        if(KEY_OBJ[i].READ_PIN() == KEY_OBJ[i].KEY_DOWN_LEVEL)
            KEY_OBJ[i].KEY_LEVEL = 0;
        else
            KEY_OBJ[i].KEY_LEVEL = 1;
    }
}
  • 这个函数分两步走,先判断按键是否有效,如果有效,则获取按键IO电平,来记录一下当前按键电平。这里你会发现,KEY_NUM又用上了,即使我增加了按键,这个函数也不用做任何改动。

2.4 按键状态机

  • 主角来了,先上代码,再听我细细道来。
static void KEY_StatueMachine(void)
{
    uint8_t i = 0;
	
    Get_Key_Level();//获取按键状态
	
    for(i = 0;i < KEY_NUM;i++)
    {
        switch(KEY_OBJ[i].KEY_STATUS)
        {
            //状态0:
            case KEY_NULL:
                if(KEY_OBJ[i].KEY_LEVEL == 0)
                {
                    KEY_OBJ[i].KEY_STATUS = KEY_SURE;//转入状态1
					KEY_OBJ[i].KEY_EVENT = KEY_NULL;
                }
                else
                {
                    KEY_OBJ[i].KEY_EVENT = KEY_NULL;
                }
                break;
            //状态1:
            case KEY_SURE:
                if(KEY_OBJ[i].KEY_LEVEL == 0)
                {
                    KEY_OBJ[i].KEY_STATUS = KEY_DOWN;//转入状态2
					KEY_OBJ[i].KEY_EVENT = KEY_DOWN;//按下事件
                    KEY_OBJ[i].KEY_COUNT = 0;
                }
                else
                {
                    KEY_OBJ[i].KEY_STATUS = KEY_NULL;//转入状态0
                    KEY_OBJ[i].KEY_EVENT = KEY_NULL;
                }
                break;
            //状态2:
            case KEY_DOWN:
                if(KEY_OBJ[i].KEY_LEVEL != 0)
                {
                    KEY_OBJ[i].KEY_STATUS = KEY_NULL;//转入状态0
                    KEY_OBJ[i].KEY_EVENT = KEY_UP;//松开事件
                }
                else if((KEY_OBJ[i].KEY_LEVEL == 0) 
				&& (++KEY_OBJ[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY))//计数
                {
                    KEY_OBJ[i].KEY_STATUS = KEY_LONG;//转入状态3
                    KEY_OBJ[i].KEY_EVENT = KEY_LONG;//长按事件
					KEY_OBJ[i].KEY_COUNT = 0;
                }
                else
                {
                    KEY_OBJ[i].KEY_EVENT = KEY_NULL;
                }
                break;
            //状态3:
            case KEY_LONG:
                if(KEY_OBJ[i].KEY_LEVEL != 0)
                {
                    KEY_OBJ[i].KEY_STATUS = KEY_NULL;//转入状态0
                    KEY_OBJ[i].KEY_EVENT = KEY_UP;//松开事件
					KEY_OBJ[i].KEY_EVENT = KEY_NULL;
                }
                else if((KEY_OBJ[i].KEY_LEVEL == 0) 
				&& (++KEY_OBJ[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY))//计数
                {
                    KEY_OBJ[i].KEY_EVENT = KEY_LONG;//长按事件
                    KEY_OBJ[i].KEY_COUNT = 0;
                }
                else
                {
                    KEY_OBJ[i].KEY_EVENT = KEY_NULL;
                }
                break;
        }
	}
  • 这里拿一个按键来做说明,首先我们先获取按键的状态,按键一开始状态都是KEY_NULL(现态),此时有按键按下(满足按下的条件),转入KEY_SURE(次态),完成第一步退出,这里的KEY_SURE主要是做消除抖动用的;
    在这里插入图片描述

  • 第二次进来,现态就是KEY_SURE,再获取一下按键状态(条件),如果没有检测到按键按下就返回,说明上次是干扰信号;如果检测到按键按下,那就进入真正的按下状态KEY_DOWN(次态),同时我们给KEY_EVENT赋值,表示我们触发了按下事件,退出后,去判断是否要执行动作
    在这里插入图片描述

  • 第三次进来,这次现态从KEY_DOWN开始,再去获取按键状态(条件),如果按键释放了,就转入状态KEY_NULL(次态),并标记事件为KEY_UP,如果没用释放,进行计数,因为条件没用满足我们不进行状态转移,退出下次再来。
    在这里插入图片描述

  • 当计数值到达长按指定时间,比如1秒,满足长按条件,转移到长按状态KEY_LONG(次态),事件标记为长按,退出后可执行想要的动作
    在这里插入图片描述

  • 以上就是完整的状态机代码的实现了,同样看得出,就算按键增加了,这里的函数也不需要修改,这就是我们前面做那么多准备工作的原因。

2.4 按键处理函数

  • 上面的状态机运行中,我们做了一堆事件的标记,却没用做任何事情,因为我们要把它分开处理,在外面来判断触发了哪些事件,这个事件要做哪些处理,具体就需要根据事件场景来写了。
void KEY_Scan(void)
{
	KEY_StatueMachine();
	
	if(KEY_OBJ[KEY1].KEY_EVENT == KEY_UP)
	{
		printf("KEY1 Down\n");
	}
	else if(KEY_OBJ[KEY1].KEY_EVENT == KEY_LONG)
	{
		printf("KEY1 Long Down\n");
	}
	
	if(KEY_OBJ[KEY2].KEY_EVENT == KEY_UP)
	{
		printf("KEY2 Down\n");
	}
	else if(KEY_OBJ[KEY2].KEY_EVENT == KEY_LONG)
	{
		printf("KEY2 Long Down\n");
	}
	
	if(KEY_OBJ[KEY3].KEY_EVENT == KEY_UP)
	{
		printf("KEY3 Down\n");
	}
	else if(KEY_OBJ[KEY3].KEY_EVENT == KEY_LONG)
	{
		printf("KEY3 Long Down\n");
	}
}
  • 这个函数,包含了按键状态机和按键处理函数,把它放到主函数中去调用即可,注意这个函数要做延迟调用,一般设置20ms循环调用即可。
  • 有没有发现这里有体现了我们结构体的妙用,我们可以很清楚的知道是哪个按键标记的时间。如果你再回头去看看,发现如果你想要移植这部分代码,你要实现的地方就只有一处,而那里正是和你底层硬件有关的地方。这正是程序封装的高级用法,关注我以后带你学习更多的单片机程序封装的方法。

三、总结

本章主要学习了按键状态机的使用,同时也教大家如何对单片机程序进行封装,体验一下单片机更高级的编程方式,也方便日后的代码移植。写作不易,如果觉得对你有用,帮忙点个赞,你的支持会让我有更多动力写下去!
多按键状态机

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一Z攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值