单片机状态机实现多个按键同时检测单击、多击、长按等操作

1.背景

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

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

2.实现效果

单击翻转绿灯电平

双击翻转红灯电平

长按反转红绿灯电平

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

3.代码实现

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

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

cpp 复制代码
#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文件

cpp 复制代码
#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文件

cpp 复制代码
#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扫描一次

cpp 复制代码
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灯演示

cpp 复制代码
  /* 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.按键状态机思路

cpp 复制代码
void button_scan(void) 

主要思路是这样:

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

cpp 复制代码
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按下的时刻,

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

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

然后就会进入到

cpp 复制代码
    if (current_state == 0)    // 按键按下
    {    
        if (button->state == BUTTON_RELEASED)    // 如果之前是松开状态
        {  
            button->press_time = current_time;  // 记录按下时间
            button->state = BUTTON_PRESSED;    // 更新按键状态为按下
        }  	
    } 

在这里由于我们是第一次按下会被标记为状态为按下,然后将你的结构体中的按下时间记录为这一次扫描按键时的HAL_GetTick();

然后你按下按键是需要松手的吧

现在你松手了,接上面的if语句:

cpp 复制代码
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();,

cpp 复制代码
uint32_t press_duration = button->release_time - button->press_time;   // 计算按下持续时间

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

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

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

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

cpp 复制代码
    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语句:

cpp 复制代码
    if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME) 
    {
        button->click_count = 0;  // 重置点击计数
        button_num.once |= buttons[i].num;      // 标记单击事件
    }

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

cpp 复制代码
current_time - button->release_time > DOUBLE_CLICK_TIME 

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

cpp 复制代码
current_time - button->release_time > DOUBLE_CLICK_TIME

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

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

接着上面的if判断:

cpp 复制代码
  if (button->click_count == 1 && current_time - button->release_time > DOUBLE_CLICK_TIME) 
    {
        button->click_count = 0;  // 重置点击计数
        button_num.once |= buttons[i].num;      // 标记单击事件
    }

目前你还没有超过双击的时间阈值

紧接着你又按下了一次按键,并且这一次按下时间同样低于双击的阈值,那么就会继续增加的点击计数

直到本次按键的时间间隔大于双击的阈值,则判断结束,可以返回按键的点击次数了

5.结束

目前代码能够正常检测单击,双击,长按等操作,如果读者使用此代码发现有什么bug,或者值得优化的地方,欢迎评论区留言!

相关推荐
hi9428 分钟前
Vivado - 远程调试 + 远程综合实现 + vmWare网络配置 + NFS 文件共享 + 使用 VIO 核
嵌入式硬件·fpga开发·vivado 远程开发·vmware网络配置
一只电子牛蛙2 小时前
【单片机】IIC需要注意什么(企业级回答)
单片机·嵌入式硬件
沐欣工作室_lvyiyi3 小时前
基于单片机的无线水塔监控系统设计(论文+源码)
人工智能·stm32·单片机·嵌入式硬件·单片机毕业设计
上海文顺负载箱3 小时前
怎样衡量电阻负载的好坏
单片机·嵌入式硬件
云山工作室6 小时前
基于单片机的智能照明控制系统(论文+源码
stm32·单片机·嵌入式硬件·毕业设计·毕设
前面的题目以后再来探索吧7 小时前
ota-总结--wifi
stm32·单片机
奇偶变不变7 小时前
RTOS之事件集
java·linux·jvm·单片机·算法
染不尽的流年7 小时前
EMMC , UFS, SSD介绍
嵌入式硬件
Whappy0017 小时前
《第十二部分》1.STM32之RTC实时时钟介绍---BKP实验
stm32·嵌入式硬件·实时音视频
每月一号准时摆烂8 小时前
数字逻辑(五)——用二进制来表示音频和视频
嵌入式硬件·音视频