前言
本文目的是讲述一个按键扫描处理的面向对象开发的设计思想,适用于裸机开发,通过按键扫描,检测到按键是否按下,松开等状态,并将该状态通过其他形式反馈给其他模块进行处理。初次使用按键时,最常用的办法就是如以下代码一样,硬延时抖动滤波,等待确认后做相应的处理。
void KEY_Scan(void)
{
if(KEY0 == 0)
{
delay_ms(10);//去抖动
if(KEY0 == 0)
{
// 处理想做的事情
}
}
}
以上方式最大的弊端就是硬延时去抖动,极大的浪费了 CPU 资源,对熟悉阻塞式和非阻塞式程序开发的人来说,这种写法是十分不可取的;程序框架稍微好的,会摒弃此种做法,采用分时调度(时间片论法)的形式按时(比如10ms)调用该函数,通过一定累计次数确定按键是否有动作,并处理相关数据。
重构后的代码模块:轻量级按键动作识别模块(C语言)
设计需求
软件功能需求
- 支持六种状态:没有按下、首次按下、短按持续按下、短按松开、长按持续按下和长按松开,其中后两种可以使能或禁止。
- 支持独立设置每个按键的模式和时间:短按模式和长按模式(区分短按和长按两种状态),且时间也可独立设置。
- 支持大部分形式的按键输入,只要满足按键操作为 0 和 1 两种逻辑状态,如以下几种均支持,且兼容各种形式按键组合:
- 矩形按键:通过多组 I/O 识别到对应 key 按下;
- 三态开关:一个开关包含两个 key1 和 key2,高电平为 key1 按下,低电平为 key2 按下;
- 模拟量开关:在一定范围表示 key1 按下、key2 按下等;
- 数字组合开关:如 0x01 代表 key1 按下,0x02 代表 key2 按下,0x04 代表key3 按下(如解析红外线遥控器的接收端 I/O 数据);
- ....等等只要最终表现满足 0 和 1 两种逻辑状态均支持;
- 模块和分层化,因此支持良好的移植性、维护性和可配置性等。
- 代码量小,便于理解及维护。
- 其他想到再说。
软件设计需求
- 模块分层:便于在不同系列 MCU 或开发平台上移植,并提高维护性;
- 非阻塞式:按键扫描任务中不允许内部有任何的延时函数进行去抖动;
- 面向对象:将按键的扫描过程封装起来,对外提供统一的接口,用来执行按键扫描任务和获取按键的操作状态等。
设计思路
模块分层
大致将按键功能模块分为四类文件(一类文件含 .c 和 .h ):
- key_drv:底层驱动初始化、底层驱动信号输入,移植时可根据原理图等初始化硬件资源,并将硬件驱动输入信号转换为 0 和 1 两种逻辑状态。
- key_core:按键操作状态识别的核心代码,通过配置信息完成按键状态的识别和相关 API 函数,移植时不需要修改。
- key_cfg:按键相关配置,如按键模式、按键有效操作时间等,移植时根据需要对按键进行初始化配置。
- key_user:按键自定义处理,通过调用 key_core 中的相关 API 函数完成按键初始化和扫描任务,并可增加其他按键形式,如编码开关等最终表现形式不满足 0 和 1 两种逻辑关系的按键开关。
非阻塞式
由于按键不能通过软件延时去抖动的方式,则需:
- 计时器:按键扫描采样时间的计时器,当检测到按键首次按下时,则触发计时器计数,当满足去抖动时间时,则表示按键正常按下。
面向对象
将按键扫描过程封装起来,则表示需要将过程和数据(操作状态)分离:
- 操作状态:通过按键扫描,可识别没有按下、首次按下、短按持续按下、短按松开、长按持续按下和长按松开六种简单状态。
- 分离方式:通常做法是当检测到按键某种状态时,需要调用相关回调函数进行处理(若封装则只能通过回调函数处理,否则涉及到修改按键模块源代码);我的做法是,当检测到按键操作时,先记录按键操作状态,之后可通过调用读取按键操作状态接口函数返回记录的按键操作状态,这样做法的好处在按键扫描任务执行时间不会由于回调函数处理的时间过长而变长。
设计方案
接口函数及变量定义
key_drv
- KEY_Initialize():按键驱动的初始化,如相关的 GPIO 初始化;
- KEY_InputHandle():对按键的输入信号处理,完成对按键输入信号转变为 0 或 1 的逻辑状态关系;
- KEY_GetInputState():获取单个指定按键的逻辑状态值;
key_cfg
- 不提供相关函数 ,只提供配置信息结构体的定义和配置信息结构体变量,通过写入初始值完成所有按键的配置。
- 短按模式:支持没有按下、首次按下、短按持续按下和短按松开四种按键状态;
- 长按模式:支持没有按下、首次按下、短按持续按下、短按松开、长按持续按下和长按松开六种状态;
- 短按和长按模式可独立设置按下有效时间。
key_core
- KEY_ConfigInit():按键配置信息的初始化,如按键的计时器等运行过程中使用的变量初始化;
- KEY_ConfigHandle():按键配置信息的处理,根据按键的逻辑状态值和配置信息识别并记录按键操作状态;
- KEY_ReadAction():读取按键记录的动作状态,通过调用该函数,依次返回记录的操作状态(从按下到松开的过程状态 );
- KEY_ResetAction():复位按键的动作状态,清除记录的动作状态;
- 其他函数:可修改按键的相关配置信息,如按键模式,按键有效时间等;
key_user
- KEY_UserInit():调用底层驱动初始化和配置信息初始化,完成按键功能模块的初始化;
- KEY_ScanTask():调用按键的输入信号处理和按键配置信息的处理,完成按键功能模块的按键动作状态识别;
- 其他函数:由于以上只适用于 0 和 1 两种逻辑状态的按键开关表现形式且通过查询非中断的检测方式,则可添加不满足 0 和 1 两种逻辑状态的按键开关,如编码器,需要通过中断触发的形式完成,此时就不适用与 key_core 文件中的处理,因此在 key_user 文件下可添加该编码器开关的功能,通过按键功能模块统一管理所有按键。
按键动作识别
短按模式
在禁止长按模式下的按键从按下到松开一系列动作识别过程如下图(短按有效时间中包含了去抖动时间):
长按模式
在使能长按模式下的按键动作识别过程有两种情况:
1、按下时间没有满足长按时间松开,和禁止长按模式的识别过程一致,如下图(短按有效时间中包含了去抖动时间):
2、按下时间满足了长按时间松开,识别过程如下图(短按有效时间中包含了去抖动时间):