按键检测(状态机)

传统的按键检测

在单片机的应用中,利用按键实现与用户的交互功能是相当常见的,同时按键的检测也是很讲究的,众所周知,在有键按下后,数据线上的信号出现一段时间的抖动,然后为低,当按键释放时,信号抖动一段时间后变高,然而这段抖动时间要维持10ms~50ms,这个与按键本身的材质有一定的关系,在这个范围内基本上都可以确定的。如果按键检测的不好,单片机的运行效率将会大打折扣,严重影响到系统的性能,导致系统的运行出现异常,在教科书中,我们见到的按键处理程序都是以下这样的结构:

if(KEY_IO != 0xFF)      //检测到有按键按下
{DelayNms (20);     //延时20毫秒(严重影响单片机的运行效率)if (KEY_IO !=0xFF)    //确认按键按下{switch (KEY_IO){case 0xFE: KeyValue=1 ; break;case 0xFD: KeyValue=2 ; break;default: KeyValue=0 ; break;}}
}

弊端:

像这样的程序经常出现在大学的教科书中,在按键的扫描中,单片机的资源全部用来做按键的扫描,特别是当中的延时程序,对单片机来说,这个一个漫长的过程。例如,我们需要用动态扫描数码管来做一个电子时钟,如果在按键持续按下的过程中,由于延时程序对单片机资源的占用,单片机这个时候就不能做动态扫描,数码管的显示就会有问题;

除非当前程序搭载了实时系统,一旦当前任务要进行延时操作,系统会自动进行任务调度,执行其他任务,当之前的任务延时完毕,系统会自动执行之前的任务。遗憾的是传统8051系列单片机不推荐搭载实时系统的,毕竟其资源有限,而且又增加额外的成本,比如搭载ucos实时系统,传统的8051系列单片机完全不能满足该系统的要求,必须拓展外部存储器才能满足,这样就间接上增加了成本,同时ucos用于商业上要收费的,成本大大地增加了。因此当没有搭载实时系统做按键检测使用软件延时是不现实的,严重影响性能

这样的教科书的按键处理程序是不实用的,在实际应用中是不可取的。所以这里介绍采用“状态机”的思想进行检测按键,不仅可以正确检测到按键,而且不会影响其他周边外设器件的运作。

有限状态机思想

​ 有限状态机是一种概念思想,把复杂的控制逻辑分解成有限个稳定状态,组成闭环系统,通过事件触发,让状态机按设定的顺序处理事务。

​ 状态机是软件编程中的一个重要概念,比这个概念更里要的定对它的灵活应用。在一个思路清晰而且高效的程序中,必然有状态机的身影浮现。

​ 比如说一个按键命令解析程序,就可以被看做状态机:本来在A状态下,触发一个按键后切换到了B状态:再触发另一个键后切换到C状态,或者返回到A状态。这就是最简单的按键状态机例子。实际的按键解析程序会比这更复杂些,但这不影响我们对状态机的认识。

​ 进一步看,击键动作本身也可以看做一个状态机。一个细小的击键动作包含了:按下、抖动、释放等状态。其实状态机思想不单只用在按键方面,数码管显示动态扫描、LED亮灭都是存在状态机的思想如亮与灭的状态。

​ 使用状态机思想去进行单片机编程,比较通用的方法就是用swtich 的选择性分支语句来进行状态跳转,既然可以 switch 来判断,那么使用 if 同样可以,但是使用 switch 来判断状态可以使代码更加清晰。

按键动作示意图

状态图

说明:

整个状态机使用定时器来驱动,每隔10ms进入一次状态机进行判断,检测消抖的时间通常也是10ms;多任务时可以避免其他任务占用CPU 过多的时间;

状态1:按键处于弹起状态,为高电平,定时器每隔10ms扫描一次,如果检测到按键的IO口为低电平了,则切换到状态2

状态2:按键抖动检测,检测到IO口低电平从状态1切换到状态2后,下一个10ms进来如果检测到IO口变回了高电平,说明低电平是由抖动引起的,按键没有被完全按下,状态2切回到状态1;如果检测到IO口任然是低电平,说明按键被按下,状态2切换到状态3

