这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » STM32 » 【转载】单片机状态机实现多个按键同时检测单击、多击、长按等操作--from森

共2条 1/1 1 跳转至

【转载】单片机状态机实现多个按键同时检测单击、多击、长按等操作--from森

工程师
2024-12-21 13:21:53     打赏
1.背景

在之前有个项目需要一个或多个按键检测:单击、双击、长按等操作

于是写了一份基于状态机的按键检测,分享一下思路

2.实现效果

单击翻转绿灯电平

双击翻转红灯电平

长按反转红绿灯电平

实现状态机检测按键单击,双击,长按等状态

3.代码实现

本代码是基于正点原子STM32F407ZGT6探索者开发板 HAL库写的

关于按键的代码可以直接移植,与芯片和HAL库没有多大联系,主要就是引脚定义是使用CubeMX生成的在main.h中,如下

#define BUTTON3_Pin GPIO_PIN_2

#define BUTTON3_GPIO_Port GPIOE

#define BUTTON2_Pin GPIO_PIN_3

#define BUTTON2_GPIO_Port GPIOE

#define BUTTON1_Pin GPIO_PIN_4

#define BUTTON1_GPIO_Port GPIOE

#define LED0_Pin GPIO_PIN_9

#define LED0_GPIO_Port GPIOF

#define LED1_Pin GPIO_PIN_10

#define LED1_GPIO_Port GPIOF

3.1 driver_button.c文件

#include "main.h"

#include "driver_boutton.h"

 

#define NUM_BUTTONS 3  

#define DOUBLE_CLICK_TIME  200  // 双击最大间隔时间(ms)  

#define LONG_PRESS_TIME  300  // 长按最小持续时间(ms)

 

void button_scan(void);

void button_init(void);

ButtonNum button_get_number(void);

 

// GPIO端口和PIN引脚数组  

const GPIO_TypeDef* button_GPIO_Ports[NUM_BUTTONS] = 

{  

    BUTTON1_GPIO_Port,BUTTON2_GPIO_Port, BUTTON3_GPIO_Port,

};  

  

const uint16_t button_GPIO_Pins[NUM_BUTTONS] = 

{  

    BUTTON1_Pin,BUTTON2_Pin, BUTTON3_Pin, 

};

 

// 按键状态定义  

typedef enum 

{  

    BUTTON_RELEASED,  //松开

    BUTTON_PRESSED,  //按下

    BUTTON_SINGLE_CLICK,  //单击

    BUTTON_DOUBLE_CLICK,  //双击

    BUTTON_LONG_PRESS  //长按

} Button_State; 

 

// 按键结构体定义  

typedef struct 

{  

GPIO_TypeDef *GPIOx;

uint16_t GPIO_PIN;              // 按键连接的GPIO引脚  

Button_State state;          // 按键状态  

uint32_t press_time;        // 按下时间  

uint32_t release_time;    // 释放时间 

uint8_t click_count;            // 连续点击次数  

uint32_t num; // 按键键值

} Button_TypeDef;  

 

//按键函数指针

const Button_Handler *button = &(const Button_Handler)

{

    .get_tick = HAL_GetTick, //获取系统时间滴答

    .init = button_init, //按键初始化

    .callback = button_scan, //按键扫描回调函数

.get_number = button_get_number, //获取键值

};

 

 

static Button_TypeDef buttons[NUM_BUTTONS]; 

 

static ButtonNum button_num = {0,0,0};

 

 

/**

  * @简要   初始化按键配置

  * @说明   该函数对每个按键的GPIO端口和引脚进行初始化,并将按键状态设置为未按下

  * @参数   无

  * @返回值 无

  */

void button_init(void) 

{  

    for (int i = 0; i < NUM_BUTTONS; i++) 

{  

        buttons[i].GPIOx = (GPIO_TypeDef*)button_GPIO_Ports[i];  

        buttons[i].GPIO_PIN = button_GPIO_Pins[i];  

        buttons[i].state = BUTTON_RELEASED;  

        buttons[i].click_count = 0;  

buttons[i].num = 0x01 << i;

    }  

}  

 

/**

  * @简要   定时器扫描按键

  * @说明   定时器消抖扫描并检测按键状态

  * @参数   无

  * @返回值 无

  */

