前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试加分项|一个优秀的按键驱动框架

面试加分项|一个优秀的按键驱动框架

作者头像
用户8913398
发布2021-08-13 13:07:47
4580
发布2021-08-13 13:07:47
举报
文章被收录于专栏:嵌入式实验基地

前言

按键是嵌入式产品中不可或缺的一部分,但往往受制于结构尺寸等因素,按键数量有限,如何利用有限的按键实现更多的骚操作,本文介绍一种优雅的按键实现方法,纯c语言实现,只需要与底层接口对接便可以轻松移植到嵌入式平台,实现单击、连击、短按、长按功能。

实现效果

原理及代码接口分析

源码来自: https://github.com/jiejieTop

Button_drive是一个小巧的按键驱动,支持单击、双击、长按、连续触发等(后续可以在按键控制块中添加触发事件),写按键驱动的目的是想要将用户按键逻辑与按键处理事件分离,用户无需处理复杂麻烦的逻辑事件。

创建按键

用过操作系统的小伙伴肯定对这操作很熟悉,创建任务、创建队列...

代码语言:javascript
复制
/************************************************************
  * @brief   按键创建
 * @param   name : 按键名称
 * @param   btn : 按键结构体
  * @param   read_btn_level : 按键电平读取函数,需要用户自己实现返回uint8_t类型的电平
  * @param   btn_trigger_level : 按键触发电平
  * @return  NULL
  * @author  jiejie
  * @github  https://github.com/jiejieTop
  * @date    2018-xx-xx
  * @version v1.0
  * @note    NULL
  ***********************************************************/
void Button_Create(const char *name,
                  Button_t *btn, 
                  uint8_t(*read_btn_level)(void),
                  uint8_t btn_trigger_level)
{
  if( btn == NULL)
  {
    PRINT_ERR("struct button is null!");
    ASSERT(ASSERT_ERR);
  }
  
  memset(btn, 0, sizeof(struct button));  //清除结构体信息,建议用户在之前清除
 
  StrnCopy(btn->Name, name, BTN_NAME_MAX); /* 创建按键名称 */
  
  
  btn->Button_State = NONE_TRIGGER;           //按键状态
  btn->Button_Last_State = NONE_TRIGGER;      //按键上一次状态
  btn->Button_Trigger_Event = NONE_TRIGGER;   //按键触发事件
  btn->Read_Button_Level = read_btn_level;    //按键读电平函数
  btn->Button_Trigger_Level = btn_trigger_level;  //按键触发电平
  btn->Button_Last_Level = btn->Read_Button_Level(); //按键当前电平
  btn->Debounce_Time = 0;
  
  PRINT_DEBUG("button create success!");
  
  Add_Button(btn);          //创建的时候添加到单链表中
  
  Print_Btn_Info(btn);     //打印信息
 
}
代码语言:javascript
复制
 Button_Create("Button1",
              &Button1, 
              Read_KEY1_Level, 
              KEY_ON);

我们以一个实例来说明如何创建按键,第1个参数是按键的名称,类似于rtos中的任务名称,命名顾名思义即可,但是要注意长度,默认是32字节,可以自定义

第2个参数是关于按键的一些结构体,成员还是非常多的,函数指针、结构体位域、结构体嵌套,知识点还是很多的,小伙伴们可以顺便学习学习写法,学了c语言不知道怎么用,这就是一个很好的例子...关于按键的触发电平、消抖时间、回调函数等都在此结构体中定义,整洁优雅

第3个参数也就是跟我们底层息息相关的了,获取按键IO状态函数,例如:

代码语言:javascript
复制
uint8_t Read_KEY1_Level(void)
{
 return HAL_GPIO_ReadPin(Usr_Key_GPIO_Port,Usr_Key_Pin);
}

第4个参数是我们触发电平的状态,例如下面的按键连接,K1、K2、K3是低电平有效,那此时KEY_ON为低电平,相反,Key_UP,是高电平有效:

按键触发事件与回调函数映射链接

此函数主要是用于把当前按键的状态与对应要执行的回调函数联系在一起,类似于HAL库中的串口回调、定时器回调等一大堆回调函数

