在蓝桥杯嵌入式的比赛中,按键是每年必考的,题目要求的运行逻辑基本都需要按键的参与,而且按键监测的直接反应到系统运行的流畅程度上,所以按键非常重要。
我在准备比赛的过程中,想实现同时监测四个按键的短按长按双击这功能,查阅了很多资料,使用了一个很多人在优快云上写的按键框架,但是按键的反应非常迟缓,一直被这个问题所困扰,后面我对这个框架进行了一些小小的修改,可以显著提高运行速度。
1、接下来我先写出我修改的思路
2、最后再放一个我在GitHub上找到的按键程序,是一个大神写的,写的很厉害!
3、之后再将按键部分的全部代码附上可以直接复制修改使用
1、修改思路:
仅仅只给需要双击的按键提供双击功能的监测,其余不需要双击的按键的,直接跳过双击监测这个功能。这样的好处是可以提高按键的反应速度,因为监测一个按键的双击是非常耗时的,会造成很严重的漏检测现象,而监测按键的单击和长按,是不耗时的。
根据代码流程走一遍就可以知道,首先每隔10ms获取一次每个按键对应GPIO的值,如果初次按下按键情况,则先进行消抖,消抖完成后继续监测同时开始计时,如果按键仍处于按下状态,则根据定义的长按时间判断是否为长按,如果在过程中松开了,则直接返回为单击状态,如果确实是达到了长按的时长,则返回为长按状态。可以看到在监测长按短按的流程中是没有额外耗时的。但是如果是监测双击,则在接收到按键为单击状态后,仍需等待一个判断为双击的最大时间,如果等待结束后确定没有第二次按下了,才返回为单击。如果用户的想法是单击,不是双击,则这个等待判断双击的时间,就是额外的耗时了,这就是用大家都用的按键框架的弊端。
而且这套代码,是使用状态机的思想写的,很容易理解, 自己跟着流程写两遍,到时候基本上就可以写出来了。到考场上可修改的空间也很大很方便,很容易就可以把这套代码修改为监测四个按键的单击、修改为监测四个按键单击和一个按键长按的组合。若是同时检测四个按键单击双击长按,确实会反应迟缓,但真实的考题应该不会有这种要求,真有的话这套代码也能用。
2、GitHub上的ButtonDrive
之前韦东山老师的MQTT智能家居的课,用的也是这个大神写的开源的MQTT库。
作者对的Button_drive简介:Button_drive是一个小巧的按键驱动,支持单击、双击、长按、连续触发等(后续可以在按键控制块中添加触发事件),理论上可无限量扩展Button,Button_drive采用按键触发事件回调方式处理业务逻辑,支持在RTOS中使用,我目前仅在RT-Thread上测试过。 写按键驱动的目的是想要将用户按键逻辑与按键处理事件分离,用户无需处理复杂麻烦的逻辑事件。
3、全部代码
/**
* @file mycode.c
* @brief 可以同时监测四个按键的单击、长按,监测第一个按键的单击、长按、双击。
* 使用TIM2定时器,开启定时器中断,每10ms轮询一次。
* @author wangkaiwei (1357689541@qq.com)
* @version 1.0
* @date 2024-03-11
* @copyright Copyright (c) {2024} CJLU
* @par 修改日志:
* 2024-03-11 1.0 wangkaiwei
*
*/
#include "main.h"
#include "tim.h"
#define KEY_B1 HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin)
#define KEY_B2 HAL_GPIO_ReadPin(B2_GPIO_Port, B2_Pin)
#define KEY_B3 HAL_GPIO_ReadPin(B3_GPIO_Port, B3_Pin)
#define KEY_B4 HAL_GPIO_ReadPin(B4_GPIO_Port, B4_Pin)
typedef enum
{
key_no = 0, /* 无按键按下 */
key_click = 1, /* 单击 */
key_long = 2, /* 长按 */
key_double = 3 /* 双击 */
}KEY_EVENT;
typedef struct key{
u8 key_input_val; /* GPIO读取出来按键的真实值 */
u8 key_state_buff1; /* 按键执行状态1,用于记录状态 */
u8 key_state_buff2; /* 按键执行状态2,用于记录状态,只有按键1用 */
KEY_EVENT key_real_result; /* 最终判断结果 */
unsigned char key_time_cnt1; /* 定时器1 */
unsigned char key_time_cnt2; /* 定时器2,只有按键1用 */
}t_key;
t_key key[4];
/* 按住500ms以上判断为长按 */
#define KEY_LONG_PRESS_TIME 50
/* 两次按下间隔200ms以内判断为双击 */
#define KEY_DOUBLE_GAP_TIME 20
/**
* @brief 初始化按键的状态和值,同时开启tim2的中断
*/
void key_init(void)
{
for(int i=0;i<4;i++)
{
key[i].key_state_buff1 = 0;
key[i].key_state_buff2 = 0;
key[i].key_real_result = key_no;
key[i].key_time_cnt1 = 0;
key[i].key_time_cnt2 = 0;
}
HAL_TIM_Base_Start_IT(&htim2);
}
/**
* @brief 监测4个按键的长按和短按,这个函数放在tim2的溢出中断中
* @param knum 按键编号
* @return KEY_EVENT
*/
static KEY_EVENT key_driver(int knum)
{
KEY_EVENT key_result = key_no;
switch(key[knum].key_state_buff1)
{
case 0:
{
if(key[knum].key_input_val == 0)
{
/* 如果按键按下,状态切换到按键消抖和确认状态 */
key[knum].key_state_buff1 = 1;
}
break;
}
case 1:/* 消抖和确认状态 */
{
if(key[knum].key_input_val == 0)
{
/* 检测到按键仍处于按下状态 */
/* 消抖完成,计时器key_time_cnt1开始计时 */
/* 状态切换到计时状态 */
key[knum].key_time_cnt1 = 0;
key[knum].key_state_buff1 = 2;
}
else
{
/* 按键松开在抖动,返回state_0 */
key[knum].key_state_buff1 = 0;
}
break;
}
case 2:/* 计时状态 */
{
if(key[knum].key_input_val == 1)
{
/* 按键松开了,产生一次click操作 */
key_result = key_click;
key[knum].key_state_buff1 = 0;
}
else if( ++key[knum].key_time_cnt1 > KEY_LONG_PRESS_TIME)
{
/* 继续按下超过1000ms,返回长按操作 */
key_result = key_long;
key[knum].key_state_buff1 = 3;/* 状态切换到按键释放状态,等待按键松开 */
}
break;
}
case 3:
{
/* 等待按键松开,松开后回到初始状态 */
if(key[knum].key_input_val == 1)
{
key[knum].key_state_buff1 = 0;
}
break;
}
}
return key_result;
}
/**
* @brief TIM溢出中断回调函数。记得一定要在CUBEMX中勾选TIM2的中断,设置其触发一次中断时间为10ms。
* @param htim My Param doc
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
KEY_EVENT key_result;
if(htim->Instance == TIM2)
{
/* 10ms读取一次 */
key[0].key_input_val = KEY_B1;
key[1].key_input_val = KEY_B2;
key[2].key_input_val = KEY_B3;
key[3].key_input_val = KEY_B4;
for(int i=0;i<4;i++)
{
if(i != 0)
{
/* 按键2 3 4仅判断长按和单击 */
key[i].key_real_result = key_driver(i);
}
else
{
/* 单独判断按键1的双击情况 */
key_result = key_driver(i);
switch(key[i].key_state_buff2)
{
case 0:
{
if(key_result == key_click)
{
/* 对于单击,判断是否还有还有下次一按下,进入到下一个状态判断是否为双击 */
key[i].key_time_cnt2 = 0;
key[i].key_state_buff2 = 1;
}
else
{
/* 如果返回结果不是双击,则是长按,长按的话直接返回结果即可 */
key[i].key_real_result = key_result;
}
break;
}
case 1:
{
if(key_result == key_click)/* 又一次单击,时间间隔小于KEY_DOUBLE_GAP_TIME定义的时间 */
{
key[i].key_real_result = key_double; /* 返回双击事件 */
key[i].key_state_buff2 = 0;
}
else if(++key[i].key_time_cnt2 > KEY_DOUBLE_GAP_TIME)
{
key[i].key_real_result = key_click;/* 时间超过KEY_DOUBLE_GAP_TIME定义的时间,返回为一次的单击 */
key[i].key_state_buff2 = 0;
}
break;
}
}
}
}
}
}
void key_proc(void)
{
/*B1*/
if (key[0].key_real_result == key_click)
{
LCD_ClearLine(Line1);
LCD_DisplayStringLine(Line1, (uint8_t *)" key1_click ");
key[0].key_real_result = key_no;/* 清除按键事件,置为无键事件 */
}
else if (key[0].key_real_result == key_long)
{
LCD_ClearLine(Line1);
LCD_DisplayStringLine(Line1, (uint8_t *)" key1_long ");
key[0].key_real_result = key_no;
}
else if (key[0].key_real_result == key_double)
{
LCD_ClearLine(Line1);
LCD_DisplayStringLine(Line1, (uint8_t *)" key_double ");
key[0].key_real_result = key_no;
}
/*B2*/
if (key[1].key_real_result == key_click)
{
LCD_ClearLine(Line2);
LCD_DisplayStringLine(Line2, (uint8_t *)" key2_click ");
key[1].key_real_result = key_no;
}
else if (key[1].key_real_result == key_long)
{
LCD_ClearLine(Line2);
LCD_DisplayStringLine(Line2, (uint8_t *)" key2_long ");
key[1].key_real_result = key_no;
}
/*B3*/
if (key[2].key_real_result == key_click)
{
LCD_ClearLine(Line3);
LCD_DisplayStringLine(Line3, (uint8_t *)" key3_click ");
key[2].key_real_result = key_no;
}
else if (key[2].key_real_result == key_long)
{
LCD_ClearLine(Line3);
LCD_DisplayStringLine(Line3, (uint8_t *)" key3_long ");
key[2].key_real_result = key_no;
}
/*B4*/
if (key[3].key_real_result == key_click)
{
LCD_ClearLine(Line4);
LCD_DisplayStringLine(Line4, (uint8_t *)" key4_click ");
key[3].key_real_result = key_no;
}
else if (key[3].key_real_result == key_long)
{
LCD_ClearLine(Line4);
LCD_DisplayStringLine(Line4, (uint8_t *)" key4_long ");
key[3].key_real_result = key_no;
}
}