前言:
按键是嵌入式系统经常会用到的外设,编写也很简单,就是检测IO的高低电平,但是因为按键有需要消抖的问题,在单片机学习中会采用delay延时来进行,但是在具体项目中,延时函数是大忌,会影响系统的实时性,所以一般采用定时器来写,本文将利用状态机的思路配合滴答定时器来编写按键扫描。
程序的编写方法以及讲解
1. 先定义函数中用到的参数
/* 按键相关变量*/
typedef struct
{
tEnumKeyStateType eState;
tEnumKeyType tKeyOld;
tEnumKeyType tKeyNew;
uint8_t u8JitterCnts;
}tKeyVar;
解释:
tEnumKeyStateType eState:按键当前的状态
tEnumKeyType tKeyOld:上一次按键的键值
tEnumKeyType tKeyNew:当前按键的键值
uint8_t u8JitterCnts:延时消抖的计数值。
/* 按键类型*/
typedef enum
{
/*无按键按下*/
ENUMKEYNONE = 0,
/*设置按键按下*/
ENUMKEYSET,
/*累加按键按下*/
ENUMKEYADD,
/*移动按键按下*/
}tEnumKeyType;
解释:这段是一个枚举结构,里面是按键按下之后返回的键值,我们根据键值来判断是哪个按键按下的。
/* 按键状态*/
typedef enum
{
ENUM_KEY_IDLE = 0,
ENUM_KEY_WAIT,
}tEnumKeyStateType;
解释:在这里定义两个当前按键的状态,用于简易状态机的状态定义。
2. 函数的编写
void KeyInit(void)
{
/*初始化按键管脚*/
DISP_KEY_DIR &= ~KEY_MASK;
gtKeyVar.eState = ENUM_KEY_IDLE;
gtKeyVar.tKeyOld = ENUMKEYNONE;
gtKeyVar.tKeyNew = ENUMKEYNONE;
}
解释:首先对于按键GPIO的初始化,因为我用的是MSP430,如果采用STM32,只需要把IO配置为输入就行,上拉下拉取决于低电平使能还是高电平使能。
static tEnumKeyType KeyRead(void)
{
uint8_t u8Key = 0;
u8Key = DISP_KEY_IN & KEY_MASK;
/*累加按键*/
if(u8Key == 0x0A)
{
return ENUMKEYMOVE;
}
/*设置按键*/
else if(u8Key == 0x0C)
{
return ENUMKEYADD;
}
/*移动按键*/
else if(u8Key == 0x06)
{
return ENUMKEYSET;
}
/*无按键*/
else
{
return ENUMKEYNONE;
}
}
解释:此函数是检测IO的高低电平,用于获取当前是哪一个按键按下,在STM32中直接替换为读取当前引脚电平函数即可,写到这里,基本上套路都是相同的,不同的是下面这个函数。
tEnumKeyType KeyScan(void)
{
tEnumKeyType tKey = ENUMKEYNONE;
switch (gtKeyVar.eState)
{
case ENUM_KEY_IDLE:
{
gtKeyVar.tKeyNew = KeyRead();
if(gtKeyVar.tKeyNew != ENUMKEYNONE)
{
gtKeyVar.tKeyOld = gtKeyVar.tKeyNew;
gtKeyVar.u8JitterCnts = KEYJITTERCNTS;
gtKeyVar.eState = ENUM_KEY_WAIT;
}
break;
}
case ENUM_KEY_WAIT:
{
if(gtKeyVar.u8JitterCnts == 0)
{
gtKeyVar.tKeyNew = KeyRead();
if(gtKeyVar.tKeyNew == gtKeyVar.tKeyOld)
{
tKey = gtKeyVar.tKeyNew;
}
gtKeyVar.eState = ENUM_KEY_IDLE;
}
break;
}
default:
{
break;
}
}
return tKey;
}
解释:这部分代码是整个程序的核心,程序运行进来时先将tKey清零,然后在switch语句中判断gtKeyVar.eState当前状态,如果当前状态为ENUM_KEY_IDLE,则读取当前的键值并赋值给当前的变量gtKeyVar.tKeyNew,如果当前检测的变量不为0也就是有按键按下时,将当前键值变量gtKeyVar.tKeyNew的值给gtKeyVar.tKeyOld,并且给gtKeyVar.u8JitterCnts进行赋值,“KEYJITTERCNTS”是宏定义的数据,用于调节消抖的时间,我在这里定义的是15,然后就改变当前按键的状态从ENUM_KEY_IDLE变为ENUM_KEY_WAIT。 当函数第二次被轮询时,当前的状态为ENUM_KEY_WAIT,则就会跑第二个case,在第二个case里面检测gtKeyVar.u8JitterCnts是否等于0,如果等于0那么就再次读取键值并赋值给gtKeyVar.tKeyNew,因为在上一次读取键值时gtKeyVar.tKeyOld被赋值也就保存的是上一次的键值,所以判断gtKeyVar.tKeyNew与gtKeyVar.tKeyOld是否相等,如果相等,则认为这个按键被按下并且将键值gtKeyVar.tKeyNew赋值给返回值tKey,然后将按键的状态更改为ENUM_KEY_IDLE,最后返回我们要的键值,这样完成了一次按键的读取,这种写法不仅可以防止误触发,也可以起到消抖并且消抖时间可更改,最重要的就是没有用delay函数,不会让程序死等,不会影响程序的实时性。
到这里,很多人会问,为什么gtKeyVar.u8JitterCnts的值会变成0,我们不是给它赋值了15吗,是因为在滴答定时器中有一条语句是下面这样:
#pragma vector = TIMER0_A0_VECTOR
__interrupt void TIMER0_A0_ISR(void)
{
/*按键定时,计数单位20ms*/
if(gtKeyVar.u8JitterCnts > 0)
{
gtKeyVar.u8JitterCnts--;
}
else
{
;
}
}
在这个函数中我们对这个变量进行的自减,利用定时器去卡按键之间延时,保证了程序的实时性。