状态3:此状态下按键已确认被按下,IO口会一直是低电平,如果长按,也一直是处于这个状态3;这个状态可以做一些按键动作的检测,如长按、双击;该状态下如果检测到IO口为高电平,则切换到状态4

状态4:进行弹起抖动检测,从状态3切到该状态时,在下一个10ms如果检测到IO口又变为低电平了,则说明上一步高电平是有抖动引起的,再将状态切回到状态3;如果检测到IO口继续为高电平,说明按键松开了,将状态切换到状态1

又开始下一个循环……

程序

实现效果

按键2单击指示灯电平翻转,长按2秒则闪一下,双击则闪三下

按键检测程序思路

单击与双击:按键第一次检测到单击时,将单击状态缓存,同时启动双击定时器,超过一定时间,比如 200ms,认定按键为单击;如果没超过 200ms,又检测到了单击,认定按键为双击。

长按:检测到按键按下后,启动长按定时器,过了一段时间,比如 2s,按键依然为按下状态 ,认定按键为长按。

文件结构

main.c -> 主函数文件,包含 main 函数等;

Public.c -> 公共函数文件,包含 Delay 延时函数等;

Sys_init -> 系统初始化函数,包含 GPIO 初始化函数等;

LED.c -> LED 外设函数,包含 LED 打开、关闭函数等;

Timer0.c -> 定时器函数,包含定时器初始化,中断函数等;

KEY1.c -> 按键 1 函数,包含按键检测,中断函数等;

KEY2.c -> 按键 2 函数,包含按键状态机检测函数等;

KEY2.h:

用枚举定义状态机的4种状态,用结构体定义扫描定时器,将结构体变量声明为外部可调用

#ifndef __KEY2_H_
#define __KEY2_H_//定义状体机使用的枚举类型
typedef enum
{STA1_KEY_Up             = (uint8_t)0x01,    //按键弹起STA2_KEY_DownShake      = (uint8_t)0x02,    //按下抖动STA3_KEY_Down           = (uint8_t)0x03,    //按键按下STA4_KEY_UpShake        = (uint8_t)0x04     //弹起抖动
}STA_Machine_Status_t;//定义结构体类型
typedef struct
{STA_Machine_Status_t  ucSTA_Machine_Status;       //状态机状态uint16_t volatile     ucSTA_Machine_Scan_Timer;   //状态机扫描定时器uint16_t volatile     usKEY2_Double_Click_Timer;  //KEY2双击定时器uint16_t volatile     usKEY2_Press_Timer;         //KEY2长按定时器
}STA_Machine_t;/* extern variables-----------------------------------------------------------*/
extern KEY_t          KEY2;
extern STA_Machine_t  STA_Machine;
/* extern function prototypes-------------------------------------------------*/ #endif
/********************************************************End Of File
********************************************************/

KEY2.c:

