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

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.结束

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/62799.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】字符与ASCII码转换的深度探讨

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目一&#xff1a;打印ASCII码代码实现代码分析代码优化优化思路 &#x1f4af;题目二&#xff1a;打印字符代码实现代码分析代码优化优化思路 &#x1f4af;C中字符与ASC…

计算机毕业设计Spark+SpringBoot旅游推荐系统 旅游景点推荐 旅游可视化 旅游爬虫 景区客流量预测 旅游大数据 大数据毕业设计

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

C++实现Raft算法之更多的细节(clerk与RPC)

本篇细节讲解的是clerk和RPC原理的讲解 clerk clerk相当于是一个外部的客户端&#xff0c;其作用就是向整个raft集群发起命令并接收响应。 clerk需要与kvServer建立网络链接&#xff0c;那么既然已经实现了已经简单的RPC&#xff0c;那么使用RPC来完成这个过程。 clerk本身的…

基于C#+SQLite开发数据库应用的示例

SQLite数据库&#xff0c;小巧但功能强大&#xff1b;并且是基于文件型的数据库&#xff0c;驱动库就是一个dll文件&#xff0c;有些开发工具 甚至不需要带这个dll&#xff0c;比如用Delphi开发&#xff0c;用一些三方组件&#xff1b;数据库也是一个文件&#xff0c;虽然是个文…

C++之异常智能指针其他

C之异常&智能指针&其他 异常关于函数异常声明异常的优劣 智能指针auto_ptrunique_ptrshared_ptrweak_ptr定制删除器 智能指针的历史与boost库 特殊类单例模式饿汉和懒汉的优缺点 C四种类型转换CIO流结语 异常 try括起来的的代码块中可能有throw一个异常&#xff08;可…

前端跳转路由的时候,清掉缓存

清除路由缓存的方法 ‌使用 $router.push() 方法‌&#xff1a;在跳转路由时&#xff0c;可以通过传递一个包含 replace: true 属性的对象来实现清除路由缓存。例如&#xff1a; this.$router.push({ path: "/new-route", replace: true }); ‌使用 $router.replace…

SpringBoot -拦截器Interceptor、过滤器 Filter 及设置

Spring Boot拦截器&#xff08;Interceptor&#xff09;的概念 - 在Spring Boot中&#xff0c;拦截器是一种AOP的实现方式。它主要用于<font style"color:#DF2A3F;">拦截请求</font>&#xff0c;在请求处理之前和之后执行特定的代码逻辑。与过滤器不同的…

Ubuntu 20.04 Server版连接Wifi

前言 有时候没有网线口插网线或者摆放电脑位置不够时&#xff0c;需要用Wifi联网。以下记录Wifi联网过程。 环境&#xff1a;Ubuntu 20.04 Server版&#xff0c;无UI界面 以下操作均为root用户&#xff0c;如果是普通用户&#xff0c;请切换到root用户&#xff0c;或者在需要权…

Java项目实战II基于微信小程序的亿家旺生鲜云订单零售系统的设计与实现(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着移动互联网技术的不断…

多线程安全单例模式的传统解决方案与现代方法

在多线程环境中实现安全的单例模式时&#xff0c;传统的双重检查锁&#xff08;Double-Checked Locking&#xff09;方案和新型的std::once_flag与std::call_once机制是两种常见的实现方法。它们在实现机制、安全性和性能上有所不同。 1. 传统的双重检查锁方案 双重检查锁&am…

Javaweb梳理21——Servlet

Javaweb梳理21——Servlet 21 Servlet21.1 简介21.3 执行流程21.4 生命周期4.5 方法介绍21.6 体系结构21.7 urlPattern配置21.8 XML配置 21 Servlet 21.1 简介 Servlet是JavaWeb最为核心的内容&#xff0c;它是Java提供的一门动态web资源开发技术。使用Servlet就可以实现&…

MySQL 主从同步一致性详解

MySQL主从同步是一种数据复制技术&#xff0c;它允许数据从一个数据库服务器&#xff08;主服务器&#xff09;自动同步到一个或多个数据库服务器&#xff08;从服务器&#xff09;。这种技术主要用于实现读写分离、提升数据库性能、容灾恢复以及数据冗余备份等目的。下面将详细…

点云3DHarris角点检测算法推导

先回顾2D的Harris角点检测算法推导 自相关矩阵是Harris角点检测算法的核心之一&#xff0c;它通过计算图像局部区域的梯度信息来描述该区域的特征。在推导Harris角点检测算法中的自相关矩阵时&#xff0c;我们首先需要了解自相关矩阵的基本思想和数学背景。 参考 [经典角点检…

在 CentOS 上安装 Docker:构建容器化环境全攻略

一、引言 在当今的软件开发与运维领域&#xff0c;Docker 无疑是一颗璀璨的明星。它以轻量级虚拟化的卓越特性&#xff0c;为应用程序的打包、分发和管理开辟了崭新的高效便捷之路。无论是开发环境的快速搭建&#xff0c;还是生产环境的稳定部署&#xff0c;Docker 都展现出了…

Unity-Particle System属性介绍(一)基本属性

什么是ParticleSystem 粒子系统是Unity中用于模拟大量粒子的行为的组件。每个粒子都有一个生命周期&#xff0c;包括出生、运动、颜色变化、大小变化和死亡等。粒子系统可以用来创建烟雾、火焰、水、雨、雪、尘埃、闪电和其他各种视觉效果。 开始 在项目文件下创建一个Vfx文件…

.NET8/.NETCore 依赖注入:自动注入项目中所有接口和自定义类

.NET8/.NETCore 依赖接口注入&#xff1a;自动注入项目中所有接口和自定义类 目录 自定义依赖接口扩展类&#xff1a;HostExtensions AddInjectionServices方法GlobalAssemblies 全局静态类测试 自定义依赖接口 需要依赖注入的类必须实现以下接口。 C# /// <summary>…

Brain.js(二):项目集成方式详解——npm、cdn、下载、源码构建

Brain.js 是一个强大且易用的 JavaScript 神经网络库&#xff0c;适用于前端和 Node.js 环境&#xff0c;帮助开发者轻松实现机器学习功能。 在前文Brain.js&#xff08;一&#xff09;&#xff1a;可以在浏览器运行的、默认GPU加速的神经网络库概要介绍-发展历程和使用场景中&…

使用pyQT完成简单登录界面

import sysfrom PyQt6.QtGui import QMovie,QPixmap from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton,QLineEdit#封装我的窗口类 class MyWidget(QWidget):#构造函数def __init__(self):#初始化父类super().__init__()# 设置窗口大小self.resize(330,…

理解 Python PIL库中的 convert(‘RGB‘) 方法:为何及如何将图像转换为RGB模式

理解 Python PIL库中的 convert(RGB) 方法&#xff1a;为何及如何将图像转换为RGB模式 在图像处理中&#xff0c;保持图像数据的一致性和可操作性是至关重要的。Python的Pillow库&#xff08;继承自PIL, Python Imaging Library&#xff09;提供了强大的工具和方法来处理图像&…

avcodec_alloc_context3,avcodec_open2,avcodec_free_context,avcodec_close

avcodec_alloc_context3 是创建编解码器上下文&#xff0c;需要使用 avcodec_free_context释放 需要使用avcodec_free_context 释放 /** * Allocate an AVCodecContext and set its fields to default values. The * resulting struct should be freed with avcodec_free_co…