单片机系统中实用的按键驱动(STM32、51都适用)

68 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

在我们嵌入式开发中,产品上往往少不了按键,一个好的按键驱动可以满足不同场合的客户需求,这里我分享几个实用的按键驱动,希望抛砖引玉,大家多多指出不足以及提出改进办法,或者有更好的方案希望也能分享分享。

不废话,直接上源码:

1、自己使用的按键驱动

不同平台需要做相应的修改,原理就是需要单独使用一个定时器进行时间计算,对按键的状态按照时间进行区分。

该驱动最初是从国外一个产品的 SDK 中修改而来的,当时也是花了一些时间进行测试,优化,继续测试。

然后移植到STM32 系列上,进行了一些简单的优化,但是即便这样,因为工作中产品的功能按钮需求并没有很复杂,所以驱动中只实现了:

短按

规定时间的长按不放

规定时间的长按松开

连按暂时还没有时间,所以开放出源码,也是希望大家可以进行优化修改。

最近在STM32L系列上也在使用,所以我放出的源码是基于 STM32L 系列的,但是一般做嵌入式开发的,基本上修改都没问题。

1.1 驱动源码

用最近的一个作为demo

/*
2019/5/21 按键程序移植成功,以后可以使用此按键,需要研究一下
和以前单片机项目按钮方式类似  
by  qzh
2019/8/30  
确定了第三行,第一个必须是7,才能按下到时间自动触发
by  qzh
*/
#include "mod_button.h"

//GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
void io_getDigital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin,uint8 *pu8Value)
{  
  *pu8Value = HAL_GPIO_ReadPin(GPIOx,GPIO_Pin);
}

void time_setTimerCount(TIMER_TYPE *pu8timer,uint32 u32timeToCount)  
{
  // __HAL_TIM_SET_COUNTER
  HAL_TIM_Base_Start_IT(&htim21);
  // HAL_TIM_Base_Stop_IT
  // TIM_Cmd(TIM4, ENABLE);
  if(pu8timer->on == 0)
    pu8timer->on = 1;
  if(pu8timer->on == 1)
    //IntNum = 0;
    pu8timer->timeInit = Timer4_count;
  //pu8timer->timeInit = IntNum;
  pu8timer->timeOut = 0;
  pu8timer->timeToCount = u32timeToCount;
}

RETURN_TYPE time_getTimeOut(TIMER_TYPE *pu8timer)
{
  uint32 Temp_Val;
  if(Timer4_count > pu8timer->timeInit)
    Temp_Val = Timer4_count - pu8timer->timeInit;
  else
    Temp_Val = (0xFFFFFFFF-pu8timer->timeInit)+Timer4_count;
  if(Temp_Val >= pu8timer->timeToCount)
  {
    pu8timer->timeOut = 1;
    pu8timer->on = 0;
    pu8timer->timeToCount = 0;
    pu8timer->timeInit = 0;
  }  
  else 
    pu8timer->timeOut = 0;
  return (pu8timer->timeOut == 1)?TIME_OUT:OK;
}

BTN_STATE btn_getState(BTN_STRUCT *pBtn)
{
const uint8 transition_table[8][4]={  0,  1,  0,  1,
                    5,  2,  5,  1,
                    7,  2,  5,  3,
                    5,  4,  5,  4,
                    5,  4,  5,  4,
                    6,  1,  0,  1,
                    6,  1,  7,  1,
                    0,  1,  0,  1 };
  
  //register uint8 u8Input;
  uint8 u8Input;
  // Get button state
  io_getDigital(pBtn->u8Pin,pBtn->GPIO_Pin ,&u8Input);
  u8Input = (u8Input == pBtn->u8ActiveState)?1:0;
  
  // Get timeout state
  u8Input |= ((time_getTimeOut(&(pBtn->tTimer))==TIME_OUT)?2:0);

  // Get new state
  pBtn->u8State = transition_table[pBtn->u8State][u8Input]; // we want only the state, not action

  // Perform action 
  switch (pBtn->u8State)
  {
    case 1:
      time_setTimerCount(&(pBtn->tTimer), pBtn->u16TimeOutON);
      break;
    case 5:
      time_setTimerCount(&(pBtn->tTimer), pBtn->u16TimeOutOFF);
      break;
  }
  // return pBtn->u8State;
  //待测试
  return (BTN_STATE)pBtn->u8State;  
} 