代码语言:javascript
复制
/************************************************************
  * @brief   按键触发事件与回调函数映射链接起来
 * @param   btn : 按键结构体
 * @param   btn_event : 按键触发事件
  * @param   btn_callback : 按键触发之后的回调处理函数。需要用户实现
  * @return  NULL
  * @author  jiejie
  * @github  https://github.com/jiejieTop
  * @date    2018-xx-xx
  * @version v1.0
  ***********************************************************/
void Button_Attach(Button_t *btn,Button_Event btn_event,Button_CallBack btn_callback)
{
  if( btn == NULL)
  {
    PRINT_ERR("struct button is null!");
    //ASSERT(ASSERT_ERR);       //断言
  }
  
  if(BUTTON_ALL_RIGGER == btn_event)
  {
    for(uint8_t i = 0 ; i < number_of_event-1 ; i++)
      btn->CallBack_Function[i] = btn_callback;    //按键事件触发的回调函数,用于处理按键事件
  }
  else
  {
    btn->CallBack_Function[btn_event] = btn_callback; //按键事件触发的回调函数,用于处理按键事件
  }
}
代码语言:javascript
复制
Button_Attach(&Button1,BUTTON_DOWM,Btn1_Dowm_CallBack);                       //单击
  Button_Attach(&Button1,BUTTON_DOUBLE,Btn1_Double_CallBack);                   //双击
  Button_Attach(&Button1,BUTTON_CONTINUOS,Btn1_Continuos_CallBack);             //连按  
  Button_Attach(&Button1,BUTTON_CONTINUOS_FREE,Btn1_ContinuosFree_CallBack);    //连按释放  
  Button_Attach(&Button1,BUTTON_LONG,Btn1_Long_CallBack);                       //长按

依然是以实例的方式来看,第1个参数是上面注册的按键,不用多说

第2个参数是按键触发的方式,单击、双击、连击等等,根据触发事件链接对应的回调函数

代码语言:javascript
复制
typedef enum {
  BUTTON_DOWM = 0,
  BUTTON_UP,
  BUTTON_DOUBLE,
  BUTTON_LONG,
  BUTTON_LONG_FREE,
  BUTTON_CONTINUOS,
  BUTTON_CONTINUOS_FREE,
  BUTTON_ALL_RIGGER,
  number_of_event, /* 触发回调的事件 */
  NONE_TRIGGER
}Button_Event;

第3个参数是对应的回调函数

代码语言:javascript
复制

/* USER CODE BEGIN 0 */
void Btn1_Dowm_CallBack(void *btn)
{
  PRINT_INFO("Button1 单击!");
}

void Btn1_Double_CallBack(void *btn)
{
  PRINT_INFO("Button1 双击!");
}

void Btn1_Long_CallBack(void *btn)
{
  PRINT_INFO("Button1 长按!");
}

void Btn1_Continuos_CallBack(void *btn)
{
  PRINT_INFO("Button1 连按!");
}
void Btn1_ContinuosFree_CallBack(void *btn)
{
  PRINT_INFO("Button1 连按释放!");
}


void Btn2_Dowm_CallBack(void *btn)
{
  PRINT_INFO("Button2 单击!");
}

void Btn2_Double_CallBack(void *btn)
{
  PRINT_INFO("Button2 双击!");
}

void Btn2_Long_CallBack(void *btn)
{
  PRINT_INFO("Button2 长按!");
}

void Btn2_Continuos_CallBack(void *btn)
{
  PRINT_INFO("Button2 连按!");
}
void Btn2_ContinuosFree_CallBack(void *btn)
{
  PRINT_INFO("Button2 连按释放!");
}

删除按键

代码语言:javascript
复制
/************************************************************
  * @brief   删除一个已经创建的按键
 * @param   NULL
  * @return  NULL
  * @author  jiejie
  * @github  https://github.com/jiejieTop
  * @date    2018-xx-xx
  * @version v1.0
  * @note    NULL
  ***********************************************************/
void Button_Delete(Button_t *btn)
{
  struct button** curr;
  for(curr = &Head_Button; *curr;) 
  {
    struct button* entry = *curr;
    if (entry == btn) 
    {
      *curr = entry->Next;
    } 
    else
    {
      curr = &entry->Next;
    }
  }
}

按键处理

按键处理进程很好的运用了状态机的思想,关于状态机在按键中的应用请参见:状态机实现按键

