当前位置: 首页 > news >正文

按键收集单击,双击和长按

按键收集单击,双击和长按

引言

在我们生活中, 按键是必不可少的, 不同的电器, 有不同的按键, 但是按键总有不够用的时候, 那么给与一个按键赋予不同的功能,就必不可少了. 一个按键可以通过按下的时间长短和频次, 来定义其类型。

一次按键收集, 都是在一个按键收集周期的, 比如500毫秒内, 我要收集一个按键类型, 在500毫秒内,按键按下一次按键, 那么到达500毫秒后, 我们就宣布答案, 此次按键是单击。

同理, 在500毫秒内, 如果我们按下了, 两次按键,那么就是双击, 以此类推。

那长按呢? 长按就是, 在500毫秒内, 我们按下了按键,并且500ms时间到时, 按键还未松开, 就代表着长按。

本博客最终修改项目链接下载:

点击下载

https://wwyz.lanzoul.com/itXC327v9dra

按键收集模型

image-20240818183801773

单片机实现方法

因为按键, 需要每时每刻的去检测, 是否按下, 如果使用while阻塞循环, 则会大大影响程序效率, 所以我们使用外部中断去检测中断。

按键事件触发检测

按键按下的瞬间, 单片机的io口, 就会被从高电平拉到低电平, 然后单片机io口就会检测到, 从而触发下降沿中断。

此时,就代表着按键事件已经触发了。

按键类型判断

​ 按键事件触发,仅仅代表着我们按下了按键, 但是我们想区分单击,双击和长按, 那么我们就得从按键按下开始计算,直到按键计算周期结束,这一段时间我们定义成 500毫秒。

所以, 我们在按键事件触发后,我们需要启动定时器,计时500ms, 然后500ms时间到后, 判断按键按下的次数, 还有按键此时是否仍然被按下.

在这 500毫秒内, 我们重复按下按键, 就需要重复的检测按键按下的事件, 然后计入按键次数内.

所以我们的处理模型是

image-20240818193546975

代码实现

按键中断

按键中断初始化

我们初始化io端口PB1,配置下降沿中断

image-20240819125528156

