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

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.rhkb.cn/news/482949.html

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

相关文章

oracle数据库的启动与关闭

一.oracle数据库的启动过程 启动实例&#xff08;Start the Instance&#xff09; 启动实例&#xff1a;一个Oracle数据库实例由内存结构和后台进程组成&#xff0c;启动实例时会加载这些内存结构和启动进程。实例是数据库的一个运行时环境&#xff0c;它包含了数据库的控制文…

最新版Chrome浏览器调用ActiveX控件之allWebOffice控件

allWebOffice控件概述 allWebOffice控件能够实现在浏览器窗口中在线操作微软Office及WPS办公文档的应用&#xff08;阅读、编辑、保存等&#xff09;&#xff0c;支持编辑文档时保留修改痕迹&#xff0c;支持书签位置内容动态填充&#xff0c;支持公文套红&#xff0c;支持文档…

AD软件如何快速切换三维视图,由2D切换至3D,以及如何恢复

在Altium Designer软件中&#xff0c;切换三维视图以及恢复二维视图的操作相对简单。以下是具体的步骤&#xff1a; 切换三维视图 在PCB设计界面中&#xff0c;2D切换3D&#xff0c;快捷键按住数字键盘中的“3”即可切换&#xff1b; 快捷键ctrlf&#xff08;或者vb快捷键也…

el-table 最简单的方法配置图片预览功能

el-table 最简单的方法配置图片预览功能 效果预览 1、安装插件 npm install v-viewernext viewerjs2、全局引入&#xff0c;配置main.js // main.js import VueViewer from v-viewer; import viewerjs/dist/viewer.css; app.use(VueViewer, {url: data-src, // 指定 data-* …

深度学习框架PyTorch中的Tensor详解

目录 ​编辑 引言 PyTorch Tensor基础 什么是Tensor&#xff1f; Tensor与NumPy ndarray Tensor的特性 多维数组 数据类型 设备兼容性 自动求导 广播机制 视图和副本 使用Tensor 创建Tensor 操作Tensor 移动Tensor 自动求导 结论 引言 在深度学习的浪潮中&a…

【实战】Oracle基础之控制文件内容的5种查询方法

关于Jady&#xff1a; ★工作经验&#xff1a;近20年IT技术服务经验&#xff0c;熟悉业务又深耕技术&#xff0c;为业务加持左能进行IT技术规划&#xff0c;右能处理综合性故障与疑难杂症&#xff1b; ★成长历程&#xff1a;网络运维、主机/存储运维、程序/数据库开发、大数…

【Docker】Docker配置远程访问

配置Docker的远程访问&#xff0c;你需要按照以下步骤进行操作&#xff1a; 1. 在Docker宿主机上配置Docker守护进程监听TCP端口 Docker守护进程默认只监听UNIX套接字&#xff0c;要实现远程访问&#xff0c;需要修改配置以监听TCP端口。 ‌方法一&#xff1a;修改Docker服务…

利用Ubuntu批量下载modis图像(New)

由于最近modis原来批量下载的代码不再直接给出&#xff0c;因此&#xff0c;再次梳理如何利用Ubuntu下载modis数据。 之前的下载代码为十分长&#xff0c;现在只给出一部分&#xff0c;需要自己再补充另一部分。之前的为&#xff1a; 感谢郭师兄的指导&#xff08;https://blo…

视频流媒体服务解决方案之Liveweb视频汇聚平台

一&#xff0c;Liveweb视频汇聚平台简介: LiveWeb是深圳市好游科技有限公司开发的一套综合视频汇聚管理平台&#xff0c;可提供多协议&#xff08;RTSP/RTMP/GB28181/海康Ehome/大华&#xff0c;海康SDK等&#xff09;的视频设备接入&#xff0c;支持GB/T28181上下级联&#xf…

飞凌嵌入式受邀亮相OpenHarmony人才生态大会2024

2024年11月27日&#xff0c;OpenHarmony人才生态大会2024在武汉洲际酒店举行。在这场汇聚了行业精英、技术大咖及生态伙伴的年度盛会上&#xff0c;飞凌嵌入式作为OpenHarmony社区的重要成员受邀出席&#xff0c;并展示了其在OpenHarmony 4.1系统适配方面的最新成果。 在大会的…

