多个按键识别出短按、双击、长按的一种简单方案

  • Post author:
  • Post category:其他


几个普通按键实现复杂的功能在现在的嵌入式系统中应用非常常见,一方面是为了考虑到硬件平台的大小,毕竟现在的东西越来越小巧(功能相同的情况下越小越显得高逼格);另一方面是按键太多会影响平台的美观性和适用性,毕竟要是一个平台上面有一大堆按键会加大工作人员学习难度,在这种情况下很容易发生误操作。

最近在忙一个网上接的小项目,做着做着客户提了许多新功能,但麻烦的事当初画板子的时候没有考虑到这些(是我的原因,在画板之前没有考虑周全)。项目原来只需要控制继电器按照设定的时间通断,然后提示本次是否有错误,想着挺简单的就接了。结果做着做着客户居然还要能保存和读取测量错误的数据,顿时给我懵逼了。

数据存储的话就需要用到存储器,还好芯片有自带的EEPROM,可以把数据存在这里面。麻烦的是如何控制把存进去的错误数据读出来。按照最初的要求我只设置了两个拨码开关负责通道的通断,普通按键只有三个负责修改时间和循环次数。普通按键三个分别实现:当前位选数值+1、当前位选数值-1、当前位选向左移位。至于调整的那个通道就看拨码开关哪个是打开的状态(这里双通道都打开不能识别反而不执行任何动作)。

把更新后的要求仔细分析了一下发现如果一个按键一个功能的话需要用到14个普通按键(具体功能如下):

按键一 通道一时间参数修改;
按键二 通道一循环次数参数修改;
按键三 读取通道一当前次数存储的的错误数据;
按键四 读取通道一所有存储的的错误数据;
按键五 清除通道一存储的数据;
按键六 通道二时间参数修改;
按键七 通道二循环次数参数修改;
按键八 读取通道二当前次数存储的的错误数据;
按键九 读取通道二所有存储的的错误数据;
按键十 清除通道二存储的数据;
按键十一 数码管位选移位;
按键十二 数码管位选数值+1;
按键十三 数码管位选数值-1;
按键十四 已修改的参数保存;

很明显不可能用这么多的按键,14个按键看着都头晕,只得另寻他法。在网上看了一篇如何实现按键单击、双击、长按的帖子上面还有详细的程序和注释(链接如下:

单片机按键识别篇—单击—双击—-长按 – 一切都会好 – 博客园

)。顿时发现我的这14个功能都不是是个事,一个按键三个状态,三个按键就可以组合成27个状态,这对于我这点需求不是轻而易举。

至于如何实现单按键识别三种状态网上有很多,基本都是差不多的原理,我这里就不详细说明了。我这里详细说明一下如何识别三个按键识别不同状态。

现在这里定义按键管脚以及触发时的相关参数:

/* 定义拨码开关的硬件接口  增加按键或者删除按键 需要对应增加或者删除  */
#define ADDR1 GPIO_Pin_2
#define ADDR2 GPIO_Pin_3

/* 定义按键的硬件接口  增加按键或者删除按键 需要对应增加或者删除  */
#define GPIO_PIN_K1 GPIO_Pin_4
#define GPIO_PIN_K2 GPIO_Pin_5
#define GPIO_PIN_K3 GPIO_Pin_6

// 拨码开关值   增加按键或者删除按键 需要对应增加或者删除
#define Switch_0    	 0x00 //无按键按下
#define Switch_1         0x01 // 第一个按键打开
#define Switch_2         0x02 // 第二个按键打开

//普通按键状态
#define KEY_STATE_A         0xA0 // 无
#define KEY_STATE_B         0xB0 // 软件消抖
#define KEY_STATE_C         0xC0 // 单击
#define KEY_STATE_D         0xD0 // 双击
#define KEY_STATE_E         0xE0 // 长按
#define KEY_STATE_F         0xF0 // 等待按键释放

