基于stm32标准库独立按键的多按键状态机的实现

写在前面

  一般引用都写在最后,但是这篇博文对我这个状态机的影响很大,我这里有许多借鉴他的思维。所以写在前面,如有侵权立即删除

简单按键检测

  一开始学习单片机的时候我接触到按键的时候就知道按键有抖动,记得当初按键消抖分为硬件和软件,硬件上常用于复位按键如下图
硬件消抖
  软件上来说,最经典的消抖

if(KEY1 == 0)
{
    delay_ms(20); // 延时消抖
    if(KEY1 == 0)
    {
        while(KEY1 == 0);//堵塞,等待松开
        // 按键按下处理代码
    }
}

  硬件消抖一来就是成本高,二来就是随着时间的增长它的稳定性会下降这里有篇帖子大家可以参考里面的发言
  软件消抖最大的问题就是等待的延时时间浪费了,等待按键释放,我就是不放,你能怎么样?没办法只能做超时。那我想做长按1s呢?想想就知道很麻烦
  所以我们采用了状态机的方法把按键的延时用定时器完成这样就可以节约时间了

状态机

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

基于STM32的按键状态机

按钮初始化结构体

typedef struct // 构造按键初始化类
{
	GPIOMode_TypeDef GPIO_Mode; // 初始化按键模式
	GPIO_TypeDef* GPIOx; // 初始化按键口
	uint16_t GPIO_Pin_x; // 初始化按键引脚好
	uint32_t RCC_APB2Periph_GPIOx; // 初始化时钟
}Key_Init;

  由于按键有独立按键和矩阵按键,这里采取了底层为独立按键,针对独立按键抽象出来了一个独立按键初始化类。实际上c语言是个面向过程的语言但是我看很多底层代码都会用到面向对象的思维,比如hal库的代码。这里第一次尝试这种编程思维如果有一些不合理的地方希望读者可以私信或者评论指出。

状态机初始化结构体

#include "stm32f10x_gpio.h"
#include "stm32f10x.h"
typedef struct _KEY_COMPONENTS // 状态机类
{
    FunctionalState KEY_SHIELD;       //按键屏蔽0:屏蔽,1:不屏蔽
	uint8_t KEY_COUNT;        //按键长按计数
    BitAction KEY_LEVEL;        //虚拟当前IO电平,按下1,抬起0
    BitAction KEY_DOWN_LEVEL;   //按下时IO实际的电平
    KEY_STATUS_LIST KEY_STATUS;       //按键状态
    KEY_STATUS_LIST KEY_EVENT;        //按键事件
    BitAction (*READ_PIN)(Key_Init Key);//读IO电平函数
}KEY_COMPONENTS;

  KEY_SHIELD按键屏蔽用的,这里用的是stm32f10x.h里的FunctionalState枚举类型,DISABLE表示按键不使用,ENABLE表示使用;

typedef enum 
{
	DISABLE = 0, 
	ENABLE = !DISABLE
} FunctionalState;

  KEY_COUNT长按计数器,好比秒表,按开始然后开始计数了;
  KEY_LEVEL虚拟按键按下的电平,用到了stm32f10x_gpio.h里的BitAction枚举类型,Bit_RESET表示低电平,Bit_SET表示高电平;

typedef enum
{ 
	Bit_RESET = 0,
 	Bit_SET
}BitAction;

  KEY_DOWN_LEVEL,实际按键按下的IO电平,这两个变量,主要是为了函数封装进行统一,比如你一个按键按下高电平,一个按下低电平,我不管这么多,反正我就和你KEY_DOWN_LEVEL值进行比较,相等我就认为你按下,然后把KEY_LEVEL置位,相反就清零;(后面还有解释)
  KEY_STATUS就是我们说的按键状态了,它负责记录某一时刻按键状态,这里自己创建了一个枚举类型表示可能的情况,状态有四种空状态,确认状态,按下状态,长按状态;

typedef enum _KEY_STATUS_LIST // 按键状态
{
	KEY_NULL = 0x00, // 无动作
	KEY_SURE = 0x01, // 确认状态
	KEY_UP   = 0x02, // 按键抬起
	KEY_DOWN = 0x04, // 按键按下
	KEY_LONG = 0x08, // 长按
}KEY_STATUS_LIST;

  KEY_EVENT表示按键事件,我这里分了3个事件,有按下、抬起和长按。
  状态是一段事件,而事件是一瞬间的,为了便于理解状态和事件的关系我做了下面这个图
