在单片机的应用中,利用按键实现与用户的交互功能是相当常见的,同时按键的检测也是很讲究的,众所周知,在有键按下后,数据线上的信号出现一段时间的抖动,然后为低,当按键释放时,信号抖动一段时间后变高,然而这段抖动时间要维持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 函数,包含按键状态机检测函数等;
用枚举定义状态机的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
********************************************************/
/* Includes ------------------------------------------------------------------*/
#include
/* 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)
{
//切换到状态2
STA_Machine.ucSTA_Machine_Status = STA2_KEY_DownShake;
}
break;
}
//按下抖动
case STA2_KEY_DownShake:
{
if(KEY2_State == 0)
{
//切换到状态3
STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;
}
else
{
//如果检测到高电平说明是抖动,切回到状态1
STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;
}
break;
}
//按键按下
case STA3_KEY_Down:
{
if(KEY2_State == 1)
{
//切换到状态4
STA_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
{
//否则判断为抖动,切回到状态3
STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;
}
break;
}
default:
//默认情况都切换到状态1
STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;
break;
}
//状态机扫描定时器清零,开始下一次扫描
STA_Machine.ucSTA_Machine_Scan_Timer = 0;
}
}
/********************************************************
End Of File
********************************************************/
/* Includes ------------------------------------------------------------------*/
#include
/* 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)
{
//切换到状态2
STA_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)
{
//切换到状态3
STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;
//长按定时器清0,开始计算长按时间
STA_Machine.usKEY2_Press_Timer = 0;
}
else
{
//如果检测到高电平说明是抖动,切回到状态1
STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;
}
break;
}
//按键按下
case STA3_KEY_Down:
{
if(KEY2_State == 1)
{
//切换到状态4
STA_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;
//因为已经判断为长按,所以不是双击操作,把单击缓存清0
if(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:
//默认情况都切换到状态1
STA_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
********************************************************/
定时器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函数先进行系统初始化,主要是引脚和定时器的初始化,然后在while循环里调用按键2的按键扫描函数不断扫描即可
/*
* @name main
* @brief 主函数
* @param void
* @retval int
*/
int main(void)
{
//系统初始化
Hradware.Sys_Init();
//系统主循环
while(1)
{
//按键检测
//KEY1.KEY_Detect();
KEY2.KEY_Detect();
}
}