外设驱动库开发笔记39:按键操作驱动

  按键在我们的项目中是经常使用到的组件。一般来说,我们都是在用到按键时直接针对编码,但这样每次都做很多重复性的工作。所以在这里我们考虑做一般性抽象得到一个可应用于按键操作的通用性驱动程序。

1、功能概述

  按键操作在我们的产品种经常用到,一般都是在特定的应用环境中直接有针对性的操作。但这些按键的操作往往有很多的共性,这就为代码复用提供了可能。

1.1、按键的定义

  在开始考虑按键操作之前,我们先来分析一下究竟什么是按键。按键一般来讲就是用于信号输入的按钮,通过响应它的操作我们可以实现想要的功能。但我们这里所说的按键不仅包括普通的单体按键,还包括如组合键、键盘等。

  对于这些种类的按键它们的形态、功能或许有较大的差异,但我们可以对它们所进行的操作却很类似。这也是我们能够统一考虑它们的基础。

1.2、原理分析

  我们已经给我们要操作的按键划分了范围,在此基础上我们简单分析实现按键操作的基本原理。

  首先我们来考虑按钮操作的原理,其实很简单,无非就是按下或者弹起两种状态。至于按钮本身是常开或者常闭,是低电平有效还是高电平有效都没有问题,我们只要能检测出其状态就可以了。我们考虑按键的按下、弹起、连击和长按等状态,如下图:

  其次我们来考虑按键状态的存储。在系统中的多个按键需要操作时,如何处理响应事件就会是一个问题。我们考虑以先入先出队列来存储按键的状态,进而根据状态进行操作。我们需要设计一个队列,这是一个先入先出的队列,拥有一定的存储空间和读写操作指针,具体如下图所示:

  在上图中,当读指针与写指针一样时,则表示队列为空。当写入一个数据,则写指针加一;当读出一个数据,则读指针加一;当读指针遇到写指针则表示在没有数据了。

  最后来说一说按键状态的响应。所谓响应其实就是对不同的状态我们来处理不同的事件。对于每个按键我们根据其状态定义事件。在不同的事件中处理我们需要的功能。

  在上图中,状态和时间都可以在我们的对象中声明,但具体的实现形式在应用中完成。

2、驱动设计与实现

  我们已经简单分析了按键的基本操作原理,接下来我们将以此为基础来分析并设计按键操作的通用驱动方法。

2.1、对象定义

  我们依然采用基于对象的操作方式。当然前提是我们得到了可用于操作的对象,所以我们先来分析一下如何抽象面向按键操作的对象。

