状态机加定时器消抖

目录

为什么要对按键消抖

定义按键的状态

自动触发按键事件

按键事件代码注册函数

总结


为什么要对按键消抖

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

定义按键的状态

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资源,但是要注意中断的优先级

另外不要在中断里面加一些复杂的代码,会导致中断时间延长,中断回调函数,处理代码的时间越短越好,千万别加延时函数

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值