前言
按键处理是学习单片机的必修课之一。一次按键的过程,并非是一个理想的有一定宽度的电平脉冲,而是在按下、弹起过程中存在抖动,只有在中间阶段电平信号是稳定的。一次典型的按键过程是酱紫的:
在抖动过程中,电平信号高低反复变化,如果你的按键检测是检测下降沿或上升沿或者是用外部中断检测按键,都可能在抖动时重复检测到多次按键。这就是在未消抖的按一次键显示值加1的程序中,出现按一次键显示值+2、+3甚至加更多的原因。
对于按键消抖,常用的有硬件消抖和软件消抖。本文是我个人对按键处理的一些常见方法的总结,由于我本人不太懂硬件,所以这里只讨论独立按键的软件消抖实现。水平有限,如有错误请不吝指正。
硬件环境
本文代码均在单片机STC90C516RD+、晶振12.0MHz硬件环境下试验通过。
带消抖的简单的按键处理
最简单的消抖处理就是在首次检测到电平变化后加一个延时,等待抖动停止后再次检测电平信号。这也是大多数单片机教程讲述的消抖方式。但在实际应用中基本不用这种方式,原因后面讲,先看代码:
//方法一:带消抖的简单的按键处理 #include <reg52.h> #define GPIO_KEY P1 //8个独立按键IO口 #define GPIO_LED P0 //8个LED灯,用于显示键值 unsigned char ScanKey(); void DelayXms(unsigned char x); void main() { unsigned char key; GPIO_LED = 0x00; //初始化LED while (1) { key = ScanKey(); //读取键值 // if (0xff != key) //若有键按下,则更新LED的状态 GPIO_LED = ~key; //点亮LED } } unsigned char ScanKey() { unsigned char keyValue = 0xff; //赋初值,0xff表示没有键按下 GPIO_KEY = 0xff; //给按键IO口置位 if (0xff != GPIO_KEY) //检查按键IO口的电平,如有键按下则不为0xff { DelayXms(15); //延时15ms,滤掉抖动。一般按键的抖动时间在10ms~20ms if (0xff != GPIO_KEY) //再次检查按键IO口的电平 { keyValue = GPIO_KEY; //重复检测后表明有键按下,读取键值 } // while (0xff != GPIO_KEY) ; //等待按键弹起 } return keyValue; } void DelayXms(unsigned char X) { unsigned char i, j; do { i = 2; j = 240; do { while (--j); } while (--i); } while (--X); }
可以看到,在首次检测到电平由1->0后,延时了10ms,等待抖动过去,然后再检测按键的电平。你也许已经注意到了,在延时10ms期间,单片机闲置了,就暂停在那里等待延时完成,这对处理能力本就紧张的单片机来说无疑是个巨大的浪费。特别是当你在用单片机同时运行数码管动态扫描等对时序要求高功能的时候,按键消抖延时期间程序暂停了,数码管也就熄灭了,严重影响显示效果。
利用定时器消抖的按键处理
为了避免单纯Delay()消抖所产生的问题,可以采用定时器来进行延时,这样就不用让单片机在那里干等了。
一种简单的实现方式是设置一个全局状态变量,用来标志定时器延时时间已到,在第一次检测到电平变化时开启定时器,检测到定时器延时时间已到时关闭定时器并再次进行按键检测。代码如下:
//方法二:定时器延时消抖的按键处理 #include <reg52.h> #define GPIO_KEY P1 //8个独立按键IO口 #define GPIO_LED P0 //8个LED灯,用于显示键值 unsigned char timeUp = 0; //标志位 unsigned char th0Value = (65536 - 15000) / 256; //15ms的定时器初值高8位 unsigned char tl0Value = (65536 - 15000) % 256; //15ms的定时器初值低8位 unsigned char ScanKey(); void InitialTimer0(); void main() { unsigned char key; GPIO_LED = 0x00; //初始化LED InitialTimer0(); while (1) { key = ScanKey(); //读取键值 if (0xff != key) //若有键按下,则更新LED的状态 GPIO_LED = ~key; //点亮LED } } void InitialTimer0() //12MHz { ET0 = 1; //打开定时器0 EA = 1; //打开系统总中断开关 TMOD &= 0xF0; //清空定时器0的工作模式参数 TMOD |= 0x01; //设置定时器0的工作模式为模式1,16位定时器 TH0 = th0Value; //设置定时高8位初值 TL0 = tl0Value; //设置定时低8位初值 TF0 = 0; //清除TF0溢出标志 TR0 = 0; //关闭定时器0 } void Timer0Interrupt() interrupt 1 { TH0 = th0Value; //设置定时高8位初值 TL0 = tl0Value; //设置定时低8位初值 timeUp = 1; //定时器标志位置1 } unsigned char ScanKey() { unsigned char keyValue = 0xff; //赋初值,0xff表示没有键按下 if (0 == TR0 && 0xff != GPIO_KEY) { timeUp = 0; //定时器标志位置0 TH0 = th0Value; //设置定时高8位初值 TL0 = tl0Value; //设置定时低8位初值 TR0 = 1; //开启定时器0,开始计时 } if (1 == timeUp) { TR0 = 0; //关闭定时器0 keyValue = GPIO_KEY; //读取键值 } return keyValue; }
另一种方法是利用定时器0,每2ms左右中断一次,在中断服务程序中进行多次按键检测,当检测到10次按键按下状态时,则认为发生了一次有效的按键按下动作。这种方式与上一种相比,进行了多次检测,提高了按键检测的准确性。实现代码如下:
//方法三:定时器多次检测的按键处理 #include <reg52.h> #define GPIO_KEY P1 //8个独立按键IO口 #define GPIO_LED P0 //8个LED灯,用于显示键值 unsigned char keyCur = 0xff; //暂存当前键值 unsigned char keyPress = 0; //按键按下状态标识 unsigned char th0Value = (65536 - 2000) / 256; //2ms的定时器初值高8位 unsigned char tl0Value = (65536 - 2000) % 256; //2ms的定时器初值低8位 unsigned char ScanKey(); void InitialTimer0(); void main() { unsigned char key; GPIO_LED = 0x00; //初始化LED keyCur = 0xff; //初始化 InitialTimer0(); while (1) { key = ScanKey(); //读取键值 if (0xff != key) //若有键按下,则更新LED的状态 GPIO_LED = ~key; //点亮LED } } void InitialTimer0() //12MHz { ET0 = 1; //打开定时器0 EA = 1; //打开系统总中断开关 TMOD &= 0xF0; //清空定时器0的工作模式参数 TMOD |= 0x01; //设置定时器0的工作模式为模式1,16位定时器 TH0 = th0Value; //设置定时高8位初值 TL0 = tl0Value; //设置定时低8位初值 TF0 = 0; //清除TF0溢出标志 TR0 = 1; //开启定时器0 } void Timer0Interrupt() interrupt 1 { static unsigned char counter = 0; //辅助计数 static unsigned char keyLast = 0xff; //记录上一次扫描时的键值 TH0 = th0Value; //设置定时高8位初值 TL0 = tl0Value; //设置定时低8位初值 keyCur = GPIO_KEY; //暂存当前键值 if (0xff != keyCur && keyCur == keyLast) //当前扫描时有键按下且与上一次按下的一致,则累加 counter ++; else { counter = 0; keyLast = keyCur; } if (10 == counter) //连续10次均有键按下且按按键未变,则认为时一次有效的按键 keyPress = 1; else keyPress = 0; } unsigned char ScanKey() { if (1 == keyPress) return keyCur; //读取键值 else return 0xff; }
至此,简单的按键单击实现实现告一段落。但往往实际中,我们不只要实现单击,还要实现双击、长按、连发等等功能,特别是在那些小尺寸、无法设置多个按键的项目中,一个按键往往需要通过不同的操作实现不同的功能。要实现这些复杂的功能,就需要引入一种设计模式——有限状态机模式。敬请期待下一篇:单片机按键处理方式(二)——状态机按键实现单击、双击、长按、连发(挖坑,待填)