1.先搭好状态机的框架:
/* Includes ------------------------------------------------------------------*/
#include <main.h>/* Private define-------------------------------------------------------------*/
#define KEY2_State P33#define Set_Press_TIME          TIMER_2S            //设置长按时间
#define Set_Double_Click_TIME   TIMER_200MS         //设置双击时间/* Private variables----------------------------------------------------------*/
static uint8_t Click_Buf = FALSE;                 //单击状态缓存
static void KEY_Detect();/* Public variables-----------------------------------------------------------*/
KEY_t KEY2 = {FALSE,FALSE,FALSE,FALSE,KEY_Detect};     //标志位初始化
STA_Machine_t  STA_Machine = {STA1_KEY_Up,0,0,0};      //状态和定时器初始化/* Private function prototypes------------------------------------------------*//*
* @name   KEY_Detect
* @brief  按键2检测(状态机)
* @param  None
* @retval None
*/
static void KEY_Detect()
{//状态机扫描定时器计时大于或等于10ms,进入一次状态机if(STA_Machine.ucSTA_Machine_Scan_Timer >= TIMER_10MS){switch (STA_Machine.ucSTA_Machine_Status){//按键弹起case STA1_KEY_Up:{if(KEY2_State == 0)                                          {//切换到状态2STA_Machine.ucSTA_Machine_Status = STA2_KEY_DownShake;  }break;}//按下抖动case STA2_KEY_DownShake:{if(KEY2_State == 0){//切换到状态3STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;}else{//如果检测到高电平说明是抖动,切回到状态1STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             }break;}//按键按下case STA3_KEY_Down:{if(KEY2_State == 1){//切换到状态4STA_Machine.ucSTA_Machine_Status = STA4_KEY_UpShake;      }break;}//弹起抖动case STA4_KEY_UpShake:{if(KEY2_State == 1){//切换到状态1,完成一次按键动作STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;}else{//否则判断为抖动,切回到状态3STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;}break;}default://默认情况都切换到状态1STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             break;}//状态机扫描定时器清零,开始下一次扫描STA_Machine.ucSTA_Machine_Scan_Timer = 0;}
}
/********************************************************End Of File
********************************************************/
2.完成按键单击、双击、长按的检测
/* Includes ------------------------------------------------------------------*/
#include <main.h>/* Private define-------------------------------------------------------------*/
#define KEY2_State P33#define Set_Press_TIME          TIMER_2S            //设置长按时间
#define Set_Double_Click_TIME   TIMER_200MS         //设置双击时间/* Private variables----------------------------------------------------------*/
static uint8_t Click_Buf = FALSE;                 //单击状态缓存
static void KEY_Detect();/* Public variables-----------------------------------------------------------*/
KEY_t KEY2 = {FALSE,FALSE,FALSE,FALSE,KEY_Detect};     //标志位初始化
STA_Machine_t  STA_Machine = {STA1_KEY_Up,0,0,0};      //状态和定时器初始化/* Private function prototypes------------------------------------------------*//*
* @name   KEY_Detect
* @brief  按键2检测(状态机)
* @param  None
* @retval None
*/
static void KEY_Detect()
{//状态机扫描定时器计时大于或等于10ms,进入一次状态机if(STA_Machine.ucSTA_Machine_Scan_Timer >= TIMER_10MS){switch (STA_Machine.ucSTA_Machine_Status){//按键弹起case STA1_KEY_Up:{if(KEY2_State == 0)                                          {//切换到状态2STA_Machine.ucSTA_Machine_Status = STA2_KEY_DownShake;  }else{//按键没被按下,则判断是否有单击缓存if(Click_Buf == TRUE){//如果双击定时器大于200ms后,说明没在规定时间内进行双击,判断上一次按下是单击操作/*如果双击定时器小于200ms,则下面判断不成立,说明还有时间完成双击操作,再按下按键后就从状体1到状态2再到状态3,状态3里判断是双击操作*/if(STA_Machine.usKEY2_Double_Click_Timer >= Set_Double_Click_TIME){KEY2.KEY_Flag = TRUE;KEY2.Click    = TRUE;//清除单击缓存Click_Buf = FALSE;}}}break;}//按下抖动case STA2_KEY_DownShake:{if(KEY2_State == 0){//切换到状态3STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;//长按定时器清0,开始计算长按时间STA_Machine.usKEY2_Press_Timer = 0;}else{//如果检测到高电平说明是抖动,切回到状态1STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             }break;}//按键按下case STA3_KEY_Down:{if(KEY2_State == 1){//切换到状态4STA_Machine.ucSTA_Machine_Status = STA4_KEY_UpShake;//不是长按操作,则判断是不是双击操作if(KEY2.Press == FALSE){//双击检测//有单击缓存,说明前面已经单击一次,这次就判断为双击操作if(Click_Buf == TRUE){KEY2.KEY_Flag     = TRUE;KEY2.Double_Click = TRUE;//清除单击缓存,为下一次双击准备Click_Buf = FALSE;}else{//没有单击缓存,说明这是双击的第一次点击,则进行缓存Click_Buf = TRUE;/*双击定时器清零,开始计算双击时间,如果在双击时间内,再次进入到状态3,则判断是双击操作,如果在状态1里超时了,则状态1里检测为单击*/STA_Machine.usKEY2_Double_Click_Timer = 0;}}   }else{//长按检测if(KEY2.Press == FALSE){/*如果长按定时器超过两秒,认为是长按,进入判断体内将长按标志位置TRUE;如果在两秒内松开了按键则会判断是不是双击,去执行判断双击的情况*/if(STA_Machine.usKEY2_Press_Timer >= Set_Press_TIME){STA_Machine.ucSTA_Machine_Status = STA4_KEY_UpShake;/*为什么要切换到状态4?解释:因为这里将KEY2.Press置TRUE后,后面执行一次按键动作,指示灯闪一下,随后KEY2.Press被清零;下一个10ms再进入函数时,状态位还是3,还是进入这里执行,KEY2.Press又会被置成TRUE,所以一直按住不放的话,指示灯就会一直闪,而不是闪一下的情况而在这里第一次进来后将状态位切换到4,KEY2.Press还是会置TRUE,后面指示灯闪一下,KEY2.Press被清零,下一个10ms进来后会直接跳到状态4,所以KEY2.Press不会再次被置TRUE,后面指示灯动作不会再执行,所以只闪一下*/KEY2.KEY_Flag = TRUE;KEY2.Press    = TRUE;//因为已经判断为长按,所以不是双击操作,把单击缓存清0if(Click_Buf == TRUE){Click_Buf = FALSE;}}}}break;}//弹起抖动case STA4_KEY_UpShake:{if(KEY2_State == 1){//切换到状态1,完成一次按键动作STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;}/*如果KEY2_State == 0,不跳回状态3,避免反复检测长按的情况*/break;}default://默认情况都切换到状态1STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             break;}/****执行按键动作,用于检测按键效果****///单击动作 -> 指示灯翻转if(KEY2.KEY_Flag == TRUE){if(KEY2.Click == TRUE){Run_LED.Run_LED_Flip();}//长按动作 -> 指示灯闪一下if(KEY2.Press == TRUE){Run_LED.Run_LED_Flip();Public.Delay_ms(100);Run_LED.Run_LED_Flip();}//双击动作 -> 指示灯闪三下if(KEY2.Double_Click == TRUE){Run_LED.Run_LED_Flip();Public.Delay_ms(100);Run_LED.Run_LED_Flip();Public.Delay_ms(100);Run_LED.Run_LED_Flip();Public.Delay_ms(100);Run_LED.Run_LED_Flip();Public.Delay_ms(100);Run_LED.Run_LED_Flip();Public.Delay_ms(100);Run_LED.Run_LED_Flip();}}//按键状体位清零,为下一次按下准备KEY2.Click        = FALSE;KEY2.Press        = FALSE;KEY2.Double_Click = FALSE;//状态机扫描定时器清零,开始下一次扫描STA_Machine.ucSTA_Machine_Scan_Timer = 0;}
}
/********************************************************End Of File
********************************************************/

