按键收集单击,双击和长按
按键收集单击,双击和长按
引言
在我们生活中, 按键是必不可少的, 不同的电器, 有不同的按键, 但是按键总有不够用的时候, 那么给与一个按键赋予不同的功能,就必不可少了. 一个按键可以通过按下的时间长短和频次, 来定义其类型。
一次按键收集, 都是在一个按键收集周期的, 比如500毫秒内, 我要收集一个按键类型, 在500毫秒内,按键按下一次按键, 那么到达500毫秒后, 我们就宣布答案, 此次按键是单击。
同理, 在500毫秒内, 如果我们按下了, 两次按键,那么就是双击, 以此类推。
那长按呢? 长按就是, 在500毫秒内, 我们按下了按键,并且500ms时间到时, 按键还未松开, 就代表着长按。
本博客最终修改项目链接下载:
点击下载
https://wwyz.lanzoul.com/itXC327v9dra
按键收集模型

单片机实现方法
因为按键, 需要每时每刻的去检测, 是否按下, 如果使用while阻塞循环, 则会大大影响程序效率, 所以我们使用外部中断去检测中断。
按键事件触发检测
按键按下的瞬间, 单片机的io口, 就会被从高电平拉到低电平, 然后单片机io口就会检测到, 从而触发下降沿中断。
此时,就代表着按键事件已经触发了。
按键类型判断
按键事件触发,仅仅代表着我们按下了按键, 但是我们想区分单击,双击和长按, 那么我们就得从按键按下开始计算,直到按键计算周期结束,这一段时间我们定义成 500毫秒。
所以, 我们在按键事件触发后,我们需要启动定时器,计时500ms, 然后500ms时间到后, 判断按键按下的次数, 还有按键此时是否仍然被按下.
在这 500毫秒内, 我们重复按下按键, 就需要重复的检测按键按下的事件, 然后计入按键次数内.
所以我们的处理模型是
代码实现
按键中断
按键中断初始化
我们初始化io端口PB1,配置下降沿中断
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
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的时候,进行按键类型检测
按键类型判断
当到达500ms后, 我们就可以通过判断按键次数, 区分单双击, 单击的情况下,通过判断500ms时,io口的状态,从而判断单击还是长按。
单击和长按区分
单击和长按, 按键次数 button_count 都是1, 不同的是,500毫秒之后,单击按键类型, 已经松开, 长按,则是按下状态, 所以我们通过读取检测500ms时间到时, io的状态, 就可以区分长按和单击了。
长按, io口是低电平, 单击,io是高电平, (我们是下降沿触发, 按键一端连io口,一端连 地, io口输出状态是, 推挽输出)
button_mode = 1代表 单击 ,
button_mode = 3代表 长按

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, 代表按键双击
定时器扫描函数代码
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 的数值。

调用相关现象
我们选用PA0, PA1, PA2, 来分别代表 单击,双击和长按的小灯。
我们在检测到 button_mode 的数值改变的时候, 就赋值对应的小灯亮灭,这样我们就可以看到现象了

初始化PA0,PA1,PA2
设置这三个io口分别为推挽输出模式
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, 代表着让灯灭。这样我们就可以灵活的控制小灯亮灭了。

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 加鼠标左键,快速跳转)

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主函数调用
初始化,按键和定时器, 还有调试的小灯, 我们根据按键模式, 控制对应的小灯亮灭, 观察现象
![]()
_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
点击品字,然后修改工程名字
3.我们用到main函数, 所以有User用户文件夹
我们用到按键检测, 所以用到 Key_model按键模型
我们用到系统的延时函数, 所以用到System系统文件夹
我们用到定时器,所以用到Time定时器文件夹
User
Key_model
System
Time
4.具体文件添加方式, 看最小例程构建,从第九步开始
我们User文件夹下, 添加
main.c
led.c
led.h
Key_model文件夹下添加
key.c
key.h

System文件夹下添加
delay.c
delay.h
sys.h

Time文件夹下添加
TIM4.c
TIM4.h

5.记得包含对应的文件夹路径
6.按键收集模型代码快速复制
各文件对应的代码
跳转复制
- 构建完成如图
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 注解,完成依赖注入的操作。简单来说…...

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

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...

aardio 自动识别验证码输入
技术尝试 上周在发学习日志时有网友提议“在网页上识别验证码”,于是尝试整合图像识别与网页自动化技术,完成了这套模拟登录流程。核心思路是:截图验证码→OCR识别→自动填充表单→提交并验证结果。 代码在这里 import soImage; import we…...