【智商检测——DP】

题目 代码 #include <bits/stdc.h> using namespace std; const int N 1e510, M 110; int f[N][M]; int main() {int n, k;cin >> n >> k;for(int i 1; i < n; i){int x;cin >> x;f[i][0] __gcd(f[i-1][0], x);for(int j 1; j < min(i, k)…

打造双层环形图:基础与高级渐变效果的应用

在数据可视化领域&#xff0c;环形图因其独特的展示方式而广受欢迎。今天&#xff0c;我们将通过ECharts库来创建一个具有双层渐变效果的高级环形图。本文将详细介绍如何实现这种视觉效果。 1. 环形图基础 首先&#xff0c;我们需要了解环形图的基本构成。环形图由内外两个圆…

开源的跨平台SQL 编辑器Beekeeper Studio

一款开源的跨平台 SQL 编辑器&#xff0c;提供 SQL 语法高亮、自动补全、数据表内容筛选与过滤、连接 Web 数据库、存储历史查询记录等功能。该编辑器支持 SQLite、MySQL、MariaDB、Postgres 等主流数据库&#xff0c;并兼容 Windows、macOS、Linux 等桌面操作系统。 项目地址…

MacOS 配置github密钥

MacOS 配置github密钥 1. 生成GitHub的SSH密钥对 ssh-keygen -t ed25519 -C "xxxxxxx.com" -f ~/.ssh/id_ed25519_github 其中 xxxxxxxxxxx.com 是注册github、gitee和gitlab的绑定账号的邮箱 -t ed25519:生成密钥的算法为ed25519&#xff08;ed25519比rsa速度快&…

网际协议(IP)与其三大配套协议(ARP、ICMP、IGMP)

网际协议&#xff08;Internet Protocol&#xff0c;IP&#xff09;&#xff0c;又称互联网协议。是OSI中的网络层通信协议&#xff0c;用于跨网络边界分组交换。它的路由功能实现了互联互通&#xff0c;并从本质上建立了互联网。网际协议IP是 TCP/IP 体系中两个最主要的协议之…

永磁同步电机谐波抑制算法(11)——基于矢量比例积分调节器(vector PI controller,VPI controller)的谐波抑制策略

1.前言 相比于传统的谐振调节器&#xff0c;矢量比例积分调节器&#xff08;vector PI controller&#xff0c;VPI controller&#xff09;多一个可调零点&#xff0c;能够实现电机模型的零极点对消。因此VPI调节器也被广泛应用于交流控制/谐波抑制中。 2.参考文献 [1] A. G…

Windows下从命令行(Powershell/CMD)发送内容到系统通知中心

Windows下从命令行&#xff08;Powershell/CMD&#xff09;发送内容到系统通知中心 01 前言 在平时写脚本的时候&#xff0c;将日志等信息直接输出到控制台固然是最直接的&#xff0c;而如果是一些后台执行的任务&#xff0c;不需要时刻关注运行细节但是又想知道一些大致的情…

排序(数据结构)

排序&#xff1a; 所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 常见排序法 . 常见排序算法的实现 插入排序 1.直接插入排序 2.希尔排序( 缩小增量排序&#xff09; 希尔排序的特性总结&#x…

Android:生成Excel表格并保存到本地

提醒 本文实例是使用Kotlin进行开发演示的。 一、技术方案 org.apache.poi:poiorg.apache.poi:poi-ooxml 二、添加依赖 [versions]poi "5.2.3" log4j "2.24.2"[libraries]#https://mvnrepository.com/artifact/org.apache.poi/poi apache-poi { module…

基数排序(代码+注释)

#include <stdio.h> #include <stdlib.h>// 获取数组中的最大值 int GetMax(int* a, int n) {int max a[0];for (int i 1; i < n; i) {if (a[i] > max) {max a[i];}}return max; }// 对数组按照某个位数进行计数排序 void CountingSortForRadix(int* a, i…