外设驱动库开发笔记39:按键操作驱动
按键在我们的项目中是经常使用到的组件。一般来说,我们都是在用到按键时直接针对编码,但这样每次都做很多重复性的工作。所以在这里我们考虑做一般性抽象得到一个可应用于按键操作的通用性驱动程序。
1、功能概述
按键操作在我们的产品种经常用到,一般都是在特定的应用环境中直接有针对性的操作。但这些按键的操作往往有很多的共性,这就为代码复用提供了可能。
1.1、按键的定义
在开始考虑按键操作之前,我们先来分析一下究竟什么是按键。按键一般来讲就是用于信号输入的按钮,通过响应它的操作我们可以实现想要的功能。但我们这里所说的按键不仅包括普通的单体按键,还包括如组合键、键盘等。
对于这些种类的按键它们的形态、功能或许有较大的差异,但我们可以对它们所进行的操作却很类似。这也是我们能够统一考虑它们的基础。
1.2、原理分析
我们已经给我们要操作的按键划分了范围,在此基础上我们简单分析实现按键操作的基本原理。
首先我们来考虑按钮操作的原理,其实很简单,无非就是按下或者弹起两种状态。至于按钮本身是常开或者常闭,是低电平有效还是高电平有效都没有问题,我们只要能检测出其状态就可以了。我们考虑按键的按下、弹起、连击和长按等状态,如下图:
其次我们来考虑按键状态的存储。在系统中的多个按键需要操作时,如何处理响应事件就会是一个问题。我们考虑以先入先出队列来存储按键的状态,进而根据状态进行操作。我们需要设计一个队列,这是一个先入先出的队列,拥有一定的存储空间和读写操作指针,具体如下图所示:
在上图中,当读指针与写指针一样时,则表示队列为空。当写入一个数据,则写指针加一;当读出一个数据,则读指针加一;当读指针遇到写指针则表示在没有数据了。
最后来说一说按键状态的响应。所谓响应其实就是对不同的状态我们来处理不同的事件。对于每个按键我们根据其状态定义事件。在不同的事件中处理我们需要的功能。
在上图中,状态和时间都可以在我们的对象中声明,但具体的实现形式在应用中完成。
2、驱动设计与实现
我们已经简单分析了按键的基本操作原理,接下来我们将以此为基础来分析并设计按键操作的通用驱动方法。
2.1、对象定义
我们依然采用基于对象的操作方式。当然前提是我们得到了可用于操作的对象,所以我们先来分析一下如何抽象面向按键操作的对象。
2.1.1、定义对象类型
一般来讲,一个对象会包括属性和操作。接下来我们就从这两个方面来考虑按键对象问题。
首先我们来考虑按键对象的属性问题。我们的系统中总有多个按键,为了区分这些按键我们为每一个按键分配一个ID,用于区别这些按键。所以我们将按键ID作为其一个属性。对于按键操作我们一般都会有软件滤波来实现消抖,我们一如一个滤波计数用以实现这一过程,我们将滤波计数也当作它的一个属性。长按键我们需要预设检测时长,同时需要一个计数来记录这一过程,所以我们将其设为属性。同样连续按键的周期需要预设,而且需要计数来记录过程,所以也将这两个作为属性。当然按键当前的状态,我们也可能需要记录一下,按键按下时的有效电平,我们也需要分辨,这些我们也都将其作为属性。综上所述按键对象的类型定义如下:
/*定义按键对象类型*/
typedef struct KeyObject {
uint8_t id; //按键的ID
uint8_t Count; //滤波器计数器
uint16_t LongCount; //长按计数器
uint16_t LongTime; //按键按下持续时间, 0 表示不检测长按
uint8_t State; //按键当前状态(按下还是弹起)
uint8_t RepeatPeriod; //连续按键周期
uint8_t RepeatCount; //连续按键计数器
uint8_t ActiveLevel; //激活电平
}KeyObjectType;
除了按键对象,其实我们还需要定义一个数据队列的对象。者如我们前面所说,队列除了一个数据存储区外还需要读写指针。我们定义如下:
/*定义键值存储队列的类型*/
typedef struct KeyStateQueue{
uint8_t queue[KEY_FIFO_SIZE]; //键值存储队列
uint8_t pRead; //读队列指针
uint8_t pWrite; //写队列指针
}KeyStateQueueType;
2.1.2、对象初始化配置
对象定义之后并不能立即使用我们还需要对其进行初始化。所以这里我们来考虑按键对象的初始化函数。关于对象的初始化,初始化函数需要处理几个方面的问题。一是检查输入参数是否合理;二是为对象的属性赋初值;三是对对象作必要的初始化配置。据此思路我们设计按键对象的初始化函数如下:
/*按键读取初始化*/
void KeysInitialization(KeyObjectType *pKey,uint8_t id,uint16_t longTime, uint8_t repeatPeriod,KeyActiveLevelType level)
{
if(pKey==NULL)
{
return;
}
pKey->id=id;
pKey->Count=0;
pKey->LongCount=0;
pKey->RepeatCount=0;
pKey->State=0;
pKey->ActiveLevel=level;
pKey->LongTime=longTime;
pKey->RepeatPeriod=repeatPeriod;
}
2.2、对象操作
我们已经抽象了按键对象类型,也设计了对象的初始化函数。接下来我们需要考虑使用对象如何实现操作。根据我们前面的分析,操作可分为量个部分:按键状态的检测和键值队列的操作。
2.2.1、按键状态检测
需要周期性的检测按键的状态以便我们响应按键的操作。我们一般10ms检测一次状态,并持续一定的滤波周期用于消抖。我们检测到按键的不同状态后将状态存入到相关的键值队列中。
/*按键周期扫描程序*/
void KeyValueDetect(KeyObjectType *pKey)
{
if (CheckKeyDown(pKey))
{
if (pKey->Count < KEY_FILTER_TIME)
{
pKey->Count = KEY_FILTER_TIME;
}
else if(pKey->Count < 2 * KEY_FILTER_TIME)
{
pKey->Count++;
}
else
{
if (pKey->State == 0)
{
pKey->State = 1;
/*发送按键按下事件消息*/
KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyDown));
}
if (pKey->LongTime > 0)
{
if (pKey->LongCount < pKey->LongTime)
{
/* 发送按建持续按下的事件消息 */
if (++pKey->LongCount == pKey->LongTime)
{
/* 键值放入按键FIFO */
KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyLong));
}
}
else
{
if (pKey->RepeatPeriod > 0)
{
if (++pKey->RepeatCount >= pKey->RepeatPeriod)
{
pKey->RepeatCount = 0;
/*长按键后,每隔10ms发送1个按键*/
KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyDown));
}
}
}
}
}
}
else
{
if(pKey->Count > KEY_FILTER_TIME)
{
pKey->Count = KEY_FILTER_TIME;
}
else if(pKey->Count != 0)
{
pKey->Count--;
}
else
{
if (pKey->State == 1)
{
pKey->State = 0;
/*发送按键弹起事件消息*/
KeyValueEnQueue((uint8_t)((pKey->id<<2)+ KeyUP));
}
}
pKey->LongCount = 0;
pKey->RepeatCount = 0;
}
}
2.2.2、键值队列的操作
&esmp; 键值队列的操作就简单了,主要包括数据的写入、读出、清空队列以及队列是否为空。需要说的是键值的存储,包括量方面类容:按键的ID和按键的状态。我们使用一个字节来存储这些信息,前六个位存储ID,后两位存储状态。具体如下图所示:
这样一种存储格式,我们最多可以存储64个按键和4种状态,当然这还要看队列的大小。
/*键值出队列程序*/
uint8_t KeyValueDeQueue(void)
{
uint8_t result;
if(keyState.pRead==keyState.pWrite)
{
result=0;
}
else
{
result=keyState.queue[keyState.pRead];
if(++keyState.pRead>=KEY_FIFO_SIZE)
{
keyState.pRead=0;
}
}
return result;
}
/*键值入队列程序*/
void KeyValueEnQueue(uint8_t keyCode)
{
keyState.queue[keyState.pWrite]=keyCode;
if(++keyState.pWrite >= KEY_FIFO_SIZE)
{
keyState.pWrite=0;
}
}
3、驱动的使用
我们已经设计了按键操作的驱动程序,还需要对这一设计进行验证。这一节我们将以前面的设计为基础,用一个简单的应用来验证。我们设计4个单体按键,并由它们生出两组组合键,所以我们的应用程序就是面向这6个按键对象进行操作。
3.1、声明并初始化对象
在开始面向一个对象的操作之前,我们需要得到这个对象的一个实例。那么我们要先声明对象。我们前面已经定义了按键对象类型KeyObjectType和存储键值的队列类型KeyStateQueueType。我们使用这两个类型先声明两个对象变量如下:
KeyObjectType keys[6];
KeyStateQueueType keyState;
声明了对象还需要对变量进行初始化。在驱动的设计中我们已经设计了初始化函数,对象变量的初始化操作就通过这一函数来实现。初始化函数需要一些输入参数:
KeyObjectType *pKey,按键对象
uint8_t id,按键ID
uint16_t longTime,长按有效时间
uint8_t repeatPeriod,连按间隔周期
KeyActiveLevelType level,按键按下有效电平
在这些参数中pKey为按键对象,是我们要初始化的对象。而其它参数只需要根据实际设置输入就可以了。说一初始化函数可调用为:
/*按键硬件初始化配置*/
static void Key_Init_Configuration(void)
{
KeyIDType id;
for(id=KEY1;id<KEYNUM;id++)
{
KeysInitialization(&keys[id],id,KEY_LONG_TIME,0,KeyHighLevel);
}
}
关于按键ID,我们使用枚举来定义。与我们前面定义的按键对象数组配合能够起到很好的效果。在这一我们定义按键ID为:
/*定义按键枚举*/
typedef enum KeyID {
KEY1,
KEY2,
KEY3,
KEY4,
KEY1KEY2,
KEY3KEY4,
KEYNUM
}KeyIDType;
按键ID作为作为按键的唯一标识,不但在我们的按键状态记录中要使用到,同时也可作为我们按键对象数组的下标来使用。
3.2、基于对象进行操作
我们定义了对象,接下来就可以基于对象实现我们的应用。对于按键操作我们需要考虑2个方面的事情:一是周期型的检查按键状态并压如队列;二是读取队列中的按键状态触发不同的操作。
首先我们来说一说周期型的检查按键的状态。我们采用10ms的周期来检查按键,所以我们需要使用定时中端的方式来实现,将如下函数加入到10ms定时中端即可。
/*按键扫描程序*/
void KeyScanHandle(void)
{
KeyIDType id;
for(id=KEY1;id<KEYNUM;id++)
{
KeyValueDetect(&keys[id]);
}
}
&esmp;&esmp;其实还有一个回调函数需要实现,其原型如下:
/*检查某个ID的按键(包括组合键)是否按下*/
__weak uint8_t CheckKeyDown(KeyObjectType *pKey)
根据我们定义的按键对象和ID枚举我们实现这个回调函数并不困难,我们实现其如下:
/*检查某个ID的按键(包括组合键)是否按下*/
uint8_t CheckKeyDown(KeyObjectType *pKey)
{
/* 实体单键 */
if (pKey->id < KEY1KEY2)
{
uint8_t i;
uint8_t count = 0;
uint8_t save = 255;
/* 判断有几个键按下 */
for (i = 0; i < KEY1KEY2; i++)
{
if (KeyPinActive(pKey))
{
count++;
save = i;
}
}
if (count == 1 && save == pKey->id)
{
return 1; /* 只有1个键按下时才有效 */
}
return 0;
}
/* 组合键 K1K2 */
if (pKey->id == KEY1KEY2)
{
if (KeyPinActive(&keys[KEY1]) && KeyPinActive(&keys[KEY2]))
{
return 1;
}
else
{
return 0;
}
}
/* 组合键 K3K4 */
if (pKey->id == KEY3KEY4)
{
if (KeyPinActive(&keys[KEY3]) && KeyPinActive(&keys[KEY4]))
{
return 1;
}
else
{
return 0;
}
}
return 0;
}
&esmp;&esmp;此外,我们还需要读取按键的状态并进行相应的响应。我们实现一个简单的处理函数如下:
/*按键处理函数*/
static void KeyProcessing(void)
{
uint8_t keyCode;
keyCode=KeyValueDeQueue();
if(keyCode==((keys[KEY1].id<<2)+KeyDown))
{
//key1按下时触发的事件
}
else if(keyCode==((keys[KEY1].id<<2)+KeyUP))
{
//key1弹起时触发的事件
}
}
4、应用总结
我们已经实现了按键对象的操作,并在次基础上实现了简单的验证。操作的结果符合我们的期望。而且扩展性也很强。
按照我们对信息存储方式和消息队列的设计,最多可以存储64个按键和4中状态,当然这需要看定义的队列的大小。队列不应太小,太小有可能会造成某些按键操不会响应;也不应太大,太大可能会造成操作迟缓和空间浪费。
在应用中,我们建议定义按键ID时最好使用枚举,使用枚举的好处有几点。一是不会出现重复,每个按键能保证有唯一的ID值。二是便于与按键对象数组组合操作,简化编码。三是使用枚举扩展很方便,代码改动比较小。当然,枚举值最好是连续的而且从0开始。
在使用驱动是还需要注意,检测按键操作是只对个体单键的硬件有效,如果可能也使用数组操作,能与ID枚举配合使用简化操作。对于组合键要检测多个物理硬件,但也是对这些但体检的检测,所以在硬件上不需要定义。
欢迎关注:
如果阅读这篇文章让您略有所得,还请点击下方的【好文要顶】按钮。
当然,如果您想及时了解我的博客更新,不妨点击下方的【关注我】按钮。
如果您希望更方便且及时的阅读相关文章,也可以扫描上方二维码关注我的微信公众号【木南创智】