void Key_Init(void)
{GPIO_InitTypeDef gpio_initstruct;EXTI_InitTypeDef exti_initstruct; NVIC_InitTypeDef nvic_initstruct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//打开GPIOB的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//打开复用时钟gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU;				//设置为输出gpio_initstruct.GPIO_Pin = GPIO_Pin_1;						//将初始化的Pin脚gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;				//可承载的最大频率GPIO_Init(GPIOB, &gpio_initstruct);							//初始化GPIOGPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);exti_initstruct.EXTI_Line = EXTI_Line1;exti_initstruct.EXTI_Mode = EXTI_Mode_Interrupt;exti_initstruct.EXTI_Trigger = EXTI_Trigger_Falling;exti_initstruct.EXTI_LineCmd = ENABLE;EXTI_Init(&exti_initstruct);nvic_initstruct.NVIC_IRQChannel = EXTI1_IRQn;nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 2;nvic_initstruct.NVIC_IRQChannelSubPriority = 2;nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&nvic_initstruct);}
触发中断,按键标志位赋1

image-20240819125747163

extern _Bool key_down;
void EXTI1_IRQHandler(void)
{if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){//代表按键按下(按键标志清除--一次时间处理完成)key_down = 1;}EXTI_ClearITPendingBit(EXTI_Line1);
}	
定时器中断扫描判断按键是否按下

当触发中断后,我们把key_down置成1后, 就会被定时器扫描,检测到, 我们就 把按键次数加1, 并且启动 500ms的计时, count = 1, 然后if(count >0), 就会启动 count++计时, 因为我们定时器是1ms运行一次,所以当count == 500的时候,进行按键类型检测

image-20240819125930008

按键类型判断

当到达500ms后, 我们就可以通过判断按键次数, 区分单双击, 单击的情况下,通过判断500ms时,io口的状态,从而判断单击还是长按。

单击和长按区分

单击和长按, 按键次数 button_count 都是1, 不同的是,500毫秒之后,单击按键类型, 已经松开, 长按,则是按下状态, 所以我们通过读取检测500ms时间到时, io的状态, 就可以区分长按和单击了。

长按, io口是低电平, 单击,io是高电平, (我们是下降沿触发, 按键一端连io口,一端连 地, io口输出状态是, 推挽输出)

button_mode = 1代表 单击 ,

button_mode = 3代表 长按

image-20240819130853341

image-20240819141941257

uint8_t key_scan(void)
{uint8_t value;value = 0;	//默认是不是一直低电平//检测是否长按if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)// 判断按键是否仍然在按下{//这时候还是按下状态, 不一定是真正的按键按下, 有可能是抖动造成的,接下来延时消抖//再次判断按键是否按下, 如果按下, 代表真正的按键仍然在按下if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){//那就等抬起了再做事value = 1;//这时候才可以判断是 长按下}}return value;
}
双击区分

通过判断, 按键的次数, 就可以得知双击, 记得按键类型判断完毕后, 要把按键次数置0, 为下次判断做准备。 button_mode = 2, 代表按键双击

image-20240819142123007

定时器扫描函数代码

void TIM4_IRQHandler(void)
{if(TIM_GetFlagStatus(TIM4,TIM_FLAG_Update))//一毫秒运行一次{//当 button_atom的时候,才能进行读取按键信息if(key_down == 1){	key_down = 0;button_count++;count = 1;	//开始计时}if(count > 0){count++;if(count > 500){count = 0;if(button_count == 1){button_count = 0;if(key_scan() == 0)	//短按button_mode = 1;else if(key_scan() == 1)	//长按button_mode = 3; }else if(button_count == 2){button_count = 0;button_mode = 2;		}else{button_count = 0;//判断完,到时间照样得button_count 清零,为后面做准备}}}		TIM_ClearFlag(TIM4,TIM_FLAG_Update);}
}

现象调试

判断按键类型方式

我们通过代码实操 ,已经可以得知我们按下的按键是单击,双击还是长按了,通过判断 button_mode 的数值。

image-20240819143221376

调用相关现象

我们选用PA0, PA1, PA2, 来分别代表 单击,双击和长按的小灯。

我们在检测到 button_mode 的数值改变的时候, 就赋值对应的小灯亮灭,这样我们就可以看到现象了

image-20240819144039916

初始化PA0,PA1,PA2

设置这三个io口分别为推挽输出模式

image-20240819181926289

void Led_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟//使用各个外设前必须开启时钟,否则对外设的操作无效/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;					//定义结构体变量GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;				//GPIO引脚,赋值为第0号引脚GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHzGPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数//函数内部会自动根据结构体的参数配置相应寄存器//实现GPIOA的初始化GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;				//GPIO引脚,赋值为第0号引脚GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHzGPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数//函数内部会自动根据结构体的参数配置相应寄存器//实现GPIOA的初始化GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;				//GPIO引脚,赋值为第0号引脚GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHzGPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数//函数内部会自动根据结构体的参数配置相应寄存器//引脚初始化led_go(0,0);led_go(1,0);led_go(2,0);		
}	

单独设置对应的小灯亮灭

void led_go(uint8_t led,_Bool go)

比如我们控制 PA0 io口, 我们传入 0, 代表我们要控制位序为0的小灯, 第二个参数 go = 1, 代表着让灯两, go = 0, 代表着让灯灭。这样我们就可以灵活的控制小灯亮灭了。

