三种功能的独立按键
效果
设计一款能实现单击,双击和长按的独立按键程序,并且保证程序有着良好的移植性,支持多个按键。独立按键的扫描程序还应该是非阻塞的,也就是说不能用到内部延时。按键扫描程序通过调用回调函数进行逻辑处理,这样可以使耦合度极大的降低。最后,经过简单的移植就能在不同平台上轻松实现相同功能。呜呜呜,妈妈再也不用担心我要写一天的按键逻辑了
程序逻辑
- 提取所有按键的当前信息,并且生成两个信号。一个是触发信号trg,当按键按下是变为1,等下一次扫描时不管按键状态,直接清零,相当于一个脉冲信号。另一个是持续信号cont,随着按键按下会一直置1,等按键松开才会置0。这两个变量的每一位对应一个实体按键,由于我使用的是无符号短整形,所以最大可以有16个按键。实现原理参考:http://t.csdn.cn/3rftG(我看的那个博客已经被删了,找了一个差不多的)
- 首先遍历触发信号trg,判断是否有按键按下。如果没有直接结束这次按键扫描;如果按下了,就先记录此时的按键位置,然后开启一个计数器。
- 如果开启了计数器,每次扫描就加一次值。如果在一定时间内,检测到同一按键的触发信号再次触发,判断为双击。如果超出这个时间而且没有持续信号,判断为单击。如果持续信号超过更长的一个时间就判断为长按。要注意2和3是互斥的,同时运行的应该只有一个。按键事件对应的标志规则为:按键数×3+n。单击n=0,双击n=1,长按n=2。例如:按键0发生了长按时间,0*3+2=2,那传给按键回调函数的就是2。
那么现在两个问题: 1.如果双击第二次不松,判断为什么?2.如果在长按为到时间,但是又超过了双击判断的时间,判断为什么?这两个问题是现实存在的,但是并不能称为BUG。毕竟每个人的理解不一样,处理的方式也不就不一样。第1个我还是判断为双击,并且不会再判断为长按,第2个就判断为单击。到此,程序在理论上就完成了(其实是先花一下午写程序,再来分析的)
程序源码
头文件:
/**
* @name three_keys
* @brief 三种功能的独立按键
* @feature 支持单击,双击和长按。
* @note 最多支持16个独立按键
* @author yyj
* @date 2022/3/27
*/
#ifndef __THREE_KETS_H
#define __THREE_KETS_H
#include "stm32f10x.h"
#define KEY_NUM 3 //按键个数
#define DOUBLE_TIME 300 //判断双击最长的间隔 单位:ms
#define CONTINUE_TIME 2000 //判断长按的最短持续时间 单位:ms
char Key_Init(short fre);
void Key_Scan(void);
void Key_CallBack(int key);
#endif
c文件:
#include "three_keys.h"
unsigned short trg = 0; //代表触发,每一位是一个单脉冲信号
unsigned short cont = 0; //代表是否连续按下,按键没松开,对应的位就不清零
unsigned short key_time=0; //按键计时变量
unsigned short double_time=0; //实际判断双击要延迟的周期
unsigned short continue_time=0; //实际判断长按要延迟的周期
/**
* @name Key_Init
* @brief 初始化按键的io口
* @param fre:按键扫描频率
* @return 0:初始化正常
* 1:传入的参数错误
* @note 扫描频率不用完全精确,视具体使用情况而定,主要是为了
* 确定双击和长按的时间间隔。误差大一点,只要使用没问题就行
*/
char Key_Init(short fre)
{
float num;
GPIO_InitTypeDef GPIO_InitStruct;
/****移植需要修改的地方****/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4;//PA0
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;//上拉输入
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GPIO_InitStruct);//初始化PA0
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPD;//下拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;//PA0
GPIO_Init(GPIOA,&GPIO_InitStruct);//初始化PA0
/**************************/
if(fre<1 || fre >1000)
return 1;
num=DOUBLE_TIME*fre/1000.0;
if(num>(int)num+0.5) //四舍五入
double_time = (int)num+1;
else
double_time = (int)num;
num=CONTINUE_TIME*fre/1000.0;
if(num>(int)num+0.5) //四舍五入
continue_time = (int)num+1;
else
continue_time = (int)num;
return 0;
}
/**
* @name Key_Scan
* @brief 扫描按键,判断按键事件并调用按键回调函数
* @note 不同平台移植只需要更改前面部分代码,要保证按键没按下时
* 读取的值为1,按下为0。
* 这是非阻塞式按键扫描函数,不要阻塞使用
*/
void Key_Scan()
{
unsigned short key_val=0xffff, read_data,i;
static unsigned char key=10;//暂时保存是哪个按键按下的
/****移植需要修改的地方****/
key_val =(key_val<<1) + (!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0));//取反是因为这个按键接的3.3v,为了和另外两个接地的按键数据保持一致
key_val = (key_val<<1) + GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_3);
key_val = (key_val<<1) + GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_4);
/*************************/
//以下是三行代码按键//
read_data = key_val^0xffff;//异或操作,获取按键位置
trg = read_data & (read_data ^ cont);//保存按键信息,且每次按下只保留一次,即使下次按键不松也清零
cont = read_data;//按键不松,这一直有值,且对应了按键位置
if(key_time==0) //前面没发生过按键按下
{
for (i=0;i<KEY_NUM;i++) //循环遍历每个按键的信息
{
if(trg&(0x01<<i)) //对应位置的按键按下了
{
key_time=1;//让计时变量开始计时
key=i;
break; //仅处理一个按键
}
}
}
else //前面发生过按键按下,现在判断是什么事件
{
key_time++; //计数加一
if(key_time<=double_time) //没超过双击判断时间
{
if(trg&(0x0001<<key)) //检测到相同按键再次按下
{
Key_CallBack(key*3+1);//判断为双击,并调用回调函数,传入相关按键事件
key_time=0; //结束计数
key=0;//清除暂时的按键信息
}
}
else if(key_time>double_time && ((cont&(0x0001<<key))==0)) //超过双击判断时间,并且没有连续按
{
Key_CallBack(key*3);//判断为单击,并调用回调函数,传入相关按键事件
key_time=0; //结束计数
key=0;//清除暂时的按键信息
}
else //其他情况可能是长按
{
if((cont&(0x0001<<key))==0) //如果松掉了表示不是长按
{
Key_CallBack(key*3);//还是判断为单击,并调用回调函数,传入相关按键事件
key_time=0; //结束计数
key=0;//清除暂时的按键信息
}
else if(key_time>continue_time) //超过一定时间表示为长按
{
Key_CallBack(key*3+2);//判断为长按,并调用回调函数,传入相关按键事件
key_time=0; //结束计数
key=0;//清除暂时的按键信息
}
}
}
}
/**
* @name Key_CallBack
* @breif 发生按键事件时调用的函数
* @param key:按键事件标志,规则为:按键*3+n
* 单击n=0,双击n=1,长按n=2。按键从0开始
* 为第一个
*/
void Key_CallBack(int key)
{
printf("%d\r\n",key);
}
移植事项:
- 首先根据自己的需求修改头文件的内容,由于过于简单,在此不详述
- 本程序是在stm32f103zet6单片机上实现的,移植时需要参考自己单片机的驱动与按键引脚修改Key_Init()和Key_Scan()前面与硬件有关的部分。
- Key_Init()的参数fre是按键扫描频率,也就是一秒扫描按键的次数,不能大于1000或者小于1。这个参数不用太精确,只是用来告诉按键程序现实的时间与程序执行次数的关系。这是为了判断双击和长按所必要的。如果觉得实现效果不理想可以适当加或者减fre的值。