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

2、按下时间满足了长按时间松开,识别过程如下图(短按有效时间中包含了去抖动时间):

三、示例代码
源文件部分代码:
/**
* @brief 获取按键单击次数
*
* @note 调用该函数时不能有关于按键动作的限制条件
* @param[in] eKey 指定按键, 取值为 @reg IoKeyType_e
* @param[in] time 每次单击的间隔时间
* @retval 单击次数.
*/
uint8_t KEY_GetClickCnt(IoKeyType_e eKey, uint16_t time)
{
if (eKey >= IO_KEY_MAX_NUM)
{
return 0;
}
/* 连续单击后等间隔时间再进行回调通知, 目的在松开按键后再次触发得到单击次数 */
if (sg_tKeyManage[eKey].eKeyAction == KEY_ACTION_LOSSEN)
{
sg_tKeyManage[eKey].refreshNotifyTic = time;
}
if (sg_tKeyManage[eKey].eKeyActionBak != KEY_ACTION_PRESS && sg_tKeyManage[eKey].eKeyAction == KEY_ACTION_PRESS)
{
sg_tKeyManage[eKey].refreshNotifyTic = 0;
if (sg_tKeyManage[eKey].clickCnt < 0xFF)
{
sg_tKeyManage[eKey].clickCnt++;
}
}
if (sg_tKeyManage[eKey].eKeyAction == KEY_ACTION_NO_PRESS && sg_tKeyManage[eKey].uiLossenTic >= time)
{
uint8_t cnt = sg_tKeyManage[eKey].clickCnt;
sg_tKeyManage[eKey].clickCnt = 0;
return cnt;
}
return 0;
}
头文件部分代码:
/* Exported types ----------------------------------------------------------------------------------------------------*/
typedef enum
{
KEY_ACTION_NO_PRESS = 0, /*!< 没有按下 */
KEY_ACTION_PRESS, /*!< 持续按下 */
KEY_ACTION_LOSSEN /*!< 按下松开 */
} KeyAction_e;
typedef void (*KeyFunCB)(IoKeyType_e, KeyAction_e);
/* Exported constants ------------------------------------------------------------------------------------------------*/
/* Exported macro ----------------------------------------------------------------------------------------------------*/
/* Exported functions ------------------------------------------------------------------------------------------------*/
extern void KEY_Init(void);
extern void KEY_Register(IoKeyType_e eKey, KeyFunCB pfnKeyFun);
extern void KEY_UnRegister(IoKeyType_e eKey);
extern void KEY_SetNotifyTime(IoKeyType_e eKey, uint16_t time);
extern uint8_t KEY_GetClickCnt(IoKeyType_e eKey, uint16_t time);
extern uint16_t KEY_GetPressTime(IoKeyType_e eKey);
extern uint16_t KEY_GetLossenTime(IoKeyType_e eKey);
extern void KEY_Scan(uint16_t cycleTime);
四、代码获取
https://gitee.com/const-zpc/key
原文链接:https://blog.csdn.net/qq_24130227/article/details/123782929
版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。
觉得好看,请点这里↓↓↓
文章评论