Timer0.c:

定时器0中断处理函数中让状态机里用到的3个定时器每隔5ms加1

/*
* @name   Timer0_isr
* @brief  定时器0中断处理函数(5ms进入一次)
* @param  None
* @retval None
*/
void Timer0_isr() interrupt 1
{Timer0.msMCU_Timer0_Value++;if(Timer0.msMCU_Timer0_Value >= TIMER_500MS) //计时到500ms{Timer0.msMCU_Timer0_Value = 0;//Run_LED.Run_LED_Flip();                 //运行指示灯翻转}STA_Machine.ucSTA_Machine_Scan_Timer++;         //状态机扫描定时器STA_Machine.usKEY2_Double_Click_Timer++;        //双击定时器STA_Machine.usKEY2_Press_Timer++;              //长按定时器
}

main.c:

main函数先进行系统初始化,主要是引脚和定时器的初始化,然后在while循环里调用按键2的按键扫描函数不断扫描即可

/** @name   main* @brief  主函数* @param  void   * @retval int
*/
int main(void)
{   //系统初始化Hradware.Sys_Init();//系统主循环while(1){//按键检测//KEY1.KEY_Detect();KEY2.KEY_Detect();}
}

STC15单片机-按键检测单击、双击和长按(状态机)相关推荐

  1. PIC单片机 按键检测识别

    目录 按键和PIC单片机 一.按键 二.按键的物理连接与检测 三.PIC16F18854单片机 按键检测及显示实验 按键排布 预期效果 总体流程 中断服务程序 PIC汇编程序 按键和PIC单片机 一. ...

  2. (三)单片机按键检测

    (三)单片机按键检测 一.总结 好了,单片机这个东西呢,无非是输入和输出,所以经过前面两个led灯,实际上单片机就学完了一半了

  3. 单片机按键扫描:分别实现长按两秒运行一种功能,短按松开后运行另一种功能

    单片机按键扫描: 分别实现长按两秒运行一种功能,短按松开后运行另一种功能. 代码如下: #define KEY1_PRES 1 //KEY1短按松开生效 #define KEY1_LONG_PRES ...

  4. STM32独立按键实现单击双击长按功能

    目录 前言 一.按键功能定义 二.使用步骤 1.按键初始化 2.按键扫描函数(重点) 总结 前言 在使用STM32或其他单片机开发项目时,经常需要用到独立按键进行控制. 通常一个独立按键需要使用一个I ...

  5. 独立按键检测短按、长按,松手后响应操作

    背景 有项目使用独立按键检测,短按.长按.根据使用效果,发现松手后,也就是按键弹起后响应操作比较好操作. 记得之前,博主写过一篇关于按键的检测的文章,但是过于复杂了.可能很难懂,这里就简单一点,只处理 ...

  6. c语言按键松手检测,51单片机按键检测的方法

    本文旨在介绍单片机入门的基础知识,为初接触或即将接触单片的新手提供一个入门指导.本文章会陆续推出,隔几天一个章节.希望对广大即将接触单片机的人有所帮助,如果有错误欢迎回帖指出与本人交流或加入QQ群14 ...

  7. 按键检测 短按、长按

    #define KEY_UP  1 #define KEY_DOWN 2 // u8 key_type=0;      //按键类型 u8 f_longkey=0;  //长按按键有效 void Ke ...

  8. vba循环通过键盘某个按键按下退出循环_51单片机按键检测--独立按键与矩阵键盘...

    按键可以说是51单片机项目开发的一个重要组件了,它是作为51单片机IO口输入的一种重要方式.我们可以通过按键控制单片机执行相应的程序,得到我们想要得效果.51单片机的按键输入主要有两种,一种是独立按键 ...

  9. 单片机按键检测程序c语言,单片机检测按键短击,连击c程序

    /****************************************************************************/ //文 件 名:key.c //功 能:短 ...