image-20240819182225221
void led_go(uint8_t led,_Bool go)
{if(led == 0){if(go == 1){GPIO_SetBits(GPIOA, GPIO_Pin_0);	//亮}else{GPIO_ResetBits(GPIOA, GPIO_Pin_0); //灭}}elseif(led == 1){if(go == 1){GPIO_SetBits(GPIOA, GPIO_Pin_1);	//亮}else{GPIO_ResetBits(GPIOA, GPIO_Pin_1); //灭}		}	elseif(led == 2){if(go == 1){GPIO_SetBits(GPIOA, GPIO_Pin_2);	//亮}else{GPIO_ResetBits(GPIOA, GPIO_Pin_2); //灭}		}	
}

根据 button_mode亮灭小灯

还是那个图, 我们有了底层构建, 我们根据单双击,然后会有不同的butoon_mode 按键模式, 所以我们根据这个数值, 赋值对应的小灯亮灭, 这样我们就可以看到现象了.

led控制图(ctrl 加鼠标左键,快速跳转)

image-20240819182628204
void chose_led(void)
{if(button_mode == 0){led_go(0,0);led_go(1,0);led_go(2,0);}else if(button_mode == 1){led_go(0,1);led_go(1,0);led_go(2,0);}else if(button_mode == 2){led_go(0,0);led_go(1,1);led_go(2,0);}else if(button_mode == 3){led_go(0,0);led_go(1,0);led_go(2,1);}	
}	

main.c主函数调用

初始化,按键和定时器, 还有调试的小灯, 我们根据按键模式, 控制对应的小灯亮灭, 观察现象

image-20240819182827540
_Bool key_down = 0;	//按键是否按下
uint16_t count;	//定时器计数毫秒数(按键按下时长)
uint8_t button_count = 0; 
uint8_t button_mode = 0;	//按键模式:无-0 , 单击- 1, 双击 - 2, 长按 - 3
int main(void)
{Key_Init();TIM4_Init();Led_Init();while(1){chose_led();}
}	

工程代码构建

我们上面, 代码的逻辑思路, 已经构建完毕, 同时我们也提供构建完成的工程, 我们这里带领大家, 复制黏贴

1.基本工程构建方法

创建stm32f103c8t6基本工程_stm32f103c8启动文件配置-CSDN博客

2.我们工程名字叫做

Key_detection_model

点击品字,然后修改工程名字

image-20240819183230023

3.我们用到main函数, 所以有User用户文件夹

我们用到按键检测, 所以用到 Key_model按键模型

我们用到系统的延时函数, 所以用到System系统文件夹

我们用到定时器,所以用到Time定时器文件夹

User
Key_model
System
Time

4.具体文件添加方式, 看最小例程构建,从第九步开始

我们User文件夹下, 添加

main.c
led.c
led.h

image-20240819183721379

Key_model文件夹下添加

key.c
key.h
image-20240819183757371

System文件夹下添加

delay.c
delay.h
sys.h
image-20240819200115950

Time文件夹下添加

TIM4.c
TIM4.h
image-20240819200130023

5.记得包含对应的文件夹路径

image-20240819200342838

6.按键收集模型代码快速复制

各文件对应的代码

跳转复制

  1. 构建完成如图

image-20240819201343274

8.编译运行,烧录

烧录方法

9.小灯辨别正负极方法

灯芯放在光下看, 大头是负极,小头是正极, 我们正极插在io口, 负极插在地, io口分别是 PA0, PA1, PA2

10.按键演示视频

按键演示视频_哔哩哔哩_bilibili

相关文章:

按键收集单击,双击和长按

按键收集单击,双击和长按 引言 在我们生活中, 按键是必不可少的, 不同的电器, 有不同的按键, 但是按键总有不够用的时候, 那么给与一个按键赋予不同的功能,就必不可少了. 一个按键可以通过按下的时间长短和频次, 来定义其类型。 一次按键收集, 都是在一个按键收集周…...

进程的异常终止

进程的异常终止 进程收到了某些信号,他杀 进程自己调用abort函数,产生了SIGABRT(6)信号,自杀 进程的最后一个线程收到了"取消"操作,并且做出响应 如果进程是异常结束的,atexit\on_exit它们事先注册的遗言…...

并发编程 | Future是如何优化程序性能

在初识Future一文中介绍了Future的核心方法。本文中接着介绍如何用Future优化我们的程序性能。 在此之前,有必要介绍Future接口的一个实现类FutureTask。 FutureTask介绍 FutureTask继承结构 首先我们看一下FutureTask的继承结构: public class Futur…...

Oracle笔记

一、 如何解决 sqlplus 无法使用退格键和方向键 .bashrc 中添加如下内容,解决 退格键 stty erase ^h 安装 rlwap 后,执行如下命令可解决 方向键 rlwap sqlplus 二、 都有哪些备份数据到工具 三、 谈谈 你对 oracle 中实例和数据库的理解 数据库是一…...

LVS+Keepalived 双机热备

LVSKeepalived 双机热备 Keepalived案例分析Keepalived工具介绍Keepalived工具介绍一、功能特点 一、理解Keepalived实现原理实验报告资源列表一、安装keepalived以及ipvsadm Keepalived案例分析 企业应用中,单台服务器承担应用存在单点故障的危险单点故障一旦发生…...

Web Image scr图片从后端API获取基本实现

因系统开发中需求,会有页面显示图片直接从后端获取后显示,代码如下: 后端: /*** 获取图片流* param response* param fileName*/RequestMapping(value"getImgStream",method RequestMethod.GET)public void getImgStr…...

2024音频剪辑指南:探索四大高效工具!

音频剪辑不仅仅是技术活,更是一种艺术创作,它能够让声音更加生动、更具感染力。今天,我们就来探索几款优秀的音频剪辑工具。 福昕音频剪辑 链接:www.pdf365.cn/foxit-clip/ 福昕音频剪辑是一款界面简洁、操作直观的音频编辑软件…...

“CSS”第一步——WEB开发系列13

CSS (Cascading Style Sheets,层叠样式表),是一种用来为结构化文档(如 HTML 文档或 XML 应用)添加样式(字体、间距和颜色等)的计算机语言,CSS 文件扩展名为 .css。 一、什么是 CSS&a…...

IEEE802网络协议和标准

IEEE802网络协议和标准 802委员会IEEE 802介绍现有标准 IEEE 802.3介绍物理媒介类型MAC子层与LLC子层主要内容通讯标准POE供电标准802.3af、802.3at、802.3btIEEE802.3af的工作过程:IEEE802.3af主要供电参数:IEEE802.3af的分级参数:为什么会有…...

vulnhub靶机 DC-9(渗透测试详解)

一、靶机信息收集 1、靶机下载 https://download.vulnhub.com/dc/DC-9.zip 2、靶机IP扫描 3、探测靶机主机、端口、服务版本信息 4、靶机目录扫描 二、web渗透测试 1、访问靶机IP 查看页面功能点,发现一个搜索框和登录框 2、测试一下是否存在sql注入 查看当前数…...

javaweb的新能源充电系统pf

TOC springboot339javaweb的新能源充电系统pf 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现,改变了几千年以来人们的生活,不仅仅是生活物资的丰富,还有精神层次的丰富。在互联网诞生之前,地域位置往往是人们思想上不可跨域…...

如何在桌面同时展示多个窗口

一、实现2分屏显示 win箭头 二、实现3分屏显示 1. 在实现2分屏显示的基础上,再次点击箭头图标,这次选择屏幕的上方或下方。 2. 点击后,第三个窗口将会出现在你选择的区域。现在,你可以在三个窗口之间自由切换,提高工…...

The Sandbox 游戏制作教程(第 5 部分):创建基于分类的系统

欢迎回到我们的系列,我们将记录 The Sandbox Game Maker 的 “On-Equip”(装备)功能的多种用途。 如果你刚加入 The Sandbox,装备功能是 “可收集组件”(Collectable Component)中的一个多功能工具&#x…...

HTML浏览器缓存(Browser Cache)

介绍: 浏览器缓存是Web缓存中最直接、最常见的一种形式。当浏览器首次请求某个资源时,如果服务器响应中包含了缓存控制指令(如Cache-Control、Expires等),浏览器就会将这些资源存储在本地缓存中。后续请求相同资源时&a…...

短剧APP系统,推动短剧市场发展

近年来,短剧作为一直火爆的新兴行业,凭借着剧情进奏、爽、时长短等优势,深受大众欢迎,成为了大众碎片化时间的解压神器。 目前,随着短剧市场的快速发展,各个类型的短剧层出不穷,也推动了短剧AP…...

嵌入式 | 嵌入式 Linux 系统使用摄像头

点击上方"蓝字"关注我们 01、引言 >>> 在嵌入式 Linux 系统使用摄像头 俗话说“眼见为实”,这或许是为什么近年来摄像头在嵌入式系统上快速增长的原因。它们被用于不同的场景,如: 远程监控:典型的例子是闭路电视,监控人员在监视环境(或许你所在的大楼…...

C 开源库之cJSON

cJSON简介 CJSON库是一个用于解析和生成JSON数据的C语言库。 它提供了一组函数,使得在C语言中操作JSON数据变得简单而高效。 您可以使用CJSON库来解析从服务器返回的JSON数据,或者将C语言数据结构转换为JSON格式以进行传输。 cJSON 使用 官网地址&…...

ROW_NUMBER(), RANK(), DENSE_RANK() SQL排序函数图文详解

ROW_NUMBER(), RANK(), DENSE_RANK() ROW_NUMBER(): 为结果集中的每一行分配唯一的连续编号。即使有重复的值,ROW_NUMBER() 也会为它们分配不同的序号。 SELECT column_name, ROW_NUMBER() OVER (ORDER BY column_name) AS row_num FROM table_name;2. RANK(): 对结…...

Spring IoCDI(下)—DI的尾声

我们之前学习了控制反转IoC,接下来就开始学习依赖注入DI的细节。 依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。我们使用 Autowired 注解,完成依赖注入的操作。简单来说…...

仕考网:考外省公务员可以调回本地吗?

一般情况下,公务员岗位是固定不可随意更换的,因为每个职位都对应特定的职责和要求。一旦考到外地的岗位,想要调回本地几乎是不可能的。因为这样的操作可能导致职位空缺,进而需要通过公共招聘流程来填补,而不是简单地从…...

深入解析ConvLoRA:如何通过卷积增强LoRA在SAM模型中的微调效率

1. 为什么SAM模型微调需要ConvLoRA? 如果你玩过Meta开源的Segment Anything Model(SAM),大概率会有这样的体验:这个模型在“分割一切”的通用能力上确实惊艳,但当你把它拿到自己的具体任务上,比…...

2026年紧缺岗位薪资报告

导读:调研显示,2026 年国内企业平均薪资涨幅预算预计回升至 5.0%,这个数字高于全球平均水平,在亚太地区处于中等位置。但普调时代已经彻底终结,薪资资源的分配逻辑发生了根本性转变。所有的涨幅空间,都在向…...

Qwen3-ASR-1.7B在客服场景中的应用:智能语音助手落地案例

Qwen3-ASR-1.7B在客服场景中的应用:智能语音助手落地案例 1. 引言 "您好,请问有什么可以帮您?"这句话在客服中心每天要重复成千上万次。传统的客服系统面临着一个现实问题:人工客服成本高、培训周期长,而且…...

RK3588-PPS GPIO 配置笔记总结

1. 概述 本文档详细介绍如何在 RK3588 平台上将 GPIO1_B1 配置为接收 PPS(Pulse Per Second,秒脉冲)信号,用于高精度时间同步。PPS 信号通常由 GPS 模块提供,可实现微秒级的时间同步精度。 2. 设备树配置 2.1 Pinctrl …...

最优二叉查找树避坑指南:动态规划中的概率分配与子树合并陷阱

最优二叉查找树避坑指南:动态规划中的概率分配与子树合并陷阱 如果你在准备算法面试或者刷题时,曾经对着“最优二叉查找树”的题目发呆,感觉公式都懂,代码也能背,但一写就错,尤其是边界条件和概率累加总对不…...

适配 Native AOT:CommonLibraries 迎来重大更新

本文主要介绍了 Sang.AspNetCore.CommonLibraries 的最新更新。为了拥抱 .NET 的 Native AOT 特性,我们对核心类库进行了重构,并新增了对 code 与 status 字段的双向兼容支持,旨在性能与兼容性之间取得平衡。1. 为什么要更新?随着…...

IT系统全生命周期管理和运营方案(Word)

1 项目总体概述1.1 项目背景1.2 蓝图架构1.3 核心业务流程1.4 系统总体架构1.5 系统业务模型流程1.6 实施阶段划分1.7 一阶段建设目标2 一阶段解决方案2.1 系统总体架构2.2 系统总体流程2.3 软件功能设计2.3.1 统一门户(含多租户、权限、用户、角色、菜单、授权管理…...

GitHub 2FA 双因素认证实战:Microsoft Authenticator 应用配置与安全备份指南

1. 为什么你的GitHub账户急需2FA双因素认证? 如果你是一个开发者,GitHub账户里存放的可能远不止几行代码。那里有你的开源项目、私人仓库、协作团队,甚至可能关联着你的求职简历和职业声誉。想象一下,如果某天你突然无法登录&…...

parted实战扩容GPT大磁盘(避坑指南)

1. 线上服务器磁盘告急,我为什么选择了parted? 那天下午,监控告警突然响了,提示线上服务器的根目录 / 使用率超过了95%。我心里咯噔一下,赶紧连上去看。用 df -h 一查,好家伙,根分区 /dev/sda2 …...

快速上手Kook Zimage真实幻想Turbo:5个实用技巧提升你的AI绘画效率

快速上手Kook Zimage真实幻想Turbo:5个实用技巧提升你的AI绘画效率 你是不是已经部署好了Kook Zimage真实幻想Turbo,看着那个简洁的Web界面,输入了几个提示词,也生成了几张图,但总觉得效果差点意思,或者效…...