代码语言:javascript
复制
/************************************************************
  * @brief   按键周期处理函数
  * @param   btn:处理的按键
  * @return  NULL
  * @author  jiejie
  * @github  https://github.com/jiejieTop
  * @date    2018-xx-xx
  * @version v1.0
  * @note    必须以一定周期调用此函数,建议周期为20~50ms
  ***********************************************************/
void Button_Cycle_Process(Button_t *btn)
{
  uint8_t current_level = (uint8_t)btn->Read_Button_Level();//获取当前按键电平
  
  if((current_level != btn->Button_Last_Level)&&(++(btn->Debounce_Time) >= BUTTON_DEBOUNCE_TIME)) //按键电平发生变化,消抖
  {
      btn->Button_Last_Level = current_level; //更新当前按键电平
      btn->Debounce_Time = 0;                 //确定了是按下
      
      //如果按键是没被按下的,改变按键状态为按下(首次按下/双击按下)
      if((btn->Button_State == NONE_TRIGGER)||(btn->Button_State == BUTTON_DOUBLE))
      {
        btn->Button_State = BUTTON_DOWM;
      }
      //释放按键
      else if(btn->Button_State == BUTTON_DOWM)
      {
        btn->Button_State = BUTTON_UP;
        TRIGGER_CB(BUTTON_UP);    // 触发释放
        PRINT_DEBUG("释放了按键");
      }
  }
  
  switch(btn->Button_State)
  {
    case BUTTON_DOWM :            // 按下状态
    {
      if(btn->Button_Last_Level == btn->Button_Trigger_Level) //按键按下
      {
        #if CONTINUOS_TRIGGER     //支持连续触发

        if(++(btn->Button_Cycle) >= BUTTON_CONTINUOS_CYCLE)
        {
          btn->Button_Cycle = 0;
          btn->Button_Trigger_Event = BUTTON_CONTINUOS; 
          TRIGGER_CB(BUTTON_CONTINUOS);    //连按
          PRINT_DEBUG("连按");
        }
        
        #else
        
        btn->Button_Trigger_Event = BUTTON_DOWM;
      
        if(++(btn->Long_Time) >= BUTTON_LONG_TIME)  //释放按键前更新触发事件为长按
        {
          #if LONG_FREE_TRIGGER
          
          btn->Button_Trigger_Event = BUTTON_LONG; 
          
          #else
          
          if(++(btn->Button_Cycle) >= BUTTON_LONG_CYCLE)    //连续触发长按的周期
          {
            btn->Button_Cycle = 0;
            btn->Button_Trigger_Event = BUTTON_LONG; 
            TRIGGER_CB(BUTTON_LONG);    //长按
          }
          #endif
          
          if(btn->Long_Time == 0xFF)  //更新时间溢出
          {
            btn->Long_Time = BUTTON_LONG_TIME;
          }
          PRINT_DEBUG("长按");
        }
          
        #endif
      }

      break;
    } 
    
    case BUTTON_UP :        // 弹起状态
    {
      if(btn->Button_Trigger_Event == BUTTON_DOWM)  //触发单击
      {
        if((btn->Timer_Count <= BUTTON_DOUBLE_TIME)&&(btn->Button_Last_State == BUTTON_DOUBLE)) // 双击
        {
          btn->Button_Trigger_Event = BUTTON_DOUBLE;
          TRIGGER_CB(BUTTON_DOUBLE);    
          PRINT_DEBUG("双击");
          btn->Button_State = NONE_TRIGGER;
          btn->Button_Last_State = NONE_TRIGGER;
        }
        else
        {
            btn->Timer_Count=0;
            btn->Long_Time = 0;   //检测长按失败,清0
          
          #if (SINGLE_AND_DOUBLE_TRIGGER == 0)
            TRIGGER_CB(BUTTON_DOWM);    //单击
          #endif
            btn->Button_State = BUTTON_DOUBLE;
            btn->Button_Last_State = BUTTON_DOUBLE;
          
        }
      }
      
      else if(btn->Button_Trigger_Event == BUTTON_LONG)
      {
        #if LONG_FREE_TRIGGER
          TRIGGER_CB(BUTTON_LONG);    //长按
        #else
          TRIGGER_CB(BUTTON_LONG_FREE);    //长按释放
        #endif
        btn->Long_Time = 0;
        btn->Button_State = NONE_TRIGGER;
        btn->Button_Last_State = BUTTON_LONG;
      } 
      
      #if CONTINUOS_TRIGGER
        else if(btn->Button_Trigger_Event == BUTTON_CONTINUOS)  //连按
        {
          btn->Long_Time = 0;
          TRIGGER_CB(BUTTON_CONTINUOS_FREE);    //连发释放
          btn->Button_State = NONE_TRIGGER;
          btn->Button_Last_State = BUTTON_CONTINUOS;
        } 
      #endif
      
      break;
    }
    
    case BUTTON_DOUBLE :
    {
      btn->Timer_Count++;     //时间记录 
      if(btn->Timer_Count>=BUTTON_DOUBLE_TIME)
      {
        btn->Button_State = NONE_TRIGGER;
        btn->Button_Last_State = NONE_TRIGGER;
      }
      #if SINGLE_AND_DOUBLE_TRIGGER
      
        if((btn->Timer_Count>=BUTTON_DOUBLE_TIME)&&(btn->Button_Last_State != BUTTON_DOWM))
        {
          btn->Timer_Count=0;
          TRIGGER_CB(BUTTON_DOWM);    //单击
          btn->Button_State = NONE_TRIGGER;
          btn->Button_Last_State = BUTTON_DOWM;
        }
        
      #endif

      break;
    }

    default :
      break;
  }
  
}