状态转换与事件的关系

  READ_PIN是一个函数指针变量,需要把你读IO的函数接口给它。

按键结构体

typedef struct // 按键类
{
	Key_Init* Key; // 继承初始化父类
	KEY_COMPONENTS Status; // 继承状态机父类
}Key_Config;

按键注册表

typedef enum _KEY_LIST // 按键注册表
{
	KEY0, // 用户添加的按钮名称
	KEY1, // 用户添加的按钮名称
	WK_UP, // 用户添加的按钮名称
	KEY_NUM, // 必须要有的记录按钮数量,必须在最后
}KEY_LIST;

  这里用枚举类型做了一个按键的注册表,就比如我需要配置三个按键KEY0,KEY1,WK_UP那么我就需要在注册表里添加它们。根据枚举类型的特性如果没有赋值那么它就会自动从小到大赋值整数所以KEY_NUM只要在最后就可以表示按钮的数量

状态机函数

void Creat_Key(Key_Init* Init); // 初始化按钮函数
void ReadKeyStatus(void); // 状态机函数

  这里只有两个函数一个是初始化函数

Key_Config Key_Buf[KEY_NUM];// 创建全局对象

static BitAction KEY_ReadPin(Key_Init Key) //按键读取函数
{
  return (BitAction)GPIO_ReadInputDataBit(Key.GPIOx,Key.GPIO_Pin_x);
}

void Creat_Key(Key_Init* Init)
{
	uint8_t i; 
	GPIO_InitTypeDef  GPIO_InitStructure[KEY_NUM];
  	for(i = 0;i < KEY_NUM;i++)
	{
		Key_Buf[i].Key = &Init[i]; // 按钮对象的初始化属性赋值
		RCC_APB2PeriphClockCmd(Key_Buf[i].Key->RCC_APB2Periph_GPIOx, ENABLE);//使能相应时钟
		GPIO_InitStructure[i].GPIO_Pin = Key_Buf[i].Key->GPIO_Pin_x;	//设定引脚			
		GPIO_InitStructure[i].GPIO_Mode = Key_Buf[i].Key->GPIO_Mode; 	//设定模式		
		GPIO_Init(Key_Buf[i].Key->GPIOx, &GPIO_InitStructure[i]);       //初始化引脚
		// 初始化按钮对象的状态机属性
		Key_Buf[i].Status.KEY_SHIELD = ENABLE;
		Key_Buf[i].Status.KEY_COUNT = 0;
		Key_Buf[i].Status.KEY_LEVEL = Bit_RESET;
		if(Key_Buf[i].Key->GPIO_Mode == GPIO_Mode_IPU) // 根据模式进行赋值
			Key_Buf[i].Status.KEY_DOWN_LEVEL = Bit_RESET;
		else
			Key_Buf[i].Status.KEY_DOWN_LEVEL = Bit_SET;
		Key_Buf[i].Status.KEY_STATUS = KEY_NULL;
		Key_Buf[i].Status.KEY_EVENT = KEY_NULL;
		Key_Buf[i].Status.READ_PIN = KEY_ReadPin;	//赋值按键读取函数
	}
}

  以上就是按钮对象的初始化的函数调用的时候需要创建一个Init的按键初始化对象,后面会细说用法

#define KEY_LONG_DOWN_DELAY 30 // 设置长按计数器为30个计时器的中断,因为计时器是20ms一次所以就是600ms算长按

static void Get_Key_Level(void) // 根据实际按下按钮的电平去把它换算成虚拟的结果
{
    uint8_t i;
    
    for(i = 0;i < KEY_NUM;i++)
    {
        if(Key_Buf[i].Status.KEY_SHIELD == DISABLE)
            continue;
        if(Key_Buf[i].Status.READ_PIN(*Key_Buf[i].Key) == Key_Buf[i].Status.KEY_DOWN_LEVEL)
            Key_Buf[i].Status.KEY_LEVEL = Bit_SET;
        else
            Key_Buf[i].Status.KEY_LEVEL = Bit_RESET;
    }
}

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

  这里解释一下static void Get_Key_Level(void)函数的意义,众所周知因为按钮有共阴极共阳极接法如下图所示
共阴极共阳极接法
  像WK_UP就是共阳极接法,引脚需要下拉,按钮按下的时候是高电平,松开是低电平,
