基于状态机的51单片机按键检测与消抖思路

按键从未按下到按下,再到弹起,可以视为一个有4个状态的有限状态机。
分别是未按下、前沿抖动、按下、后沿抖动。
即按键只在这4个状态间进行迁移,而且是当条件满足时按一定的顺序进行迁移:
未按下–(条件1)->前沿抖动–(条件2)->按下–(条件3)->后沿抖动–(条件4)->未按下
当且仅当条件成立时,才进行状态的迁移,当条件不成立时,状态机会保持当前状态不变。

状态机有如下性质

状态机会在条件满足时发生状态转移,由于每个状态会持续一段时间,而我们需要按键是理想的,即我们需要一个瞬间的按键事件,而不是连续的按键事件。
总结:
(1)当状态转移条件成立时,要进行状态的迁移。
(2)状态转移是瞬间的,所以我们把某个状态转移当成按键来用。
(3)未进行状态转移时,状态机会保持之前的状态。

按键状态机发生状态迁移的条件

条件发生前的状态 条件 需要迁移到的状态
未按下 前沿抖动开始 前沿抖动
前沿抖动 前沿抖动结束 按下
按下 后沿抖动开始 后沿抖动
后沿抖动 后沿抖动结束 未按下

如何准确判断条件是否发生并进行状态迁移

即如何对状态机的迁移条件进行准确判定。
注意:按键的初始状态是未按下

//在定义时将按键状态初始化为未按下
unsigned char KEY_Status = NOPRESS;
(1)条件1:前沿抖动开始 从未按下状态到前沿抖动开始这一事件发生,电平为由1变为0 即在未按下状态下,突然检测到低电平,就说明迁移条件成立了,立即把状态迁移到前沿抖动,或执行其他操作。
//KEY_Status == NOPRESS && KEY_Value == 0,等价于条件1成立
if (NOPRESS ==  KEY_Status && 0 == KEY_Value)
{
KEY_Status = PRESHAKE; //执行状态迁移
timer(); //设置一个10ms定时器,用定时器溢出事件来模拟前沿抖动结束事件。
}
(2)条件2:前沿抖动结束 从前沿抖动状态到前沿抖动结束这一事件发生,有大约10毫秒的时间,虽然没有具体的事件,但可以用定时器中断来模拟前沿抖动结束事件。在前沿抖动的状态下,一旦定时器中断,就说明迁移条件成立了,立即把状态迁移至按下,或执行其他操作。
//若PRESHAKE == KEY_Status && 1 == TF1 则条件2成立
if (PRESHAKE == KEY_Status && 1 == TF1)
{
KEY_Status = PRESS; //执行状态迁移
TF = 0; //清除溢出标志
TR1 = 0; //关闭定时器
}
(3)条件3:后沿抖动开始 从按下状态到后沿抖动开始这一事件发生,电平由0变1,即只要在按下状态中检测到电平为1,就认为迁移条件成立了,立即把状态迁移至后沿抖动。
//KEY_Status == PRESS && KEY_Value == 1则条件3成立
if (PRESS == KEY_Status && 1 == KEY_Value)
{
KEY_Status = TAILSHAKE; //执行状态迁移
n++; //执行想要的操作
timer(); //用定时器溢出来模拟后沿抖动结束事件
}
(4)条件4:后沿抖动结束 从后沿抖动状态到后沿抖动结束这一事件发生,虽然没有具体的事件,但是历时是确定的,基本在10ms左右,可以通过定时器溢出来模拟后沿抖动结束事件。一旦定时器溢出(或者定时器中断发生),就说明迁移条件成立了,立即把状态迁移至未按下。
//TF1 == 1即后沿抖动结束标志。
//要在此时关闭定时器,防止重复判断。
if (1 == TF1)
{
KEY_Status = NOPRESS;
TF1 = 0; //清除溢出标志
TR1 = 0; //关闭定时器
}

确保状态机能稳定地进行状态迁移

如果状态能稳定的迁移,也就是说如果我们能对状态发生迁移的条件进行准确判断,状态机就会实现稳定的迁移。就可以在此基础上进行其他操作,不用担心状态机的状态界定错误,因为状态机是可以稳定迁移的。
所以关键是对状态发生迁移的条件进行准确判定,从而使各个状态不发生交叉,即稳定迁移。
要反复检查上面我们对状态机迁移条件界定是否准确。直到确定是准确的,就可以在此基础上做点别的事情了。
可以在心中模拟程序执行来验证思路是否正确。

按键消抖的原理:状态切换

(按键)状态机中,由于状态的切换是通过判定条件来进行的,一旦条件成立,状态就瞬间切换了,每个状态会持续一段时间,在此期间循环可能会执行多次,但由于状态切换的条件不满足,所以不会进入if语句中操作。所以如果把任何状态切换到下一状态的瞬间当成按键事件,那么按键就只会被检测到一次,这就是按键消抖的原理。

按键操作触发的时机

实际测试发现,把对按键事件的响应操作放在后沿抖动开始时(即状态机由按下状态迁移到后沿抖动状态的瞬间)效果比较不错,不会显得太敏感也不会显得很迟钝。

代码

//状态机的定义(即按键检测与消抖程序), 确保按键状态机能够发生稳定的状态迁移
void keyScan()
{
 //KEY_Status == NOPRESS && KEY_Value == 0,等价于条件1成立
 if (NOPRESS == KEY_Status && 0 == KEY_Value)
 {
  KEY_Status = PRESHAKE; //执行状态迁移
  timer(); //设置一个10ms定时器,用定时器溢出事件来模拟前沿抖动结束事件。
 }
 //若TF1 == 1; 则条件2成立
 if (PRESHAKE == KEY_Status && 1 == TF1)
 {
  KEY_Status = PRESS; //执行状态迁移
  TR1 = 0; //关闭定时器
  TF1 = 0; //清除溢出标志
 } 
 //KEY_Status == PRESS && KEY_Value == 1则条件3成立
 if (PRESS == KEY_Status && 1 == KEY_Value)
 {
  KEY_Status = TAILSHAKE; //执行状态迁移
  n++; //以n++为例, 响应按键事件
  timer(); //用定时器溢出来模拟后沿抖动结束事件
 }
 //TAILSHAKE == KEY_Status && 1 == TF1则条件4成立
 //要在此时关闭定时器,防止重复判断。
 if (TAILSHAKE == KEY_Status && 1 == TF1)
 {
  KEY_Status = NOPRESS;
  TR1 = 0; //关闭定时器
  TF1 = 0; //清除溢出标志
 } 
}


//10ms定时模块
void timer()
{
      TMOD = 0x11; //选择定时器1和定时器0工作模式为16位模式
      //设置定时器初始值为0xDC00,会在10毫秒后溢出
      TH1 = 0xDC;
      TL1 = 0x00;
      TR1 = 1; //启动定时器1
}