这个设计思想能启蒙你很多年,嵌入式裸机按键扫描

摘要:本文目的是讲述一个按键扫描处理的面向对象开发的设计思想,适用于裸机开发,通过按键扫描,检测到按键是否按下,松开等状态,并将该状态通过其他形式反馈给其他模块进行处理。初次使用按键时,最常用的办法就是如以下代码一样,硬延时抖动滤波,等待确认后做相应的处理。

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)
{
return0;
}
/*连续单击后等间隔时间再进行回调通知,目的在松开按键后再次触发得到单击次数 */
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;
}
return0;
}

头文件部分代码:

/* Exported types ----------------------------------------------------------------------------------------------------*/
typedefenum
{
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 ------------------------------------------------------------------------------------------------*/
externvoid KEY_Init(void);
externvoid KEY_Register(IoKeyType_e eKey, KeyFunCB pfnKeyFun);
externvoid KEY_UnRegister(IoKeyType_e eKey);
externvoid KEY_SetNotifyTime(IoKeyType_e eKey, uint16_t time);
externuint8_t KEY_GetClickCnt(IoKeyType_e eKey, uint16_t time);
externuint16_t KEY_GetPressTime(IoKeyType_e eKey);
externuint16_t KEY_GetLossenTime(IoKeyType_e eKey);
externvoid KEY_Scan(uint16_t cycleTime);