而KEY0和KEY1就是共阴极解法,引脚需要上拉,按钮按下的时候是低电平,而松开的时候是高电平。
所以我们在做判断的时候会出现有些的按下是低电平有些的是按下高电平不方便我们判断,状态机里的KEY_DOWN_LEVEL就是实际的的按下时候的电平,KEY0和KEY1这里我们就需要赋值为Bit_RESET(低电平),而WK_UP就要赋值为Bit_SET(高电平)这个在上面的void Creat_Key(Key_Init* Init)里也有体现

if(Key_Buf[i].Key->GPIO_Mode == GPIO_Mode_IPU) // 根据模式进行赋值
		Key_Buf[i].Status.KEY_DOWN_LEVEL = Bit_RESET;
	else
		Key_Buf[i].Status.KEY_DOWN_LEVEL = Bit_SET;

  那么通过KEY_DOWN_LEVEL我们只需要判断读出来的电平和它是否相同,如果相同那就把KEY_LEVEL 设置成Bit_SET,如果不同就把KEY_LEVEL 设置成Bit_RESET这样一来我们就不用管它电平是高电平还是低电平统一的把KEY_LEVEL 是Bit_SET看成按钮按下,把KEY_LEVEL 是Bit_RESET看成按钮抬起

static void Get_Key_Level(void) // 根据实际按下按钮的电平去把它换算成虚拟的结果
{
    uint8_t i;
    
    for(i = 0;i < KEY_NUM;i++)
    {
        if(Key_Buf[i].Status.KEY_SHIELD == DISABLE)
            continue;
        if(Key_Buf[i].Status.READ_PIN(*Key_Buf[i].Key) == Key_Buf[i].Status.KEY_DOWN_LEVEL)
            Key_Buf[i].Status.KEY_LEVEL = Bit_SET;
        else
            Key_Buf[i].Status.KEY_LEVEL = Bit_RESET;
    }
}

  然后就是解释void ReadKeyStatus(void)了,这个看代码如果还是有点问题那么我用程序框图总结了一下
按键状态机流程图
  最终我们需要访问的就是KEY_EVENT按键事件就行

实例用法

  首先看看主函数

#include "key.h"
#include "usart.h"
#include "timer.h"

int main()
{
	uart_init(115200); // 用于查看输出
	TIM3_Int_Init(72-1,20000-1); //调用定时器使得20ms产生一个中断
	Key_Init KeyInit[KEY_NUM]=
	{
		{GPIO_Mode_IPU, GPIOC, GPIO_Pin_1 , RCC_APB2Periph_GPIOC}, // 初始化KEY0
		{GPIO_Mode_IPU, GPIOC, GPIO_Pin_13, RCC_APB2Periph_GPIOC}, // 初始化KEY1
		{GPIO_Mode_IPD, GPIOA, GPIO_Pin_0 , RCC_APB2Periph_GPIOA}  // 初始化WK_UP
	};
	Creat_Key(KeyInit); // 调用按键初始化函数
	while(1)
	{

	}
}

  非常简洁,第二就是对于定时器的中断函数里怎么使用状态机

void TIM3_IRQHandler(void)   //TIM3中断
{
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
	{
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 
		ReadKeyStatus();  //调用状态机
		uint8_t i, status;
		for(i = 0;i < KEY_NUM;i++)
    	{
			status = Key_Buf[i].Status.KEY_EVENT;
			if(status!=KEY_NULL)
				printf("%d,%d\n",i,status);//事件处理
		}
	}
}

  这里先用ReadKeyStatus();更新状态和事件再读取每个按钮事件,如果产生事件则进行处理,这里我们直接输出按钮的编号和状态

运行结果

  首先来回顾一下事件的枚举类型 KEY_UP = 0x02,KEY_DOWN = 0x04,KEY_LONG = 0x08,当我们短按的时候,串口查看器里的显示是这样的
串口查看器结果
  这里0表示第1个按钮也就是KEY0,4表示按下事件,2表示松开事件
串口查看器结果
  这里是长按的结果,4表示按下事件,8表示长按事件,2表示松开事件
串口查看器结果
  这里是按着不放的结果,4表示按下事件,8表示长按事件,2表示松开事件,一直出现8

结论

  状态机好处在于可以通过定时器帮我们节省堵塞的时间,可以记录多种状态,来打到一个按键多种用途。面向对象的思维可以让我们通过修改初始化的部分,变成矩阵按钮等更多用途。

原创文章 10 获赞 10 访问量 1919

猜你喜欢

转载自blog.csdn.net/qq_42679566/article/details/105892105