单片机状态机实现多个按键同时检测单击、多击、长按等操作
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,或者值得优化的地方,欢迎评论区留言!
相关文章:
单片机状态机实现多个按键同时检测单击、多击、长按等操作
1.背景 在之前有个项目需要一个或多个按键检测:单击、双击、长按等操作 于是写了一份基于状态机的按键检测,分享一下思路 2.实现效果 单击翻转绿灯电平 双击翻转红灯电平 长按反转红绿灯电平 实现状态机检测按键单击,双击,长…...

oracle之用户的相关操作
(1)创建用户(sys用户下操作) 简单创建用户如下: CREATE USER username IDENTIFIED BY password; 如果需要自定义更多的信息,如用户使用的表空间等,可以使用如下: CREATE USER mall IDENTIFIED BY 12345…...

黑马redis
Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理 Redisson分布式锁实现15问 文章目录 主线程和IO线程是如何协作的Unix网络编程中的五种IO模型Linux世界一切皆文件生产上限制keys *、flushdb、flushall等危险命令keys * 遍历查询100W数据花…...
HCIA-Access V2.5_1_2 PON技术的特点、优势与典型应用
PON接入技术优势 它的接入方式有两种,点到点光接入和点到多点光接入。 点到点 PON口的资源被一个用户独占,该用户可以享受到更好的带宽体验,同时故障好排查,出现问题,重点检测这一条链路以及终端用户,同…...

css部分
前面我们学习了HTML,但是HTML仅仅只是做数据的显示,页面的样式比较简陋,用户体验度不高,所以需要通过CSS来完成对页面的修饰,CSS就是页面的装饰者,给页面化妆,让它更好看。 1 层叠样式表&#…...

【TCP 网络通信(发送端 + 接收端)实例 —— Python】
TCP 网络通信(发送端 接收端)实例 —— Python 1. 引言2. 创建 TCP 服务器(接收端)2.1 代码示例:TCP 服务器2.2 代码解释: 3. 创建 TCP 客户端(发送端)3.1 代码示例:TCP…...

LSTM+改进的itransformer时间序列预测模型代码
代码在最后 本次设计了一个LSTM基于差分多头注意力机制的改进的iTransformer时间序列预测模型结合了LSTM(长短期记忆网络)和改进版的iTransformer(差分多头注意力机制),具备以下优势: 时序特征建模能力&am…...

Apache-HertzBeat 开源监控默认口令登录
0x01 产品描述: HertzBeat(赫兹跳动) 是一个开源实时监控系统,无需Agent,性能集群,兼容Prometheus,自定义监控和状态页构建能力。HertzBeat 的强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速构建自有监控系统。0x02 漏洞描述: HertzBeat(赫兹跳动) 开源实时…...

Delete Number
翻译: 主要思路解释 整体思路概述: 本题的目标是给定整数(要删除的数字个数)和整数(以字符串形式表示的数字),通过合理删除个数字,使得最终得到的新数字最小。程序采用了一种贪心算…...

Linux常用快捷键
目录 编辑 剪切/复制/粘贴/删除等快捷键 终端及标签页快捷键 历史命令快捷键 移动光标快捷键 控制命令 剪切/复制/粘贴/删除等快捷键 快捷键 功能 ShiftCtrlC 复制 ShiftCtrlV 粘贴 CtrlInsert 复制命令行内容 ShiftInsert 粘贴命令行内容 Ctrlk 剪切&#…...
针对xpath局限的解决方案
上篇《网页数据提取利器 -- Xpath》我们对xpath的介绍中提到了xpath的几点局限性: 结构依赖性强性能动态网页支持不足 本篇是针对这些局限提出的解决方案和补充方法,以提升 XPath 的实用性和适应性。 1. 动态网页的处理 局限: XPath 无法…...

深入解析 HTML Input 元素:构建交互性表单的核心
🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…...

ffmpeg转码与加水印
文章目录 转码 与加水印引入jar包代码ffmpeg安装错误解决方法 转码 与加水印 引入jar包 <dependency><groupId>net.bramp.ffmpeg</groupId><artifactId>ffmpeg</artifactId><version>0.6.2</version></dependency>代码 impo…...

Leetcode 104. 二叉树的最大深度(Java-深度遍历)
题目描述: 给定一个二叉树 root ,返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例: 输入:root [3,9,20,null,null,15,7] 输出:3示例 2: 输入:…...
阳明心学-传习录学习总结
资料 王阳明介绍:明代杰出的思想家、军事家、教育家;自刑部主事历任贵州龙场驿丞、庐陵知县、右佥都御史、南赣巡抚、两广总督等职,接连平定南赣、两广盗乱及宸濠之乱,因功获封“新建伯”,成为明代因军功封爵的三位文…...
macOS sequoia 15.1中应用程序“程序坞”没有权限打开
在macOS sequoia 15.1版本中新安装的应用程序在访达中打开报错显示应用程序“程序坞”没有权限打开“(null)”。 解决办法 在启动台中找到终端,点击打开,切换到应用目录下,输入 cd /Applications/ 找到需要打开的应用程序目录࿰…...

使用 MinIO 和 KKFileView 实现在线文件预览功能
在项目开发中,文件的在线预览是常见的需求,尤其是对 PDF、Word、Excel 等格式的文件进行无客户端依赖的直接查看。本文将介绍如何通过 MinIO 和 KKFileView 搭建在线文件预览服务,并通过 docker-compose 一键部署。 一、环境准备 1. Docker …...
Conda-Pack打包:高效管理Python环境
在Python开发中,环境管理是一个不可忽视的重要环节。Conda是一个流行的包管理器和环境管理器,它允许用户创建隔离的环境,以避免不同项目之间的依赖冲突。Conda-pack是一个工具,可以帮助我们将一个conda环境打包成一个可移植文件&a…...
云服务器上搭建 WordPress 全流程指南
WordPress 是全球最受欢迎的开源内容管理系统(CMS),通过 WordPress,你可以轻松搭建博客、企业网站或电子商务平台。而通过云服务器搭建 WordPress,可以使网站获得更好的性能和灵活性。本文将为你提供详细的步骤&#x…...

图像超分辨率技术新进展:混合注意力聚合变换器HAAT
目录 1. 引言: 2. 混合注意力聚合变换器(HAAT): 2.1 Swin-Dense-Residual-Connected Block(SDRCB): 2.2 Hybrid Grid Attention Block(HGAB): 3. 实验结…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...

3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案
在移动互联网营销竞争白热化的当下,推客小程序系统凭借其裂变传播、精准营销等特性,成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径,助力开发者打造具有市场竞争力的营销工具。 一、系统核心功能架构&…...

数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !
我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...
uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)
UniApp 集成腾讯云 IM 富媒体消息全攻略(地理位置/文件) 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型,核心实现方式: 标准消息类型:直接使用 SDK 内置类型(文件、图片等)自…...