一般引用都写在最后,但是这篇对我这个状态机的影响很大,我这里有许多借鉴他的思维。所以写在前面,如有侵权立即删除
一开始学习单片机的时候我接触到按键的时候就知道按键有抖动,记得当初按键消抖分为硬件和软件,硬件上常用于复位按键如下图
if(KEY1 == 0)
{
delay_ms(20); // 延时消抖
if(KEY1 == 0)
{
while(KEY1 == 0);//堵塞,等待松开
// 按键按下处理代码
}
}
硬件消抖一来就是成本高,二来就是随着时间的增长它的稳定性会下降大家可以参考里面的发言
软件消抖最大的问题就是等待的延时时间浪费了,等待按键释放,我就是不放,你能怎么样?没办法只能做超时。那我想做长按1s呢?想想就知道很麻烦
所以我们采用了状态机的方法把按键的延时用定时器完成这样就可以节约时间了
状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机的内在因果关系的考虑。
"现态"和"条件"是因,"动作"和"次态"是果。
现态:是指当前所处的状态。
条件:当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
次态:条件满足后要迁往的新状态。"次态"是相对于"现态"而言的,"次态"一旦被激活,就转变成新的"现态"了。
有了理论的支撑,有没有发现,状态机这种机制,其实可以运行到很多场景的啦,不仅仅局限于按键。
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)函数的意义,众所周知因为按钮有共阴极共阳极接法如下图所示
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)了,这个看代码如果还是有点问题那么我用程序框图总结了一下
首先看看主函数
#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,当我们短按的时候,串口查看器里的显示是这样的
状态机好处在于可以通过定时器帮我们节省堵塞的时间,可以记录多种状态,来打到一个按键多种用途。面向对象的思维可以让我们通过修改初始化的部分,变成矩阵按钮等更多用途。
因篇幅问题不能全部显示,请点此查看更多更全内容