前言

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

实现效果



原理及代码接口分析

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

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

创建按键

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

/************************************************************
* @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); //打印信息

}
Button_Create("Button1",
&Button1,
Read_KEY1_Level,
KEY_ON);

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

面试加分项|一个优秀的按键驱动框架_回调函数

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

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

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

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,是高电平有效:

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

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

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

/************************************************************
* @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; //按键事件触发的回调函数,用于处理按键事件
}
}
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个参数是按键触发的方式,单击、双击、连击等等,根据触发事件链接对应的回调函数

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个参数是对应的回调函数

/* 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 连按释放!");
}

删除按键

/************************************************************
* @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;
}
}
}

按键处理

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

/************************************************************
* @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,低电平有效,配置为输入上拉模式即可面试加分项|一个优秀的按键驱动框架_#endif_04

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

串口配置:

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

配置比较简单,不啰嗦啦

应用代码编写

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

  • 1、创建句柄
Button_t Button1;
Button_t Button2;
  • 2、创建按键,初始化按键信息,包括按键名字、按键电平检测函数接口、按键触发电平
Button_Create("Button1",    //按键名字
&Button1, //按键句柄
Read_Button1_Level, //按键电平检测函数接口
BTN_TRIGGER); //触发电平

......
  • 3、按键触发事件与事件回调函数链接映射,当按键事件被触发的时候,自动跳转回调函数中处理业务逻辑。
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
Button_Process();     //需要周期调用按键处理函数

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

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

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

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

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

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

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

源码获取及交流

欢迎添加小飞哥好友,进群一起交流,公众号后台回复:“Button”,即可获取完整工程面试加分项|一个优秀的按键驱动框架_回调函数_09