目录
为什么要对按键消抖
按键是一个机械结构,当你按压下去的时候,电平不会马上趋于理想化,而是会上下乱跳,这导致我们不能根据按键做出正确的决定,所以我们需要消抖,怎么进行消抖了,那就需要跳过这段上下乱跳的时间,这时我们就用到了定时器,为什么要用到状态机了,因为我们可以根据按键的状态来区分不同的情况

定义按键的状态
typedef struct {
GPIO_TypeDef* port; // 设备GPIO端口
uint16_t pin; // 设备引脚
uint8_t state; // 当前状态机状态
uint16_t debounce_cnt; // 消抖计数器
uint32_t last_event_time; // 上次事件触发时间
uint8_t click_count; // 点击次数
KeyEvent event; // 当前发生的事件
void (*event_handler)(uint8_t key_id, KeyEvent event); // 事件处理器
} DebounceDevice;
通过状态机检测当前按键的状态,可以触发按键对应的驱动事件
// 按键处理状态机
typedef enum {
BTN_STATE_IDLE, // 空闲
BTN_STATE_DEBOUNCE_PRESS, // 消抖按下
BTN_STATE_PRESSED, // 按下
BTN_STATE_DEBOUNCE_RELEASE, // 消抖释放
BTN_STATE_RELEASED, // 释放
BTN_STATE_WAIT_DOUBLE, // 等待双击
BTN_STATE_LONG_PRESS, // 长按
BTN_STATE_LONG_HOLD_PRESS //持续长按
} ButtonState;
有自己需要的可以在.h中自己添加
事件阈值,可以自己调试
/*定义时间阈值*/
#define DEBOUNCE_TIME 10 // 10ms消抖时间
#define DOUBLECLICK_TIMEOUT 300 // 300ms双击超时
#define LONG_PRESS_TIME 1000 // 1000ms长按判定
#define HOLD_REPEAT_INTERVAL 2000 // 2000ms持续长按
// 按键事件处理器函数指针类型
typedef void (*KeyEventHandler)(uint8_t key_id, KeyEvent event);
通过创建一个定时器,按照1ms中断1次,来定期扫描按键的状态,最基本的就是按下,按下消抖,松开,松开消抖,这四个状态来创建其他状态,比如单击,双击(只要两次单击事件时间没有超时就认为是双击)长按的时间,这个很简单吧
自动触发按键事件
inline void TriggerEvent(uint8_t key_id, KeyEvent event)
{
device[key_id].event = event;
assert(custom_handlers[key_id] != NULL);
//调用按键事件处理函数
custom_handlers[key_id](key_id, event);
#ifdef KEY_EVENT_DEBUG
switch(event)
{
case KEY_EVENT_SINGLE_CLICK:
printf("KEY%d single click\r\n", key_id);
break;
case KEY_EVENT_DOUBLE_CLICK:
printf("KEY%d double click \r\n", key_id);
break;
case KEY_EVENT_LONG_PRESS:
printf("KEY%d long click\r\n", key_id);
break;
case KEY_EVENT_HOLD:
printf("KEY%d >2s long click\r\n", key_id);
break;
case WAIT_KEY_EVENT_LONG_PRESS:
printf("KEY%d wait long click release\r\n",key_id);
break;
default:
break;
}
#endif
}
按键事件代码注册函数
初始化按键时钟,GPIO,这里默认的是电阻上拉,按下低电平,一些按键是电阻下拉,按下高电平,可以自己根据PORT来区分,根据原理图来设计,这里简化了
void DebounceDevice_init(DebounceDevice *device)
{
assert(device != NULL);
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(device->port == GPIOE)
{
__HAL_RCC_GPIOE_CLK_ENABLE();
}
else if(device->port == GPIOA)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
}
/*
additional code
*/
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;//查看原理图设置,比如GPIOA PIN_0为下拉输入
GPIO_InitStruct.Pin = device->pin;
HAL_GPIO_Init(device->port, &GPIO_InitStruct);
}
按键初始化函数
void key_init(uint8_t key_id, GPIO_TypeDef *port, uint16_t pin, KeyEventHandler handler)
{
assert(key_id < KEY_COUNT);
//初始化按键设备
device[key_id].port = port;
device[key_id].pin = pin;
device[key_id].state = BTN_STATE_IDLE;
device[key_id].debounce_cnt = 0;
device[key_id].last_event_time = 0;
device[key_id].click_count = 0;
device[key_id].event = KEY_EVENT_NONE;
//注册按键事件处理函数
if(handler == NULL)
{
custom_handlers[key_id] = KeyE_Handler;
}
else
{
custom_handlers[key_id] = handler;
}
DebounceDevice_init(&device[key_id]);
/*初始化定时器为1ms*/
tim_init(TIM2,7200-1,10-1);
}
这个是按键事件驱动函数,可以在其他.c中定义,根据key_id来区分对应的事件驱动,比如更改esp8266的sta/ap/sta+ap模式,都可以,如果不需要定义,传入NULL即可,事件注册函数会自动加载内部例子事件驱动
__weak void KeyE_Handler(uint8_t key_id , KeyEvent event)//自定义的按键组
{
if(event == KEY_EVENT_SINGLE_CLICK)//单击事件
{
//if(key_id == 0)
//根据自己需要
LED0_TOGGLE();
}
else if(event == KEY_EVENT_DOUBLE_CLICK)//双击事件
{
//if(key_id == 0)
beep_toggle();
}
else if(event == KEY_EVENT_LONG_PRESS)//长按事件
{
//if(key_id == 0)
uint8_t tmp=0,hum=0;
uint8_t result=DHT11_Get_Data(&tmp,&hum);
printf("DHT11_ERROR:%d\r\n",result);
Oled_printf(0,0,1,"tmp:%d hum:%d",tmp,hum);
}
/*
key_id 主要用来处理按键组的单个按键,不需要定义太多的单按键事件处理函数
*/
}
总结
完整代码
key.c
#include "./BSP/KEY/key.h"
/*可以在该文件中注册按键设备*/
KeyEventHandler custom_handlers[KEY_COUNT] = {NULL};
DebounceDevice device[KEY_COUNT]; //按键设备数组
void DebounceDevice_init(DebounceDevice *device)
{
assert(device != NULL);
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(device->port == GPIOE)
{
__HAL_RCC_GPIOE_CLK_ENABLE();
}
else if(device->port == GPIOA)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
}
/*
additional code
*/
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;//查看原理图设置,比如GPIOA PIN_0为下拉输入
GPIO_InitStruct.Pin = device->pin;
HAL_GPIO_Init(device->port, &GPIO_InitStruct);
}
/**
* @brief 初始化按键设备
*
* @param key_id 按键ID
* @param port 按键连接的GPIO端口
* @param pin 按键连接的GPIO引脚
* @param handler 按键事件处理函数指针
*
* @note 该函数会初始化按键设备结构体,注册事件处理函数,并初始化定时器为1ms
*/
void key_init(uint8_t key_id, GPIO_TypeDef *port, uint16_t pin, KeyEventHandler handler)
{
assert(key_id < KEY_COUNT);
//初始化按键设备
device[key_id].port = port;
device[key_id].pin = pin;
device[key_id].state = BTN_STATE_IDLE;
device[key_id].debounce_cnt = 0;
device[key_id].last_event_time = 0;
device[key_id].click_count = 0;
device[key_id].event = KEY_EVENT_NONE;
//注册按键事件处理函数
if(handler == NULL)
{
custom_handlers[key_id] = KeyE_Handler;
}
else
{
custom_handlers[key_id] = handler;
}
DebounceDevice_init(&device[key_id]);
/*初始化定时器为1ms*/
tim_init(TIM2,7200-1,10-1);
}
inline void TriggerEvent(uint8_t key_id, KeyEvent event)
{
device[key_id].event = event;
assert(custom_handlers[key_id] != NULL);
//调用按键事件处理函数
custom_handlers[key_id](key_id, event);
#ifdef KEY_EVENT_DEBUG
switch(event)
{
case KEY_EVENT_SINGLE_CLICK:
printf("KEY%d single click\r\n", key_id);
break;
case KEY_EVENT_DOUBLE_CLICK:
printf("KEY%d double click \r\n", key_id);
break;
case KEY_EVENT_LONG_PRESS:
printf("KEY%d long click\r\n", key_id);
break;
case KEY_EVENT_HOLD:
printf("KEY%d >2s long click\r\n", key_id);
break;
case WAIT_KEY_EVENT_LONG_PRESS:
printf("KEY%d wait long click release\r\n",key_id);
break;
default:
break;
}
#endif
}
/*单独按键事件实现函数添加处理,可以在别的.c文件定义*/
__weak void KeyE_Handler(uint8_t key_id , KeyEvent event)//自定义的按键组
{
if(event == KEY_EVENT_SINGLE_CLICK)//单击事件
{
//if(key_id == 0)
//根据自己需要
LED0_TOGGLE();
}
else if(event == KEY_EVENT_DOUBLE_CLICK)//双击事件
{
//if(key_id == 0)
beep_toggle();
}
else if(event == KEY_EVENT_LONG_PRESS)//长按事件
{
//if(key_id == 0)
uint8_t tmp=0,hum=0;
uint8_t result=DHT11_Get_Data(&tmp,&hum);
printf("DHT11_ERROR:%d\r\n",result);
Oled_printf(0,0,1,"tmp:%d hum:%d",tmp,hum);
}
/*
key_id 主要用来处理按键组的单个按键,不需要定义太多的单按键事件处理函数
*/
}
tim.c
#include "./BSP/TIM/tim.h"
/*配置定时器,提供API接口*/
extern DebounceDevice device[];
/*
add tim handle here
*/
TIM_HandleTypeDef g_htim2;
TIM_HandleTypeDef g_htim3;
void tim_init(TIM_TypeDef * instance, uint16_t prescaler, uint16_t arr)
{
assert(instance != NULL);
if(instance == TIM2)
{
__HAL_RCC_TIM2_CLK_ENABLE();
g_htim2.Instance = instance;
g_htim2.Init.Prescaler = prescaler;
g_htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
g_htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
g_htim2.Init.Period = arr;
HAL_TIM_Base_Init(&g_htim2);
HAL_TIM_Base_Start_IT(&g_htim2);
/*设置中断优先级*/
HAL_NVIC_SetPriority(TIM2_IRQn, 4, 0);
/*使能中断*/
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
else if(instance == TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE();
g_htim3.Instance = instance;
g_htim3.Init.Prescaler = prescaler;
g_htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
g_htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
g_htim3.Init.Period = arr;
HAL_TIM_Base_Init(&g_htim3);
HAL_TIM_Base_Start_IT(&g_htim3);
HAL_NVIC_SetPriority(TIM3_IRQn, 4, 0);
/*使能中断*/
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
/*
additional code here
*/
/*htim 需要在其他外设中自定义初始化*/
}
/*定时器中断服务函数*/
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_htim2);
}
void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_htim3);
}
/*定时器中断回调函数*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t tick_cnt = 0;
tick_cnt++;
if(htim->Instance == TIM2)
{
for(uint8_t i=0; i< KEY_COUNT; i++)
{
//获取按键状态
DebounceDevice * btn = &device[i];
uint8_t pin_state = HAL_GPIO_ReadPin(btn->gpio, btn->pin);
switch(btn->state)
{
case BTN_STATE_IDLE:
if(pin_state = GPIO_PIN_RESET) //检测到按下
{
btn->state = BTN_STATE_DEBOUNCE_PRESS;
btn->debounce_cnt = DEBOUNCE_TIME; //开始消抖
btn->last_event_time = tick_cnt; //记录按键按下时间
}
break;
case BTN_STATE_DEBOUNCE_PRESS:
if(pin_state == GPIO_PIN_RESET) //检测到按下
{
if(--btn->debounce_cnt == 0)
{
btn->state = BTN_STATE_PRESSED;
TriggerEvent(i, KEY_EVENT_PRESS);
btn->last_event_time = tick_cnt; //记录按键按下时间
}
}
else
{
btn->state = BTN_STATE_IDLE;
}
break;
case BTN_STATE_PRESSED:
//检测长按
if(tick_cnt - btn->last_event_time >= LONG_PRESS_TIME)
{
btn->state = BTN_STATE_LONG_PRESS;
TriggerEvent(i, KEY_EVENT_LONG_PRESS);
btn->last_event_time = tick_cnt; //记录按键按下时间
}
//检测松开
if(pin_state == GPIO_PIN_SET)
{
btn->state = BTN_STATE_DEBOUNCE_RELEASE;
btn->debounce_cnt = DEBOUNCE_TIME; //开始消抖
}
break;
case BTN_STATE_DEBOUNCE_RELEASE:
if(pin_state == GPIO_PIN_SET) //检测到松开
{
if(--btn->debounce_cnt == 0)
{
TriggerEvent(i, KEY_EVENT_RELEASE);
//增加单击计数
btn->click_count++;
//判断是否为双击
btn->state = BTN_STATE_WAIT_DOUBLE;
btn->last_event_time = tick_cnt; //记录按键按下时间
}
}
else
{
btn->state = BTN_STATE_PRESSED;
}
break;
case BTN_STATE_WAIT_DOUBLE:
//是否超时
if(tick_cnt - btn->last_event_time >= DOUBLE_CLICK_TIME)
{
// 根据点击次数触发事件
if(btn->click_count == 1)//单击
{
TriggerEvent(i, KEY_EVENT_SINGLE_CLICK);
}
else if(btn->click_count >= 2)//双击
{
TriggerEvent(i, KEY_EVENT_DOUBLE_CLICK);
}
//重置计数
btn->click_count = 0;
btn->state = BTN_STATE_IDLE;
}
//是否为双击
else if(pin_state == GPIO_PIN_RESET) //再次按下
{
btn->state = BTN_STATE_DEBOUNCE_PRESS;
btn->debounce_cnt = DEBOUNCE_TIME; //开始消抖
}
break;
default :
btn->state = BTN_STATE_IDLE;
break;
}
}
/*additional code here*/
}
else if(htim->Instance == TIM3)
{
/*additional code here*/
}
/*
additional code here
*/
}
原理是很简单的,通过中断定期扫描可以节约cpu资源,但是要注意中断的优先级
另外不要在中断里面加一些复杂的代码,会导致中断时间延长,中断回调函数,处理代码的时间越短越好,千万别加延时函数
1569

被折叠的 条评论
为什么被折叠?



