【说在前面的话】
按键作为单片机的输入设备,可以向单片机输入数据、传输命令等,是设置参数和控制设备的常用接口。所以,学会按键驱动也是初学者必不可少的能力。说到按键驱动程序,大家应该也不陌生,而一般的按键驱动流程图如下
这里,可能有人会问,为什么要延时10ms啊?
那是因为按键被按下时,不会像理想的情况非0即1,而是会有抖动,如下图
当机械按键被按下或松开时,会有10ms的抖动时间,所以要延时10ms来消去波形抖动(* ̄︶ ̄)
知道了这个,一般初学者编写的按键驱动程序如下:
//延时1ms
void Delay1ms() { //@12.000MHz
unsigned char i, j;
i = 12;
j = 169;
do{
while (--j);
} while (--i);
}
//ms延时
void delay_ms(int ms){
char i = 0;
for(i = 0; i < ms; i++){
Delay1ms();
}
}
char get_key(){
//检测按键是否被按下
if(KEY1 == 0){
//延时10ms
delay_ms(10);
//再次检测按键是否被按下
if(KEY1 == 0){
//等待按键松开
while(KEY1 == 0){ }
delay_ms(10);
return 1;
}
}
return 0;
}
像这种按键驱动程序也很简单,作为基础学习和一些简单的系统中还可以,但是在很多的产品设计中,这种按键程序还是有很大的不足和缺陷。因为他不仅采用了软件延时使单片机效率降低而且还在那里死等按键松开,系统的实时性也变得很差。为此,有人提出了 一种基于状态机的按键驱动程序 ,很好地解决了上述程序的缺陷。下面我们就简单讲一下什么是状态机。
【状态机简介】
对于学电子的同学,首先接触到的状态机应该是在数字逻辑电路( 简称数电 )中,状态机的分析方法被应用于时序逻辑电路的设计中,其实状态机的思想对我们的软件设计也很有用,首先简单介绍一下状态机,它是由有限的状态和相互之间的迁移构成的。在任何时候,只能处于状态机的某一个状态,当接收到一个转移事件时,状态机进行状态的转移。
下面,就以按键驱动为例,画出他的状态转移图,如下
有了状态转移图,那我们就用程序实现一下这个按键驱动程序。从图中我们知道按键驱动程序由3个状态,刚好可以用C语言的switch case语句来实现这3个状态,而状态间的迁移就可以用if条件判断语句来实现。知道了这个,那我们就动手实现一下。
基于状态机的按键驱动程序
首先,打开原理图,看一下按键接到了单片机的哪个管脚,如下
我们以按键1为例,接到了单片机的P33脚,当按键 按下时为低电平 , 松开为高电平 ,基于此我们的按键程序如下:
sbit KEY1 = P3^3;//key1
char get_key(){
//保存按键状态
static char key_flag = 0;
//软件延时计时器
static unsigned int s_Counter = 0;
switch(key_flag){
//状态0为无按键按下
case 0:
if(KEY1 == 0){
//如果有按键按下,转为状态1
key_flag = 1;
}
break;
//状态1为延时消抖
case 1:
s_Counter++;
if(s_Counter > 1000){
//延时10ms,计时器清零
s_Counter = 0;
if(KEY1 == 0){
//如果按键被按下,转为状态2
key_flag = 2;
return 1;
}else{
//如果按键未按下,转为状态0
key_flag = 0;
}
}
break;
//状态2为等待按键释放
case 2:
if(KEY1 == 1){
//如果按键松开,转为状态0
key_flag = 0;
}
break;
}
return 0;
}
- 注意,每个case结束后都有一个break
- 第18行,s_Counter > 1000相当于延时10ms,当然这个1000是随便给的值,大家要根据具体情况设置此值,如果测试小于10ms就可以加大此值,我们只是为了说明用s_Counter 可以延时。
- 第24行,在延时去抖完成后就返回了1(相当于按键按下),这样做的好处就是可以提高按键响应速度。当然也可以在状态2按键松开后返回1。
基于状态机的按键驱动程序我们就简单写完了,相信大家也get到重点了,这个只是简单实现了按键的单击,当然,我们也可以实现按键的双击和长按。
哈哈,在编写驱动之前,我们先细化一下需求,首先区分单击和长按,这个很简单,规定一个时间就可以,我们定为1秒钟。即按下时间小于1秒为单击,大于1秒为长按。
那双击怎么办呢?
我们规定,当第一次按下持续时间小于500ms内松开按键,在之后500ms内又按下按键,此时为双击事件。这里有两点需要注意,1、第一次按下的时间不能超过500ms,否则就被判断为单击或长按。2、在第一次按下松开后开始计时,如果500ms内没有按键再次按下则为单击。按键双击的原理如下图所示
按键双击和长按的需求我们讲完了,接下来画出他的状态转移图,如下
哈哈哈,还可以吧,没那么复杂。相信大家应该能看懂。这里有必要说一下状态5,判断双击其实就是第二次按下延时10ms消抖,如果确实按下则为双击否则为单击。好了,看看程序怎么实现吧,如下
#define DELAY_10ms 500
#define DELAY_500ms 10000
char get_key3(){
static char key_flag = 0;
static unsigned int s_Counter = 0;
switch(key_flag){
case 0://无按键按下
if(KEY1 == 0){
key_flag = 1;
}
break;
case 1://延时10ms消抖
s_Counter++;
if(s_Counter > DELAY_10ms){
s_Counter = 0;
if(KEY1 == 0){
key_flag = 2;
}else{
key_flag = 0;
}
}
break;
case 2://计时500ms,等待按键松开
s_Counter++;
if(s_Counter > DELAY_500ms){//500ms内按键未松开
s_Counter = 0;
key_flag = 6;
}
if(KEY1 == 1){//500ms内按键松开
s_Counter = 0;
key_flag = 3;
}
break;
case 3://按键松开,延时10ms消抖
s_Counter++;
if(s_Counter > DELAY_10ms){
s_Counter = 0;
key_flag = 4;
}
break;
case 4://等待双击
s_Counter++;
if(s_Counter > DELAY_500ms){
s_Counter = 0;
key_flag = 7;//500ms内按键未按下
}
if(KEY1 == 0){//500ms内按键被按下
s_Counter = 0;
key_flag = 5;
}
break;
case 5://延时10ms消抖
s_Counter++;
if(s_Counter > DELAY_10ms){
s_Counter = 0;
if(KEY1 == 0){
key_flag = 8;//等待按键松开
return 2;//双击
}else{
key_flag = 7;//单击
}
}
break;
case 6:
s_Counter++;
if(s_Counter > DELAY_500ms){
s_Counter = 0;
key_flag = 8;//等待按键松开
return 3;//长按
}
if(KEY1 == 1){
s_Counter = 0;
key_flag = 7;
}
break;
case 7://单击
key_flag = 8;
return 1;//单击
break;
case 8://等待按键松开
if(KEY1 == 1){
s_Counter = 0;
key_flag = 0;
}
break;
}
return 0;
}