void button_scan(void) {  

    uint32_t current_time = button->get_tick();  // 获取当前时间  

 

    for (int i = 0; i < NUM_BUTTONS; i++) //遍历所有按键

{  

Button_TypeDef *button = &buttons[i];  

uint8_t current_state = HAL_GPIO_ReadPin(button->GPIOx, button->GPIO_PIN);  // 读取按键状态  

 

        if (current_state == 0) // 按键按下

{    

            if (button->state == BUTTON_RELEASED) // 如果之前是松开状态

{  

                button->press_time = current_time;  // 记录按下时间

                button->state = BUTTON_PRESSED;  //更新按键状态为按下

            } 

        } 

else  // 按键释放 

{   

            if (button->state == BUTTON_PRESSED) // 如果之前是按下状态

{  

                button->release_time = current_time;  // 记录释放时间

 

                uint32_t press_duration = button->release_time - button->press_time;   // 计算按下持续时间

 

                if (press_duration >= LONG_PRESS_TIME) // 如果按下时间超过长按阈值

{  

                    button->state = BUTTON_LONG_PRESS; // 更新状态为长按

                    button_num.more |= buttons[i].num; // 标记长按事件

                } 

else //如果按下时间在长按阈值范围内

{  

                    button->click_count++;  // 增加点击计数

                }  

                // 复位按键状态  

                button->state = BUTTON_RELEASED;  

            }  

        }

        if (button->click_count)  // 如果有点击计数

{

            // 距离下一次按下时间大于 DOUBLE_CLICK_TIME 可认为是单击

            if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME) 

{

                button->click_count = 0;  // 重置点击计数

                button_num.once |= buttons[i].num; // 标记单击事件

            }

            // 否则 在 DOUBLE_CLICK_TIME 时间段内按几下算几连击

            else if (button->click_count >= 2 && current_time - button->release_time > DOUBLE_CLICK_TIME)

{

                button->click_count = 0;   // 重置点击计数

                button_num.twice |= buttons[i].num; // 标记双击事件

            }                                   

        }

    }  

}  

 

/**

  * @简要   获取按键状态

  * @说明   返回当前各类按键的键值

  * @参数   无

  * @返回值 按键的键值

  */

ButtonNum button_get_number(void) 

{

    ButtonNum temp = button_num;

    button_num.once = 0;

    button_num.twice = 0;

    button_num.more = 0;

    return temp;

}

3.2 driver_button.h文件

#ifndef __driver_button__

#define __driver_button__

 

#include <stdint.h>

 

#define BUTTON1_ONCE (0x01 << 0)

#define BUTTON2_ONCE (0x01 << 1)

#define BUTTON3_ONCE (0x01 << 2)

 

#define BUTTON1_TWICE (0x01 << 0)

#define BUTTON2_TWICE (0x01 << 1)

#define BUTTON3_TWICE (0x01 << 2)

 

#define BUTTON1_MORE (0x01 << 0)

#define BUTTON2_MORE (0x01 << 1)

#define BUTTON3_MORE (0x01 << 2)

 

typedef struct{

uint32_t once; //单击

uint32_t twice; //双击

uint32_t more; //长按

}ButtonNum;

 

extern ButtonNum button_num;

// 按键处理函数结构体定义  

typedef struct {

    uint32_t (*get_tick)(void);           // 获取系统时间的函数指针

    void (*init)(void);                  // 初始化函数指针

    void (*callback)(void);              // 回调函数指针

ButtonNum (*get_number)(void);

} Button_Handler;

 

extern const Button_Handler *button;

 

 

#endif 

3.3 在定时器中断中 检测按键

这里我使用的是TIM6,每10ms扫描一次 

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

{

static uint32_t timerCount_key = 0;

  if(htim->Instance == TIM6)

  {

  timerCount_key++;

  if(timerCount_key == 10)

  {

  timerCount_key = 0;

  button->callback();

  }

  }

}

3.4 主函数中使用方法

这里使用按键控制led灯演示

  /* USER CODE BEGIN 2 */

HAL_TIM_Base_Start_IT(&htim6);

button->init();

  /* USER CODE END 2 */

 

  /* Infinite loop */

  /* USER CODE BEGIN WHILE */

  while (1)

  {

    /* USER CODE END WHILE */

 

    /* USER CODE BEGIN 3 */

ButtonNum num = button->get_number();  

if(num.twice == BUTTON1_TWICE) HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);

if(num.twice == BUTTON2_TWICE) HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);

if(num.twice == BUTTON3_TWICE) HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);

 

if(num.more == BUTTON1_MORE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin),HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);

if(num.more == BUTTON2_MORE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin),HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);

if(num.more == BUTTON3_MORE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin),HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin);

 

if(num.once == BUTTON1_ONCE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);

if(num.once == BUTTON2_ONCE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);

if(num.once == BUTTON3_ONCE) HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);

 

  }

  /* USER CODE END 3 */

4.按键状态机思路

void button_scan(void) 

主要思路是这样:

我每次定时器执行这个按键扫描的回调函数,都会轮询判断一下所有的按键状态。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