最新文章

  1. MQTT的学习研究(五) MQTT moquette 的 Blocking API 发布消息服务端使用
  2. Drupal 7.31 SQL注射分析POC
  3. 最新执业范围字典表_执业药师考后审核不符,可做二次说明
  4. Nodejs扩展,实现消息弹窗
  5. ubuntu vim命令
  6. IDEA这样配置注释模板,让你高出一个逼格!!
  7. 虚拟机VMware搭建代码环境
  8. python中对matlab的支持库
  9. 【二分答案】【Heap-Dijkstra】bzoj2709 [Violet 1]迷宫花园
  10. Lync 小技巧-52-Lync 2013-不加域-客户端-2-导入-证书-信任链
  11. windows批处理脚本导入注册表不弹出确认框
  12. Appium下载及环境搭建
  13. ajax 与ssh结合,基于AJAX和SSH集成框架的国有资产管理系统
  14. MATLAB迷宫算法 自动生成迷宫并可视化寻找出路
  15. 面试题:为什么说 Mybatis 是半自动ORM 映射工具?它与全自动的区别在哪里?
  16. 高薪程序员面试题精讲系列81之说一下SQL查询语句的执行顺序详解-作用篇(上)
  17. https访问出现无法访问此网站
  18. poi导入数据工具类,直接复制使用,有详细注释
  19. EFM32芯片被锁解决方法
  20. 关于电视机中DTV码流对android的Timer的影响

热门文章

  1. Ubuntu 16.04 + Nvidia 显卡驱动 + Cuda 8.0 (问题总结 + 解决方案)
  2. java执行jar包出错:Unable to access jarfile
  3. 编程狂人|金融业分布式数据库选型及HTAP场景实践
  4. 基于DOM的骨架屏自动生成方案
  5. 有N个学生,学号,姓名,性别,四门课成绩,键盘上输入信息,输出总平均分成绩最高
  6. 计网-配置静态路由与动态路由
  7. matlab光通信,光纤通信课程设计matlab仿真源代码.doc
  8. UEFI+GPT、Legacy+MBR引导模式介绍 引导修复教
  9. 驾照科目三_模拟夜间灯光
  10. 宠喵club商品页皇家商品