2.1.1、定义对象类型

  一般来讲,一个对象会包括属性和操作。接下来我们就从这两个方面来考虑按键对象问题。

  首先我们来考虑按键对象的属性问题。我们的系统中总有多个按键,为了区分这些按键我们为每一个按键分配一个ID,用于区别这些按键。所以我们将按键ID作为其一个属性。对于按键操作我们一般都会有软件滤波来实现消抖,我们一如一个滤波计数用以实现这一过程,我们将滤波计数也当作它的一个属性。长按键我们需要预设检测时长,同时需要一个计数来记录这一过程,所以我们将其设为属性。同样连续按键的周期需要预设,而且需要计数来记录过程,所以也将这两个作为属性。当然按键当前的状态,我们也可能需要记录一下,按键按下时的有效电平,我们也需要分辨,这些我们也都将其作为属性。综上所述按键对象的类型定义如下:

    /*定义按键对象类型*/typedef struct KeyObject {uint8_t id;							//按键的IDuint8_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枚举配合使用简化操作。对于组合键要检测多个物理硬件,但也是对这些但体检的检测,所以在硬件上不需要定义。

欢迎关注:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499304.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

外设驱动库开发笔记40:AT25xxx外部存储器驱动

我们在前面开发过AT24CXX系列EEPROM存储器&#xff0c;它使用的是I2C接口。不过有时候我们也会使用SPI接口的EEPROM存储器。在这一篇我们将来讨论AT25XXX系列EEPROM存储器的驱动设计、实现及使用。 1、功能概述 AT25XXX系列EEPROM存储器采用SPI接口&#xff0c;因其操作简单且…

闪存中的键值对:无文件系统 minINI

许多嵌入式系统应用需要以持久的方式存储某种数据&#xff1a;校准值、设置或日志信息。对于较少的数据量&#xff0c;使用外部存储器或文件系统是一种过度大材小用。在许多系统中&#xff0c;我使用minINI以“ini-file”的方式存储键值解析&#xff0c;但它需要使用某种文件系…

外设驱动库开发笔记41:ADS1256 ADC驱动

我们经常会碰到多通道AD采集的需求&#xff0c;有时候甚至需要高精度的ADC器件。本篇我们将来设计并实现ADS1256模数转换器的驱动。并简单讨论该驱动使用方式。 1、功能概述 ADS1256是TI公司推出的一款低噪声高分辨率的24位Sigma-Delta(E-v)模数转换器(ADC)。E-vADC与传统的逐…

PID参数自整定库之一:继电反馈整定算法

在前述的篇章中&#xff0c;我们实现了PID控制器并在后续对其进行了改进。但作为经典PID控制器还存在PID参数整定的问题。通常我们可以采取人工整定的办法&#xff0c;但人工整定涉及到比较专业的知识&#xff0c;而且找到合适的参数本身也不是一件容易的事&#xff0c;所以人们…

外设驱动库开发笔记42:DAC8552 DAC驱动

模拟信号输出是经常会遇到的应用需求&#xff0c;解决的办法应多种&#xff0c;但我们使用最多的还是数模转换。对于不同的数模转换器我们需要为其编写适用的驱动程序&#xff0c;在这一篇中我们就来考虑如何实现DAC8552高精度模数转换器的驱动程序。 1、功能概述 该DAC8552是…

软件设计开发笔记2:基于QT设计串口调试工具

串口通信是我们经常会遇到的问题。很多时候当我们设计一个串口应用时&#xff0c;我们希望有一个简便的、可视的方式来验证它。这一篇中我们就来基于QT设计一个串口调试工具。 1、概述 在开始软件设计之前&#xff0c;我们来简略地分析一下这样一个小软件其要包含的主要内容有…

外设驱动库开发笔记43:GPIO模拟SPI驱动

SPI总线是我们常用的串行设备接口&#xff0c;一般情况下我们都会适应硬件SPI接口&#xff0c;但有些时候当硬件端口不足时&#xff0c;我们也希望可以使用软件来模拟SPI硬件接口&#xff0c;特别是要求不是很高的时候。在这一篇中我们将来讨论如何使用GPIO和软件来模拟SPI通讯…

外设驱动库开发笔记44:DDC114 ADC驱动

在产品设计过程中&#xff0c;很多时候都会用到ADC器件&#xff0c;而在一些特殊场合还需要一些特别的ADC器件。我们在这篇中将讨论常用于医疗器件方面的&#xff0c;DDC114这款电流输入ADC&#xff0c;并为其设计一个驱动程序。 1、功能概述 模数转换器DDC114是一款电流输入型…

PID控制器改进笔记之七:改进PID控制器之防超调设定

我们已经设计了PID控制器&#xff0c;并根据实际使用的情况对器进行了诸多的改进。在这一篇中我们将讨论如何改进PID控制器超调的问题。 1、问题提出 在前面的文章中&#xff0c;我们曾推导过增量式PID控制器的公式&#xff0c;并且对其进行了离散化以适用于程序实现&#xff…

软件设计开发笔记3:基于QT的Modbus RTU主站

Modbus是一种常见的工业系统通讯协议。在我们的设计开发工作中经常使用到它。在这一篇中我们将简单实现一个基于QT的Modbus RTU主站上位工具。 1、概述 Modbus RTU主站应用很常见&#xff0c;有一些是通用的&#xff0c;有一些是专用的。而这里我们希望实现一个主要针对我们的…

外设驱动库开发笔记45:MS4515DO压力传感器驱动

很多时候我们需要检测流量和压力这些参数&#xff0c;比如我们要检测大气压&#xff0c;或者通过测量差压来获得输送流体的流量等&#xff0c;都需要用到压力传感器。这一篇我们就来讨论MS4515DO压力传感器的数据获取。 1、功能概述 MS4515DO是TE公司推出的一款基于PCB安装的小…

外设驱动库开发笔记48:MCP4725单通道DAC驱动

在产品设计过程中&#xff0c;我们经常会遇到数模转换的应用需求。在本篇种我们就来讨论一下MCP4725单通道数模转换器的驱动设计与实现。 1、功能概述 MCP4725是一个低功耗&#xff0c;高精度&#xff0c;单通道&#xff0c;12位缓冲电压输出数字到模拟转换器(DAC)与非易失性存…

如何确保不使用动态内存

在许多嵌入式应用程序中&#xff0c;内存分配必须是静态的&#xff0c;而不是动态的。意味着在应用程序中不应使用对malloc()或free()等内容的调用&#xff0c;因为它们可能会在运行时失败&#xff08;内存不足、堆碎片&#xff09;。 但是&#xff0c;当与第三方库甚至 C/C 标…

go 单元测试 testing 打印输出_2020,你需掌握go 单元测试进阶篇

本文说明go语言自带的测试框架未提供或者未方便地提供的测试方案&#xff0c;主要是用于解决写单元测试中比较头痛的依赖问题。也就是伪造模式&#xff0c;经典的伪造模式有桩对象(stub),模拟对象(mock)和伪对象(fake)。比较幸运的是&#xff0c;社区有丰富的第三方测试框架支持…

一文读懂Git工作流

Git是目前最流行的代码管理工具&#xff0c;相信大家也都是在用Git来管理自己团队的源代码。 团队一般为了规范开发&#xff0c;保持良好的代码提交记录以及维护 Git 分支结构清晰&#xff0c;方便后续维护等&#xff0c;都会迫切需要一个比较规范的 Git 工作流。 本文就是在…

xbox360fsd更新游戏封面_游戏类短视频创作指南

一&#xff0e;起步阶段1.内容发布垂直&#xff0c;整体风格一致&#xff0c;选定一个品类的游戏内容风格持续更新注意&#xff1a;冷启动时期不要频繁更换游戏类型2.账号IP化 根据自身风格特色打造独特的风格账号。有利延长账号生命周期&#xff0c;提升粉丝转化率。搞笑、中二…

开发者们都在关注的网站

开发者们都在关注的网站 &#x1f609; 综合类&#xff08;5个&#xff09; 1、GitHub 全球最大的编程开源社区&#xff0c;很多优秀的开源项目都在上边&#xff0c;不知道这个都不要说自己是程序员&#x1f602; 访问地址&#xff1a;https://github.com 2、CSDN 全球最大中…

ios framework 调用第三方 framework_Python基础:标准库和常用的第三方库

Python的标准库有&#xff1a;名称作用datetime为日期和时间处理同时提供了简单和复杂的方法。zlib直接支持通用的数据打包和压缩格式&#xff1a;zlib&#xff0c;gzip&#xff0c;bz2&#xff0c;zipfile&#xff0c;以及 tarfile。random提供了生成随机数的工具。math为浮点…

作图神器ProcessOn - 免费好用

因工作需要&#xff0c;我经常需要花一些流程图&#xff0c;时序图&#xff0c;架构图什么的&#xff0c;之前使用的Windows系统&#xff0c;大部分情况下就用的Visio来画图。后来为了工作方便&#xff0c;换成了Mac电脑&#xff0c;结果发现Mac上没有Visio&#xff0c;然后就在…

三电平igbt死区时间计算_基于大功率三电平IGBT模块并联的参考设计

当前的可再生能源行业中&#xff0c;光伏和风力发电均面临着补贴逐步退坡&#xff0c;平价上网时代即将到来的挑战。为应对这一挑战&#xff0c;光伏逆变器和风力变流器厂家研发的新品单机功率越来越高&#xff0c;以取得更低的单位功率成本。市场上1.5MW的集中式光伏逆变器和3…