//用于识别按键的状态 用它与按键的状态相与即可区分
#define STATE         0xF0 // 等待按键释放
#define VALUE         0x0F // 存放按键值

//按键时间调整
#define SINGLE_KEY_TIME     2      // 消抖时间为 SINGLE_KEY_TIME*10MS
#define DOUBLE_KEY_TIME     50     
/* 双击间隔时间 DOUBLE_KEY_TIME*10MS
改变这个数值可以增加按键的灵敏度,数值越小灵敏度越高  
定时器定时时间要大于消抖时间 ,小于长按按键的时间 */
#define LONG_KEY_TIME       50    // 长按时间 LONG_KEY_TIME*10MS 


// 按键值   增加按键或者删除按键 需要对应增加或者删除
#define KEY_NO_DOWN    0x00 //无按键按下
#define KEY_K1         0x01 // 第一个按键按下
#define KEY_K2         0x02 // 第二个按键按下
#define KEY_K3         0x03 // 第三个按键按下

//按键返回值  增加按键或者删除按键 需要对应增加或者删除
#define N_KEY     0                       // no click
#define Key1_S    (KEY_STATE_C | KEY_K1 ) // single click
#define Key1_D    (KEY_STATE_D | KEY_K1 ) // double press
#define Key1_L    (KEY_STATE_E | KEY_K1 ) // long press

#define Key2_S    (KEY_STATE_C | KEY_K2 ) // single click
#define Key2_D    (KEY_STATE_D | KEY_K2 ) // double press
#define Key2_L    (KEY_STATE_E | KEY_K2 ) // long press

#define Key3_S    (KEY_STATE_C | KEY_K3 ) // single click
#define Key3_D    (KEY_STATE_D | KEY_K3 ) // double press
#define Key3_L    (KEY_STATE_E | KEY_K3 ) // long press

按键管脚初始化:

/*
*********************************************************************************************************
* 名    称:Key_Init(void)
* 功    能:初始化按键
* 入口参数:无
* 出口参数:无
* 调用方法:需要在调用处理函数之前进行 
           增加按键或者删除按键 需要对应增加或者删除
*********************************************************************************************************
*/
void Key_Init(void)
{
    /*拨码开关检测引脚*/
	GPIOB_ModeCfg(ADDR1, GPIO_ModeIN_PU);
	GPIOB_ModeCfg(ADDR2, GPIO_ModeIN_PU);
	
	/*按键检测引脚*/
	GPIOB_ModeCfg(GPIO_PIN_K1, GPIO_ModeIN_PU);
	GPIOB_ModeCfg(GPIO_PIN_K2, GPIO_ModeIN_PU);
	GPIOB_ModeCfg(GPIO_PIN_K3, GPIO_ModeIN_PU);
}

取得拨码开关状态:

/****************************************************************************
* 名    称: GetKEY(void)
* 功    能:取得拨码开关	GPIO的值
* 入口参数:无
* 出口参数:返回拨码值
* 说    明:如果读到低电平则说明有拨码开关打开,不同的CPU只要更改if里面的读取方式即可
****************************************************************************/
uint8_t Get_Addr_State(void)
{
    if(GPIOB_ReadPortPin(ADDR1) == 0) /* 读取拨码开关 1 的状态 */
    {
		return Switch_1;//拨码开关1打开  返回1
    }
    if(GPIOB_ReadPortPin(ADDR2) == 0) /* 读取拨码开关 2 的状态 */
    {
		return Switch_2;//拨码开关2打开  返回2
    }
    return Switch_0;
}

取得普通按键的状态:

/****************************************************************************
* 名    称: GetKEY(void)
* 功    能:取得按键GPIO的值
* 入口参数:无
* 出口参数:返回按键值
* 说    明:如果读到低电平则说明有按键按下,不同的CPU只要更改if里面的读取方式即可
* 调用方法:key_driver 函数调用 用于判断是否有按键按下 
            增加按键或者删除按键 需要对应增加或者删除
****************************************************************************/
uint8_t Get_KEY_State(void)
{
    if(GPIOB_ReadPortPin(GPIO_PIN_K1) == 0) /* 读取按键 2 的状态 */
    {
		return KEY_K1;//按键被按下  返回1
    }
    if(GPIOB_ReadPortPin(GPIO_PIN_K2) == 0) /* 读取按键 2 的状态 */
    {
		return KEY_K2;//按键被按下  返回2
    }
    if(GPIOB_ReadPortPin(GPIO_PIN_K3) == 0) /* 读取按键 2 的状态 */
    {
		return KEY_K3;//按键被按下  返回3
    }
	
    return KEY_NO_DOWN;
}

普通按键状态机检测按键:该函数是对 key_driver 函数的调用 用于实现按键的双击和多击

/****************************************************************************
* 名    称:char key_read(void)
* 功    能:状态机检测按键
* 入口参数:无
* 出口参数:返回按键值
* 调用方法:该函数是对 key_driver 函数的调用 用于实现按键的双击和多击
****************************************************************************/
unsigned char key_read(void)
{
	static UINT8 KeyState = 0;      	// 初始化状态机的状态
    static UINT8 LaetgKeyValue = 0;     //上一次扫描的键值
    UINT8 KeyValue,KeyReturnValue;		//当前按键键值

    KeyReturnValue = N_KEY;                       // 清除 返回按键值

    KeyValue = key_driver(); // 读取当前键值的状态

    if( KeyValue ) //保存按键的的键值只在非 0 的状态才改变其按键值
    {
        LaetgKeyValue = KeyValue & VALUE; //取出按键值 去掉按键的状态编号
    }

    switch (KeyState)
    {
    case KEY_STATE_A:                       // 按键状态0:判断有无按键按下
        if ( ( KeyValue & STATE ) == KEY_STATE_C ) // 判断是否有单击按键
        {
            KeyTime = 0;                    // 清零时间间隔计数
            KeyState = KEY_STATE_D;        // 然后进入 按键状态D
        }
        else   // 对于无键、长键,返回原事件
        {
            KeyReturnValue = KeyValue;
        }
        break;

    case KEY_STATE_D:                       // 按键状态D:双击状态
        if ( ( KeyValue & STATE ) == KEY_STATE_C )	
        {
            KeyReturnValue = (LaetgKeyValue | KEY_STATE_D ); // 返回 有效按键值:双击
            KeyState = KEY_STATE_F;  //  然后进入 按键状态F
        }
        else 
		{
			if ( ++KeyTime >= DOUBLE_KEY_TIME )   //DOUBLE_KEY_TIME*10ms时间内没有再次单击 视为单击
			{
				KeyReturnValue = (LaetgKeyValue | KEY_STATE_C ); //返回 有效按键值:单击
				KeyState = KEY_STATE_A;  //  然后进入 按键状态F
			}
		}
		
        break;

    case KEY_STATE_F:                         // 等待按键释放
        if ( KeyValue == 0 )
        {
            KeyState = KEY_STATE_A;          // 按键释放后,进入 按键状态0 ,进行下一次按键的判定
        }
        break;

    default: // 特殊情况:key_state是其他值得情况,清零key_state。这种情况一般出现在 没有初始化key_state,第一次执行这个函数的时候
        KeyState = KEY_STATE_A;
        break;
    }

    return KeyReturnValue;                          // 返回 按键值
}

状态机检测按键: 实现按键的单击长按和没有按键按下的检测