//下面是mod_button.h

#ifndef _MOD_BUTTON_H_INCLUDED
#define _MOD_BUTTON_H_INCLUDED

#include "main.h"
#include "Datadef.h"
#include "tim.h"
/*
            Timeout ON
                _______|_____
 P             |             |      Timeout OFF
 R  ___________|             |________|____
    ^      ^  ^    ^  ^  ^   ^    ^
 S    0     1  2    3  4  5   6    7

P - pressed, R - released, S - BTN_STATE
*/

/*
°´Å¥Ïà¹Ø  KEY1  learn  PB5  KEY2  CLEAR  PB6
*/
#define BTN_ACTIVE    0         //when pressed, switch to GND


typedef struct
{
  // Public 
  //uint8    u8Pin;      // e.g. ADIO0
  //uint16    u8Pin;      // e.g. ADIO0
  GPIO_TypeDef * u8Pin;
  uint16_t    GPIO_Pin;
  uint8    u8ActiveState;  // button is pressed if (io_getDigital(u8Button)==bActiveState)
  uint16    u16TimeOutON;  // time the button has to be pressed to be recognized as pressed
  uint16    u16TimeOutOFF;  // time the button has to be pressed to be recognized as released
  
  // Private
  TIMER_TYPE  tTimer;
   uint8    u8State;

} BTN_STRUCT;

typedef enum 
{
  BTN_IDLE = 0,
  BTN_EDGE1,
  BTN_TRIGGERED,
  BTN_PRESSED,  //< most important
  BTN_PRESS_HOLD,
  BTN_EDGE2,
  BTN_RELEASE_HOLD,
  BTN_RELEASED  
} BTN_STATE;

extern u16 Timer4_count;

BTN_STATE btn_getState(BTN_STRUCT *pBtn);
void time_setTimerCount(TIMER_TYPE *pu8timer,uint32 u32timeToCount); 
void io_getDigital(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin,uint8 *pu8Value);
RETURN_TYPE time_getTimeOut(TIMER_TYPE *pu8timer);


#endif //_MOD_BUTTON_H_INCLUDED

1.2 使用方法

该驱动的使用方式需要根据自己的实际需求进行结构体的定义,可以设置为短按多少ms,长按多少ms,都是根据自己的定义

除了定义按键的时间,在判断状态的额时候,有按住,弹起等状态判断。

注意使用的时候,如果长时间的按钮操作需要状态为 BTN_EDGE2 ,即长按松开 才会执行,在执行完语句最后需要包含比此次短的 BTN_EDGE2 操作,如下图:

//先定义一个按键的结构体类型,
BTN_STRUCT  K1_BUTTON_150mS ={K1_GPIO_Port,  
              K1_Pin,
            BTN_ACTIVE,                        // Active state
            150,                              // Time the button has to be in 'Active state' to be recognized as pressed
            100 };  
BTN_STRUCT  K1_BUTTON_2S = { K1_GPIO_Port,
            K1_Pin,
            BTN_ACTIVE,                        // Active state
            2000,                              // Time the button has to be in 'Active state' to be recognized as pressed
            100   } ;

//然后在主函数循环中调用不同的状态
if(btn_getState(&K1_BUTTON_150mS) == BTN_EDGE2){...}//BTN_EDGE2按住松开
if(btn_getState(&K1_BUTTON_2S) == BTN_PRESSED){...} //BTN_PRESSED为按住不松开状态
//注意使用的时候,如果长时间的按钮操作需要状态为 BTN_EDGE2 ,即长按松开 才会执行,在执行完语句最后需要包含比此次短的 BTN_EDGE2 操作:

if((btn_getState(&CLEAR_BUTTON_2S)==BTN_EDGE2){
  ...
  while(btn_getState(&CLEAR_BUTTON_150mS));// 防止长时间按住松开会触发短时间的松开操作
}

一般的情况下,上面代码中用到的按键事件足够应付,所以 BTN_STATE 中其他的按键状态我也没有花太多事件去测试,如果有谁能够把其他状态测试说清楚,还希望能够告知一声,

此按键程序最初原型是国外一家公司提供的SDK包里面的 Demo,然后经过自己的多次测试与修改才变成自己的最常用的驱动,当然他还可以更加的优化,希望大家多多指导。

2、另一种按键驱动

当初在做按键的时候,想着除了上面的是否有其他简单的通俗易懂的按键驱动,然后自己在网上各种寻找,反正不可能按键直接检测IO,然后防抖做一个短延时,最终这种还是逃不过得开定时器,然后还是做了一个测试可用的。

对于第二种方式,就更加好理解,整体的逻辑为:

IO 口设置为上升沿和下降沿都能够检测,是为了检测弹起和按下的瞬间;

然后还是需要设置一个定时器,当有中断发生的时候,开启定时器计时,然后根据第二次中断,其实就是手松开的时候,判断一下这个时候定时器计时的时间,然后认定刚才的操作是短按还是长按。

在.h文件中定义枚举类型,可以定义为短按,长按,中等时间按下。

对于不同的按下时间是需要自己写在驱动中的:

第一个延时 <20 ms 是防抖动,然后不同的时间定义不同的按键按下时间。

2.1 驱动源码

//开启按键IO的外部中断,为上升下降沿都能触发,这样是检测按下,弹起之间的时间差

EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
/*
有一个问题,就是按下以后,长按需要下一次进入外部中断才能执行
*/
void EXTI9_5_IRQHandler(void)
{
  
  if( EXTI_GetITStatus(EXTI_Line7)!= RESET)
  {
    if( KEY == 0 )
    {        
      Timer4_count = 0;
      readkey1 =0;
    }
    else readkey1=1;
    }
  EXTI_ClearITPendingBit(EXTI_Line7);  
}

//外部中断实施按键操作
KEY_TIME Press_Key1(void)
{
  KEY_TIME i;
    if( readkey1 == 1 )
    {
        if( Timer4_count < 20 )
        {
            Timer4_count = 0;
            readkey1 = 0;
            i = NOT;
        }
        else if(( Timer4_count >= 80) && (Timer4_count < 2000 ))
        {
            Timer4_count = 0;
            readkey1 = 0;
            i = Short;
        }
        else if(( Timer4_count >= 2000 ) && (Timer4_count < 5000 ))
        {
            Timer4_count = 0;
            readkey1 = 0;
            i = Medium;
        }
    else if( Timer4_count >= 5000 )
        {
            Timer4_count = 0;
            readkey1 = 0;
            i = Long;
        }
    }
    return i;
}

//在.h文件中定义枚举类型
typedef enum
{
    NOT   = 0,
  Short =2,
  Medium=3 ,
    Long =4 ,  
}KEY_TIME;
/*
好像记得当时在上面这个枚举类型时候,Short 定义为 1 是会有问题的,当时也不确定是什么问题,改成 2 测试就OK了,也没花时间研究
*/
KEY_TIME Press_Key1(void);

2.2 使用方法

该驱动的使用方式比较简单,直接如下图所示:

if(Press_Key1() == Short){
      ...  
}
else if(Press_Key1() == Medium);

当时这种按键也用了一会,但是好像是与点小问题,但是只使用 short 和 Medium 是可以的,具体什么问题,因为实际用不到那么多,所以也没继续研究下去,这个方法是在网上参照的例子拿过来使用的。