{

static uint32_t timerCount_key = 0;

  if(htim->Instance == TIM6)

  {

  timerCount_key++;

  if(timerCount_key == 10)

  {

  timerCount_key = 0;

  button->callback();

  }

  }

}

例如在此之前我从来没按下过按键,当我的按键1按下的时刻,

uint8_t current_state = HAL_GPIO_ReadPin(button->GPIOx, button->GPIO_PIN);  // 读取按键状态  

current_state被返回了低电平(取决于你的电路设计,我这里按键按下接地)

然后就会进入到

    if (current_state == 0)    // 按键按下

    {    

        if (button->state == BUTTON_RELEASED)    // 如果之前是松开状态

        {  

            button->press_time = current_time;  // 记录按下时间

            button->state = BUTTON_PRESSED;    // 更新按键状态为按下

        } 

    } 

在这里由于我们是第一次按下会被标记为状态为按下,然后将你的结构体中的按下时间记录为这一次扫描按键时的HAL_GetTick();
然后你按下按键是需要松手的吧
现在你松手了,接上面的if语句:

else    // 按键释放 

    {   

        if (button->state == BUTTON_PRESSED) // 如果之前是按下状态

        {  

            button->release_time = current_time;  // 记录释放时间

 

            uint32_t press_duration = button->release_time - button->press_time;   // 计算按下持续时间

 

            if (press_duration >= LONG_PRESS_TIME) // 如果按下时间超过长按阈值

            {  

                button->state = BUTTON_LONG_PRESS; // 更新状态为长按

                button_num.more |= buttons[i].num;    // 标记长按事件

            } 

            else // 如果按下时间在长按阈值范围内

            {  

                button->click_count++;  // 增加点击计数

            }  

            // 复位按键状态  

            button->state = BUTTON_RELEASED;  

        }  

    }

松手之后(按键释放,那么按键又被上拉到高电平了),这里先判断一下你之前的状态,必须要判断一下这个按键之前是不是被按下了,要不然就会一直进入这个if语句。
由于每次进入这个按键扫描函数都会记录一下HAL_GetTick();,

uint32_t press_duration = button->release_time - button->press_time;   // 计算按下持续时间

所以记下了你上次按下按键与这次松开按键的时间间隔,那么这就可以得出你的按下时间,如果超过了长按阈值那么肯定就是长按状态了,就执行对应的长按操作。


如果你的时间间隔少于长按的时间阈值,那么就会给你增加一次点击计数。

之后你松开了按键那么可能要把按键的状态恢复到初始化的情况。


这时这个函数还没有结束,接下来会进入到这个if语句:

    if (button->click_count)    // 如果有点击计数

    {

        // 距离下一次按下时间大于 DOUBLE_CLICK_TIME 可认为是单击

        if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME) 

        {

            button->click_count = 0;      // 重置点击计数

            button_num.once |= buttons[i].num;      // 标记单击事件

        }

        // 否则 在 DOUBLE_CLICK_TIME 时间段内按几下算几连击

        else if (button->click_count >= 2 && current_time - button->release_time > DOUBLE_CLICK_TIME)

        {

            button->click_count = 0;     // 重置点击计数

            button_num.twice |= buttons[i].num;   // 标记双击事件

        }                                   

    }

如果你按下按键的时间低于长按的时间阈值的话,那么就会进入这个函数,否则直接跳过这个if语句。
例如,这个时候从头到尾,你只按了一次低于长按时间阈值的操作,暂停时间分析:
再进入这个if语句:

    if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME) 

    {

        button->click_count = 0;  // 重置点击计数

        button_num.once |= buttons[i].num;      // 标记单击事件

    }

这里判断你的点击次数为1,但是当前你按下到松手后时间还没有超过双击的时间阈值,那么

current_time - button->release_time > DOUBLE_CLICK_TIME 

就是false,if语句就进不去,但是如果时间再过去一点,

current_time - button->release_time > DOUBLE_CLICK_TIME

就是true,时间超过了双击的阈值,所以直接判断为单击。

再回到:例如,这个时候从头到尾,你只按了一次低于长按时间阈值的操作,时间暂停分析
接着上面的if判断:

  if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME) 

    {

        button->click_count = 0;  // 重置点击计数

        button_num.once |= buttons[i].num;      // 标记单击事件

    }

目前你还没有超过双击的时间阈值
紧接着你又按下了一次按键,并且这一次按下时间同样低于双击的阈值,那么就会继续增加的点击计数
直到本次按键的时间间隔大于双击的阈值,则判断结束,可以返回按键的点击次数了

5.结束

目前代码能够正常检测单击,双击,长按等操作

来源: 整理文章为传播相关技术,网络版权归原作者所有,如有侵权,请联系删除。


专家
2024-12-21 22:18:44     打赏
2楼

感谢分享


共2条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]