/****************************************************************************
* 名    称:unsigned char key_driver(void)
* 功    能:状态机检测按键
* 入口参数:无
* 出口参数:返回按键值
* 调用方法:该函数实现按键的单击长按和没有按键按下的检测 
            添加或者删除独立按键不需要更改此处
****************************************************************************/
unsigned char key_driver(void)
{
    static UINT8 key_state = 0;         // 按键状态变量
    static UINT8 Key_LaetgValue = 0;//上一次扫描的键值
    UINT8 Key_LaetPress,key_press, key_return;//当前按键键值

    key_return = N_KEY;                         // 清除 返回按键值

    Key_LaetPress = key_press = Get_KEY_State();                      // 读取当前键值

    if( Key_LaetPress ) //保存按键的的键值只在非 0 的状态才改变其按键值
    {
        Key_LaetgValue = key_press;
    }

    switch (key_state)
    {
    case KEY_STATE_A:                       // 按键状态A:判断有无按键按下
        if ( ( key_press != 0 ) & (key_press == Key_LaetPress) )                     // 有按键按下
        {
            key_time = 0;                   // 清零时间间隔计数
            key_state = KEY_STATE_B;        // 然后进入 按键状态B
        }
        break;

    case KEY_STATE_B:                       // 按键状态B:软件消抖(确定按键是否有效,而不是误触)。按键有效的定义:按键持续按下超过设定的消抖时间。
        if (key_press == Key_LaetPress)
        {
            key_time++;                     // 一次10ms
            if(key_time>=SINGLE_KEY_TIME)   // 消抖时间为:SINGLE_KEY_TIME*10ms;
            {
                key_state = KEY_STATE_C;    // 如果按键时间超过 消抖时间,即判定为按下的按键有效。按键有效包括两种:单击或者长按,进入 按键状态2, 继续判定到底是那种有效按键
            }
        }
        else
        {
            key_state = KEY_STATE_A;       // 如果按键时间没有超过,判定为误触,按键无效,返回 按键状态0,继续等待按键
        }
        break;

    case KEY_STATE_C:                       // 按键状态C:判定按键有效的种类:是单击,还是长按
        if( key_press == 0 )                        // 如果按键在 设定的长按时间 内释放,则判定为单击
        {
            key_return = (Key_LaetgValue | KEY_STATE_C ); // 返回 有效按键值:单击
            key_state = KEY_STATE_A;       // 返回 按键状态A,继续等待按键
        }
        else
        {
            key_time++;
            if(key_time >= LONG_KEY_TIME)   // 如果按键时间超过 设定的长按时间(LONG_KEY_TIME*10ms=200*10ms=2000ms), 则判定为 长按
            {
                key_return = (Key_LaetgValue | KEY_STATE_E ); // 返回 有效键值值:长按
                key_state = KEY_STATE_F;    // 去等待释放状态
            }
        }
        break;

    case KEY_STATE_F:                         // 按键状态C:按键释放
        if ( key_press == 0 )
        {
            key_state = KEY_STATE_A;          // 按键释放后,进入 按键状态A ,进行下一次按键的判定
        }
        break;

    default: // 特殊情况:key_state是其他值得情况,清零key_state。这种情况一般出现在 没有初始化key_state,第一次执行这个函数的时候
        key_state = KEY_STATE_A;
        break;
    }

    return key_return;                          // 返回 按键值
}

然后在定时器中断中读(10ms)取按键键值:

//定时器0中断函数
void TMR0_IRQHandler( void )        // TMR0 定时中断 1ms
{
	//key_count:定时器中断——按键计数(10*1ms)
	//Timer_count:定时器中断——时间计数(1000*1)
	static UINT8 key_count;
	static UINT16 N_Key_Time;
	static UINT16 Glisten_Time;
	if( TMR0_GetITFlag( TMR0_3_IT_CYC_END ) )
	{
		TMR0_ClearITFlag( TMR0_3_IT_CYC_END );      // 清除中断标志
		/*用户代码区*/
		//按键功能扫描 	周期:10*1ms
		key_count++;
		if(key_count==10)
		{
			Key_State  = key_read();
			key_count=0;
		}
	}				
}

这只是最低级的按键多状态检测,这个检测存在一个问题:按键检测会一直在中断中执行不管按键是否被调用和读取。在程序里面就要以按键键值变化为主导而不能在程序里面读取按键状态。



版权声明:本文为wfm700305原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。