【stm32单片机基础】按键状态机


文章目录

  • 【stm32单片机基础】按键状态机
  • 前言
  • 一、按键的消抖
  • 二、按键状态机实现
    • 0.状态机模式
    • 1.单个按键检测
    • 2.单个按键实现长按和短按
  • 三、长按和短按测试示例
  • 四 、多按键检测
  • 按键处理经典例程:
  • 总结

前言

在单片机的教学例程中,常使用delay延迟的方式消除按键抖动,而delay延迟的方式使CPU处于空等的状态,不能进行其他任务,直到结束delay延时函数,这种阻塞的方式不利于多任务的情形。本文将使用非阻塞的方式消抖,并采用状态机的模式编写按键处理函数。


一、按键的消抖

按键消抖:通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,因而在闭合及断开的瞬间均伴随有一连串的抖动,按键抖动会引起一次按键被误读多次。


抖动时间的长短由按键的机械特性决定,一般为5ms~10ms

软件消抖:硬件方法将导致系统硬件电路设计复杂化,常采用软件方法进行消抖。

软件方法去抖,即检测出键闭合后执行一个延时程序,5ms~10ms的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。

二、按键状态机实现

0.状态机模式

简单理解为:将一个事件划分为有限个状态,满足相应的条件,在有限个状态之间跳转;可以使用状态图来描述事件处理过程,这种方式使得程序逻辑思路更加清晰严谨。以按键为例,按键检测的过程可以分为三个状态:按键检测状态、按键确认状态、按键释放状态;而在这三个状态之间跳转的条件为当前状态下按键的值。
在单片机中实现状态机最常用的语句便是switch case语句。

【状态机中如何非阻塞消抖】:使用定时器中断,定时每10ms执行一次switch case语句,即两个状态之间跳转的时间为10ms,这样便代替了delay延时。当定时中断发生时,才跳转到中断服务函数执行。

1.单个按键检测

独立按键电路

单个按键的状态转移图如下:
S1状态为按键检测,S2为按键确认,S3为释放按键;状态跳转条件为当前状态下读取到的按键高低电平Key,当Result为1时,表示按键已经成功按下。

单个按键检测的代码实现:

#ifdef SingleKeyEvent typedef enum
{KEY_CHECK = 0,KEY_COMFIRM = 1,KEY_RELEASE = 2
}KEY_STATE;KEY_STATE KeyState =KEY_CHECK;  // 初始化按键状态为检测状态
u8 g_KeyFlag = 0;                 // 按键有效标志,0: 按键值无效; 1:按键值有效
/*** 单个按键检测事件* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;* 状态机使用switch case语句实现状态之间的跳转* */
void Key_Scan(void)
{switch (KeyState){//按键未按下状态,此时判断Key的值case   KEY_CHECK:    if(!Key)   {KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态}break;//按键按下状态:case   KEY_COMFIRM:if(!Key)                     //查看当前Key是否还是0,再次确认是否按下{KeyState =  KEY_RELEASE;  //进入下一个释放状态g_KeyFlag = 1;           //按键有效值为1, 按键确认按下,按下就执行按键任务;        }   else                         //当前Key值为1,确认为抖动,则返回上一个状态{KeyState =  KEY_CHECK;      //返回上一个状态} break;//按键释放状态case  KEY_RELEASE:if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态{ KeyState =  KEY_CHECK;//  g_KeyFlag = 1;        //如果置于此,则在按键释放状态后,才执行按键任务;} break;default: break;}
}#endif

2.单个按键实现长按和短按

单个按键实现短按和长按是个很常用的方式,区别单个按键是否是长按和短按依靠检测按键按下的持续时间。

此处将短按时间T设为 10ms < T <1 s;长按时间的T设置为:T > 1s.

在上面的按键实现基础上可继续实现长按和短按判断,具体程序如下:

代码如下(示例):

#ifdef SingleKey_LongShort_Event
/*** 单个按键检测短按和长按事件* 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;* 状态机使用switch case语句实现状态之间的跳转* lock变量用于判断是否是第一次进行按键确认状态* :按键释放后才执行按键事件*/
void Key_Scan(void)
{static u8 TimeCnt = 0;static u8 lock = 0;switch (KeyState){//按键未按下状态,此时判断Key的值case   KEY_CHECK:    if(!Key)   {KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态 }TimeCnt = 0;                  //计数复位lock = 0;break;case   KEY_COMFIRM:if(!Key)                     //查看当前Key是否还是0,再次确认是否按下{if(!lock)   lock = 1;TimeCnt++;   }   else                       {if(lock)                // 不是第一次进入,  释放按键才执行{/*按键时长判断*/if(TimeCnt > 100)            // 长按 1 s{g_KeyActionFlag = LONG_KEY;TimeCnt = 0;  }else                         // Key值变为了1,说明此处动作为短按{g_KeyActionFlag = SHORT_KEY;          // 短按}/*按键时长判断*/KeyState =  KEY_RELEASE;    // 需要进入按键释放状态 }else                          // 当前Key值为1,确认为抖动,则返回上一个状态{KeyState =  KEY_CHECK;    // 返回上一个状态}} break;case  KEY_RELEASE:if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态{ KeyState =  KEY_CHECK;    }break;default: break;}
}
#endif

按键释放之后,才检测不太合理,做如下调整,长按事件需要达到时长就执行,短按可以在按键释放后执行。

/*** 单个按键检测短按和长按事件* 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;* 状态机使用switch case语句实现状态之间的跳转* lock变量用于判断是否是第一次进行按键确认状态* :长按键事件提前执行,短按键事件释放后才执行*/
void Key_Scan(void)
{static u8 TimeCnt = 0;static u8 lock = 0;switch (KeyState){//按键未按下状态,此时判断Key的值case   KEY_CHECK:    if(!Key)   {KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态 }TimeCnt = 0;                  //计数复位lock = 0;break;case   KEY_COMFIRM:if(!Key)                     //查看当前Key是否还是0,再次确认是否按下{if(!lock)   lock = 1;TimeCnt++;  /*按键时长判断*/if(TimeCnt > 100)            // 长按 1 s{g_KeyActionFlag = LONG_KEY;TimeCnt = 0;  lock = 0;               //重新检查KeyState =  KEY_RELEASE;    // 需要进入按键释放状态}                }   else                       {if(1==lock)                // 不是第一次进入,  释放按键才执行{g_KeyActionFlag = SHORT_KEY;          // 短按KeyState =  KEY_RELEASE;    // 需要进入按键释放状态 }else                          // 当前Key值为1,确认为抖动,则返回上一个状态{KeyState =  KEY_CHECK;    // 返回上一个状态}} break;case  KEY_RELEASE:if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态{ KeyState =  KEY_CHECK;    }break;default: break;}
}

三、长按和短按测试示例

头文件

/*** 使用定时器来轮询Key_Scan()函数,定时节拍为2ms,* 状态转换时间为10ms,即每次进入switch case语句的时间差为10ms* 利用该10ms的间隙跳过按键抖动*/
#ifndef __BUTTON_H
#define __BUTTON_H
#include "stm32f10x.h"//按键对应的IO管脚 KEY1  PA.15
#define KEY_IO_RCC        RCC_APB2Periph_GPIOA
#define KEY_IO_PORT       GPIOA
#define KEY_IO_PIN        GPIO_Pin_15//Key: 1:高电平,按键未按下, 0:低电平,按键按下
#define Key  GPIO_ReadInputDataBit(KEY_IO_PORT,KEY_IO_PIN)typedef enum
{KEY_CHECK = 0,KEY_COMFIRM = 1,KEY_RELEASE = 2
}KEY_STATE;typedef enum
{NULL_KEY = 0,SHORT_KEY =1,LONG_KEY
}KEY_TYPE;//extern u8 g_KeyFlag;
//extern KEY_TYPE g_KeyActionFlag; //单个按键事件
//#define SingleKeyEvent//单个按键实现长按和短按
#define SingleKey_LongShort_Event   1
void Key_Init(void);
void Key_Scan(void);

main函数

KEY_STATE KeyState =KEY_CHECK;  // 初始化按键状态为检测状态
u8 g_KeyFlag = 0;                // 按键有效标志,0: 按键值无效; 1:按键值有效KEY_TYPE g_KeyActionFlag;      //用于区别长按和短按int main()
{Key_Init();Timer_init(19,7199);//10Khz的计数频率,计数到20为2mswhile(1){switch(g_KeyActionFlag){case SHORT_KEY:/*执行短按对应的事件*/g_KeyActionFlag = 0;     //状态回到空break;case LONG_KEY:/*执行长按对应的事件*/g_KeyActionFlag = 0;       //状态回到空default: break;}}
}void TIM3_IRQHandler(void)   //TIM3 每 2ms 中断一次
{   static u8 cnt;if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源 {cnt++;if(cnt>5)           // 每10ms 执行一次按键扫描程序{Key_Scan();cnt = 0;}TIM_ClearITPendingBit(TIM3, TIM_IT_Update);  //清除TIMx的中断待处理位:TIM 中断源 }
}

四 、多按键检测

同样的,多按键也是一样的;
示例如下:
Buttion.h 头文件中只需要把Key做下修改;


#define KEY0  GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)//读取按键0
#define KEY1  GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取按键1
#define WK_UP   GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键2
#define Key  (KEY0 && KEY1 && (!WK_UP))typedef enum
{KEY_CHECK = 0,KEY_COMFIRM = 1,KEY_RELEASE = 2,
}KEY_STATE;//对应的按键值,
typedef enum
{KEY_NULL = 0,KEY_0,KEY_1,KEY_WK_UP
}KEY_VALUE;

对应的状态机中对按键值进行区分即可:

void Key_Scan(void)
{switch (KeyState){//按键未按下状态,此时判断Key的值case   KEY_CHECK:    if(!Key)   {KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态}break;//按键按下状态:case   KEY_COMFIRM:if(!Key)                     //查看当前Key是否还是0,再次确认是否按下{KeyState =  KEY_RELEASE;  //进入下一个释放状态//g_KeyFlag = 1;           //按键有效值为1, 按键确认按下,按下就执行按键任务; // 多按键判断if(0 == KEY0)g_Key = KEY_0;else if(0 == KEY1)g_Key = KEY_1;else if(1 == WK_UP)g_Key = KEY_WK_UP;}   else                         //当前Key值为1,确认为抖动,则返回上一个状态{KeyState =  KEY_CHECK;      //返回上一个状态} break;//按键释放状态case  KEY_RELEASE:if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态{ KeyState =  KEY_CHECK;} break;default: break;}
}

main函数中也是一样,使用switch case 语句执行按键事件即可:

 extern KEY_VALUE g_Key;switch(g_Key){case KEY_0:/* KEY 0 按键事件 */g_Key=KEY_NULL;break;case KEY_1:/* KEY 1 按键事件 */g_Key=KEY_NULL;break;case KEY_WK_UP:/* KEY_WK_UP 按键事件 */g_Key=KEY_NULL;break;default: break;}

按键处理经典例程:

以下是按键处理的经典程序:这是一个论坛中某位博主写的,在csdn这个blog中的也对这种方法进行了归纳:
[独立按键] - 1: 单击,双击,三击以及N击
按键处理的核心就是把driver层和中间层进行拆分,driver层只需要判断长按,短按和无按下这三种状态,中间层再在上面进行处理,即可处理更多种情况;

示例场景:一个按键需要通过长按和短按实现三种功能;短按的时候,在2 秒之内,继续短按,即转变为其他功能,长按的时候则是另外一种功能。对应具体需求就是:首先短按1下开机,开机之后,如果2秒之内继续短按,则短按切换为设置键;超过2秒之后,短按1下,则关机;
这种情况下,程序可以如下:
driver 层不需要更改,只需要对中间层进行处理;

//============================ key.c ===================#define KEY_INPUT           P1.0    // 按键IO#define KEY_STATE_0         0       // 按键状态
#define KEY_STATE_1         1
#define KEY_STATE_2         2
#define KEY_STATE_3         3#define LONG_KEY_TIME       300     // LONG_KEY_TIME*10MS = 3S
#define SINGLE_KEY_TIME     3       // SINGLE_KEY_TIME*10MS = 30MS#define N_KEY    0                  // no click
#define S_KEY    1                  // single click
#define L_KEY    10                 // long pressunsigned char key_driver(void)
{     static unsigned char key_state = 0;         // 按键状态变量static unsigned int key_time = 0;           // 按键计时变量unsigned char key_press, key_return; key_return = N_KEY;                         // 清除 返回按键值key_press = KEY_INPUT;                      // 读取当前键值switch (key_state)     {       case KEY_STATE_0:                       // 按键状态0:判断有无按键按下if (!key_press)                     // 有按键按下{key_time = 0;                   // 清零时间间隔计数key_state = KEY_STATE_1;        // 然后进入 按键状态1}        break;case KEY_STATE_1:                       // 按键状态1:软件消抖(确定按键是否有效,而不是误触)。按键有效的定义:按键持续按下超过设定的消抖时间。if (!key_press)                     {key_time++;                     // 一次10msif(key_time>=SINGLE_KEY_TIME)   // 消抖时间为:SINGLE_KEY_TIME*10ms = 30ms;{key_state = KEY_STATE_2;    // 如果按键时间超过 消抖时间,即判定为按下的按键有效。按键有效包括两种:单击或者长按,进入 按键状态2, 继续判定到底是那种有效按键}}         else key_state = KEY_STATE_0;       // 如果按键时间没有超过,判定为误触,按键无效,返回 按键状态0,继续等待按键break; case KEY_STATE_2:                       // 按键状态2:判定按键有效的种类:是单击,还是长按if(key_press)                       // 如果按键在 设定的长按时间 内释放,则判定为单击{ key_return = S_KEY;            // 返回 有效按键值:单击key_state = KEY_STATE_0;       // 返回 按键状态0,继续等待按键} else{key_time++;                     if(key_time >= LONG_KEY_TIME)   // 如果按键时间超过 设定的长按时间(LONG_KEY_TIME*10ms=200*10ms=2000ms), 则判定为 长按{key_return = L_KEY;         // 返回 有效键值值:长按key_state = KEY_STATE_3;    // 去状态3,等待按键释放}}break;case KEY_STATE_3:                         // 等待按键释放if (key_press) {key_state = KEY_STATE_0;          // 按键释放后,进入 按键状态0 ,进行下一次按键的判定}         break; default:                                // 特殊情况:key_state是其他值得情况,清零key_state。这种情况一般出现在 没有初始化key_state,第一次执行这个函数的时候key_state = KEY_STATE_0;break;}return key_return;                          // 返回 按键值
}

中间层接收到driver层的返回数据,再根据功能需求编写处理函数;

void key_event_handle(void)
{static uint8_t long_click_cnt = 0;static uint8_t short_click_cnt = 0;volatile static uint32_t prev_cnt, cur_cnt = 0;switch(key_return){case S_KEY:    // 短按 开关{/* turn on */if(power_state != ON){prev_cnt = get_tim3_systick();power_state = ON;printf("turn on!! \r\n"); }           else{cur_cnt = get_tim3_systick();         // 在定时器3中获取最新计数值if((cur_cnt - prev_cnt) < 2000)          // 两次单击时间小于2s{short_click_cnt++;short_click_cnt = short_click_cnt%2;/* 根据按键的次数依次执行不同的动作 */switch(short_click_cnt){case 0:printf("enter case 0 \r\n");/*此处可调用需要执行的函数*/  break;case 1:printf("enter case 1 \r\n");/*此处可调用需要执行的函数*/ break;default:break;}       }else     // 超过2秒,turn offs{printf("turn off \r\n");power_state = OFF;}/* 更新计数值 */prev_cnt = cur_cnt;}}break;case L_KEY:         {if(power_state){long_click_cnt++;if(long_click_cnt > 50)      // 500ms 进行一次调用{ long_click_cnt = 0;printf("enter long press mode\r\n");/*此处调用长按处理函数*/}}}break;default:break;}
}

总结

以上便是对按键状态机程序的总结,对长按和短按的判断实现……

【stm32单片机基础】按键状态机实现长按和短按相关推荐

  1. stm32单片机基础知识总结(三)

    今天天津下了好大的雪,我想这应该是2022年的第一场雪.虽然已经进入了春天,但是能下这么大的雪,对于大多数北方人来说,并不会感到意外.大家都知道有个词叫做"倒春寒",就是说春天来临 ...

  2. STM32单片机基础之蜂鸣器

    说明: 这里说明一下,STM32有很多文件,我这里上传的只是部分配置文件,不是所有的文件. 代码: main.c 文件#include "system.h" #include &q ...

  3. STM32单片机基础19——使用SDMMC接口读写SD卡数据

    本篇详细的记录了如何使用STM32CubeMX配置STM32L431RCT6的硬件SDMMC外设读取SD卡数据. 1. 准备工作 硬件准备 开发板 首先需要准备一个开发板,这里我准备的是STM32L4 ...

  4. STM32单片机基础12——使用通用定时器产生PWM驱动蜂鸣器

    本篇详细的记录了如何使用STM32CubeMX配置STM32L431RCT6的通用定时器外设,产生PWM驱动无源蜂鸣器. 1. 准备工作 硬件准备 开发板 首先需要准备一个开发板,这里我准备的是STM ...

  5. STM32单片机基础17——使用硬件SPI驱动TFT-LCD(ST7789)

    本篇详细的记录了如何使用STM32CubeMX配置STM32L431RCT6的硬件SPI外设与ST7789通信,驱动16bit TFT-LCD 屏幕. 0. 前言 学习 SPI 外设驱动LCD屏幕没有 ...

  6. 基于STM32单片机的远程智能浇花花盆GSM短信浇水补光方案原理图程序设计

    硬件电路的设计 (末尾附文件) 3.1系统的功能分析及体系结构设计 3.1.1系统功能分析 本设计由STM32F103C8T6单片机核心电路+温湿度传感器DHT11电路+LCD1602液晶显示电路+继 ...

  7. STM32 单片机 程序 指纹 门禁锁 RC522 射频卡 发短信 报警

    功能按这个做: 功能设计如下: 硬件:单片机+显示器+指纹模块+继电器+按键+振动模块+发短信模块+人体红外+RC522射频卡刷卡模块+震动模块 1 开机后,检测指纹模块是否存在,如不存在,屏幕会有相 ...

  8. 【ESP32】按键驱动,长按、短按,可设置多个按键

    文章目录 一.ESP32 二.实现 1.源文件 2.头文件 三.调用 总结 一.ESP32 之后的项目要用到ESP32,对按键驱动进行移植 二.实现 1.源文件 支持短按.长按和多个按键. 我在这里设 ...

  9. STM32 -- 实现按键的长按与短按检测(其他单片机可移植)

    一 前言 今天在逛博客的时候,偶然看到了一篇关于按键检测的文章,兴趣使然自己尝试了一番,写了一些代码去验证自己的思路,通过验证完美实现了长按和短按检测,后续有时间的我也会更新一下连按检测等 二 思路 ...

  10. 51单片机——按键,定时器消抖,短按,长按,三行代码,状态机

    0. 序 今天太晚了,先不写介绍了,代码中注释非常详细,很方便移植 双击,三击等操作,可以在状态机的短按操作中做更改. 组合键需要重新构思,在扫描中确定被按下和被抬起的是哪个键. 1. 分析 (1)按 ...

最新文章

  1. Libre OJ 「BalticOI 2013」非回文数 数位dp
  2. python拨号_python 拨号代码(win10 系统亲测有效)
  3. 禁止访问 共享计算机,win7如何禁止局域网用户访问电脑
  4. 80后偷偷“变老”的20种表现
  5. 彭博社“机器学习基础”视频教程已经全部搬到腾讯视频
  6. Flutter AnimatedSwitcher 实现优美的图片切换动画
  7. 如何设置mysql远程访问
  8. Go bufio.Reader 结构+源码详解
  9. Android Webview实现文件下载功能
  10. android studio读写txt,Android Studio从.txt文件读取/写入,保存路径?
  11. 计算机开机切换用户界面,win7开机登录界面怎么设置win7更换开机画面壁纸解决办法...
  12. 超分辨网络SRCNN的Pytorch实现
  13. C语言short精解
  14. 阿里的世界版图——“风清扬”的全球梦
  15. linux克隆机器IP问题
  16. 介绍中国传统节日的网页html,介绍中国传统节日
  17. 下月起你的到手工资将有重大变化! 图解2019年个税怎么算
  18. L9110 L9110S SOP8 马达驱动 IC 芯片 H桥全桥 贴片
  19. CC2540\CC2541 资料整理
  20. 对于微信支付宝支付的总结

热门文章

  1. react 函数组件使用了 hook 后闪屏问题的分析与解决
  2. python 身高预测
  3. 浏览器与HTTP网络协议缓存原理分析 转自网界网:http://news.cnw.com.cn/news-china/htm2015/20151027_322909.shtml
  4. drf接收、处理上传图片并写入文件
  5. 女性三围--表单只能输入数字和英文输入下的逗号
  6. VB 提示框MsgBox用法
  7. 对《骨骼运动变换的数学计算过程详解》一文的理解
  8. 怎么设置html页面背景图片大小怎么设置,css怎么设置背景图片大小?
  9. Attention机制原理
  10. Xcode6的新特性、