主要介绍了几个基础函数,更多的请参看源码,不懂的可以和小飞哥交流!

按键驱动移植应用

光说不练假把式,文章开始就说了,此按键驱动非常好移植到嵌入式平台,那有多好移植呢,且看...

cubemx配置

此次用到的资源比较少,硬件上1个按键、串口、定时器,来看看如何配置,基础配置可以参考以下文章:

cubemx的正确打开方式

一种轻便的裸机多任务

systick使用详解

按键连接在PA3,低电平有效,配置为输入上拉模式即可

串口配置:

配置比较简单,不啰嗦啦

应用代码编写

代码在上节裸机多任务工程上添加,再来添加进来本次的按键驱动代码,按键驱动代码使用方法:

  • 1、创建句柄
代码语言:javascript
复制
Button_t Button1;
Button_t Button2; 
  • 2、创建按键,初始化按键信息,包括按键名字、按键电平检测函数接口、按键触发电平
代码语言:javascript
复制
  Button_Create("Button1",    //按键名字
                &Button1,     //按键句柄
                Read_Button1_Level,  //按键电平检测函数接口
                BTN_TRIGGER);      //触发电平
                
                ......
  • 3、按键触发事件与事件回调函数链接映射,当按键事件被触发的时候,自动跳转回调函数中处理业务逻辑。
代码语言:javascript
复制
  Button_Attach(&Button1,BUTTON_DOWM,Btn2_Dowm_CallBack);  //按键单击
  Button_Attach(&Button1,BUTTON_DOUBLE,Btn2_Double_CallBack); //双击
  Button_Attach(&Button1,BUTTON_LONG,Btn2_Long_CallBack);  //长按
    
    .......
  • 4、周期调用回调按键处理函数即可,建议调用周期20-50ms
代码语言:javascript
复制
Button_Process();     //需要周期调用按键处理函数

需要用户实现的2个函数:

  • 1、按键电平检测接口:
代码语言:javascript
复制
uint8_t Read_KEY1_Level(void)
{
 return HAL_GPIO_ReadPin(Usr_Key_GPIO_Port,Usr_Key_Pin);
}
  • 2、按键逻辑处理
代码语言:javascript
复制
void Btn1_Dowm_CallBack(void *btn)
{
  PRINT_INFO("Button1 单击!");
}

void Btn1_Double_CallBack(void *btn)
{
  PRINT_INFO("Button1 双击!");
}
.....
.....

关于按键消抖,连击触发间隔等时间等只需要修改宏定义即可

是不是超级简单,只需要添加接口及上层逻辑就可以了

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小飞哥玩嵌入式 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 实现效果
  • 原理及代码接口分析
    • 创建按键
      • 按键触发事件与回调函数映射链接
        • 删除按键
          • 按键处理
          • 按键驱动移植应用
            • cubemx配置
              • 应用代码编写
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档