《手把手教你学51单片机》第八课讲解按键相关内容的时候,介绍了一种状态检测扫描按键的办法,既可以检测按键,又可以有效消抖。但是部分同学以前没有接触过类似的思想和方式,所以可能接受起来有点难度,这里我再详细给讲解分析一下,如果教程第八课你已经彻底明白,那么这里可以不用再学习了,如果还模模糊糊,可以再巩固一下。

1、独立按键

常用的按键电路有两种形式,独立式按键和矩阵式按键,独立式按键比较简单,它们各自与独立的输入线相连接,如下图所示。

单片机的按键检测原理(一种实用的单片机按键检测方式)(1)

4条输入KeyIn1、KeyIn2、KeyIn3、KeyIn4接到单片机的IO口上,当按键K1按下时, 5V通过电阻R1然后再通过按键K1最终进入GND形成一条通路,那么这条线路的全部电压都加到了R1这个电阻上,KeyIn1这个引脚就和GND等电位,是个低电平。当松开按键后,线路断开,就不会有电流通过,那么KeyIn1和 5V就应该是等电位,是一个高电平。我们就可以读取通过KeyIn1这个IO口的高低电平来判断是否有按键按下,独立按键的原理还是很简单的。

2、矩阵按键

在某一个系统设计中,如果需要使用很多的按键时,做成独立按键会大量占用IO口,因此我们引入了矩阵按键的设计方式,如下图所示,用了8个IO口实现了16个按键检测的电路。

单片机的按键检测原理(一种实用的单片机按键检测方式)(2)

上图,一共有4组按键,我们只看其中一组,如下图所示。大家认真看一下,如果KeyOut1输出一个低电平,KeyOut1就相当于是GND,是否相当于4个独立按键呢。当然这时候KeyOut2、KeyOut3、KeyOut4都必须输出高电平,它们都输出高电平才能保证与它们相连的三路按键不会对这一路产生干扰,大家可以对照两张原理图分析一下。

单片机的按键检测原理(一种实用的单片机按键检测方式)(3)

同理,可以将KeyOut1,KeyOut3,KeyOut4都拉高,把KeyOut2拉低,来读取KEY5到KEY8的值。


关于按键扫描的具体程序部分,大家可以去参考教程,我这里只把一段摘出来给大家讲一下,部分同学对其中一条语句有所疑问。

while (1)

{

if (KEY4 != backup) //当前值与前次值不相等说明此时按键有动作

{

if (backup == 0) //如果前次值为0,则说明当前是由0变1,即按键弹起

{

cnt ; //按键次数 1

if (cnt >= 10)

{ //只用1个数码管显示,所以加到10就清零重新开始

cnt = 0;

}

P0 = LedChar[cnt]; //计数值显示到数码管上

}

backup = KEY4; //更新备份为当前值,以备进行下次比较

}

}

大家注意程序中加粗的部分,这一句是在 if (KEY4 != backup) 这一句的循环范围内,因此只要按键发生任何变化,按键的备份值backup这个变量,都会随着更新一次。


3、按键消抖

按键抖动的产生原理和造成的影响,这里不再赘述。那么重点研究一下如何消抖。初学者通常采用的办法是用delay来延时的办法,这种是一种演示实验的办法,做实际开发是万万不能用的,因为CPU一直在delay里工作,不能做其他事情,是一种很低级的办法。

下面介绍一种状态采集的办法,这种办法不是把消抖单独拿出来处理,而是读的过程中,通过连续读取多次来确认当前的按键状态。通过多次判断,那么就可以消除抖动带来的影响。这样讲有点绕,还是用例子。

一般的按键持续时间在100ms以上,抖动的时间,都是在10ms之内发生。那好了,那我们就2ms读一次按键,连续读8次,那一共是16ms,大于10ms的抖动时间,小于按键持续时间100ms。如果这8次状态是 11111111,那么就认为当前状态是按键弹起状态。如果这8次状态是00000000,那么就认为当前状态是按键按下状态。那如果这8次状态是1和0混合的状态,那就认为当前的状态既不是弹起,也不是按下,可能是在刚按下,可能是在抖动过程,也可能是在快抖动完毕,总之状态是不确定的,这个时候我们就不做判断,既不认为他是按下,也不认为他是弹起,如下图所示,一个按键从弹起到按下,再到弹起的过程。

单片机的按键检测原理(一种实用的单片机按键检测方式)(4)

同学们注意,我们读的是连续8次状态,而并不是间隔8个状态读一次,个别同学混淆,举例说明,数字用十六进制。

第一次:12345678

第二次:23456789

第三次:3456789A

第四次:456789AB

....................................

任何一次判断,只有全1认为是弹起,全0认为是按下,否则则认为按键处于抖动区间,不做判断。

程序方面我只写主程序部分,用定时器定时2ms,在中断内部进行按键当前状态读取和更新,keybuf用来存储连续8次的按键状态,然后判断出来,把最终我们认为是“弹起”还是“按下”这样的最终状态结果,赋值给KeySta。那这里某一个按键我们确认是“弹起”还是“按下”加上扫描和消抖判断一共需要16ms即可确认。

while (1)

{

if (KeySta != backup) //当前值与前次值不相等说明此时按键有动作

{

if (backup == 0) //如果前次值为0,则说明当前是弹起动作

{

cnt ; //按键次数 1

if (cnt >= 10)

{ //只用1个数码管显示,所以加到10就清零重新开始

cnt = 0;

}

P0 = LedChar[cnt]; //计数值显示到数码管上

}

backup = KeySta; //更新备份为当前值,以备进行下次比较

}

}

/* T0中断服务函数,用于按键状态的扫描并消抖 */

void InterruptTimer0() interrupt 1

{

static unsigned char keybuf = 0xFF; //扫描缓冲区,保存一段时间内的扫描值

TH0 = 0xF8; //重新加载初值

TL0 = 0xCD;

keybuf = (keybuf<<1) | KEY4; //缓冲区左移一位,并将当前扫描值移入最低位

if (keybuf == 0x00)

{

KeySta = 0; //连续8次扫描值都为0,可认为按键已按下

}

else if (keybuf == 0xFF)

{

KeySta = 1; //连续8次扫描值都为1,可认为按键已弹起

}

else

{} //其它情况则说明按键状态尚未稳定,则不对KeySta变量值进行更新

}


这种状态扫描的办法是工程中常用的一个办法,介绍给大家使用。这里虽然判断一次按键也用了16ms,但是真正单片机在读按键状态的程序时间是很短的,我们不需要停留在一直等待按键状态发生变化的那个过程中,这个时候单片机可以做很多其他的事情。

矩阵按键有16个,如果还是按照2ms采集一次,一次可以采集1组共4个按键的情况,一共采集8次的话,需要的总时间 = 2ms*8次*(16/4)= 64ms,这个时间就有点太长了。这里的时间计算方式,部分同学混淆,所以我把式子详细列了出来。

那我们对矩阵按键的处理方式采取状态时间间隔减半,确认最终所需要的读取状态次数减半处理,时间间隔1ms读一次,每次可以读一组4个按键,每个按键需要采集4次最终确认按键是“弹起”还是“按下”,那读完了的总时间 = 1ms*4次*(16/4)=16ms。但是有一点这里重点强调(凡事这里重点强调的,就是有其他同学混淆的),每一个按键都是16ms才能确认按键状态,但是扫描时间一旦开启,是每间隔4ms,按键就判断到一次。

第一次:1234

第二次:2345

第三次:3456

第四次:4567

----------------------

#include <reg52.h>

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

sbit KEY_IN_1 = P2^4;

sbit KEY_IN_2 = P2^5;

sbit KEY_IN_3 = P2^6;

sbit KEY_IN_4 = P2^7;

sbit KEY_OUT_1 = P2^3;

sbit KEY_OUT_2 = P2^2;

sbit KEY_OUT_3 = P2^1;

sbit KEY_OUT_4 = P2^0;

unsigned char code LedChar[] = { //数码管显示字符转换表

0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,

0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E

};

unsigned char KeySta[4][4] = { //全部矩阵按键的当前状态

{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}

};

void main()

{

unsigned char i, j;

unsigned char backup[4][4] = { //按键值备份,保存前一次的值

{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}

};

EA = 1; //使能总中断

ENLED = 0; //选择数码管DS1进行显示

ADDR3 = 1;

ADDR2 = 0;

ADDR1 = 0;

ADDR0 = 0;

TMOD = 0x01; //设置T0为模式1

TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms

TL0 = 0x67;

ET0 = 1; //使能T0中断

TR0 = 1; //启动T0

P0 = LedChar[0]; //默认显示0

while (1)

{

for (i=0; i<4; i ) //循环检测4*4的矩阵按键

{

for (j=0; j<4; j )

{

if (backup[i][j] != KeySta[i][j]) //检测按键动作

{

if (backup[i][j] != 0) //按键按下时执行动作

{

P0 = LedChar[i*4 j]; //将编号显示到数码管

}

backup[i][j] = KeySta[i][j]; //更新前一次的备份值

}

}

}

}

}

/* T0中断服务函数,扫描矩阵按键状态并消抖 */

void InterruptTimer0() interrupt 1

{

unsigned char i;

static unsigned char keyout = 0; //矩阵按键扫描输出索引

static unsigned char keybuf[4][4] = { //矩阵按键扫描缓冲区

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}

};

TH0 = 0xFC; //重新加载初值

TL0 = 0x67;

//将一行的4个按键值移入缓冲区

keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;

keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;

keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;

keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

//消抖后更新按键状态

for (i=0; i<4; i ) //每行4个按键,所以循环4次

{

if ((keybuf[keyout][i] & 0x0F) == 0x00)

{ //连续4次扫描值为0,即4*4ms内都是按下状态时,可认为按键已稳定的按下

KeySta[keyout][i] = 0;

}

else if ((keybuf[keyout][i] & 0x0F) == 0x0F)

{ //连续4次扫描值为1,即4*4ms内都是弹起状态时,可认为按键已稳定的弹起

KeySta[keyout][i] = 1;

}

}

//执行下一次的扫描输出

keyout ; //输出索引递增

keyout = keyout & 0x03; //索引值加到4即归零

switch (keyout) //根据索引,释放当前输出引脚,拉低下次的输出引脚

{

case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;

case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;

case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;

case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;

default: break;

}

}

4、长短按键

在单片机系统中应用按键的时候,如果想连续加很多数字的时候,我们会希望一直按住按键,数字就自动持续增加或减小,这就是所谓的长短按键应用。

当检测到一个按键产生按下动作后,马上执行一次相应的操作,同时在程序里记录按键按下的持续时间,该时间超过1秒后(主要是为了区别短按和长按这两个动作,因短按的时间通常都达到几百ms),每隔200ms(如果你需要更快那就用更短的时间,反之亦然)就自动再执行一次该按键对应的操作,这就是一个典型的长按键效果。

程序代码摘自《手把手教你学51单片机》教程第十课,详细代码有想了解的可以去看,这里只把和长短按键有关系的部分代码摘录出来。

void KeyDriver()

{

unsigned char i, j;

static unsigned char pdata backup[4][4] = { //按键值备份,保存前一次的值

{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}

};

static unsigned long pdata TimeThr[4][4] = { //快速输入执行的时间阈值

{1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000},

{1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}

};

for (i=0; i<4; i ) //循环扫描4*4的矩阵按键

{

for (j=0; j<4; j )

{

if (backup[i][j] != KeySta[i][j]) //检测按键动作

{

if (backup[i][j] != 0) //按键按下时执行动作

{

KeyAction(KeyCodeMap[i][j]); //调用按键动作函数

}

backup[i][j] = KeySta[i][j]; //刷新前一次的备份值

}

if (KeyDownTime[i][j] > 0) //检测执行快速输入

{

if (KeyDownTime[i][j] >= TimeThr[i][j])

{ //达到阈值时执行一次动作

KeyAction(KeyCodeMap[i][j]); //调用按键动作函数

TimeThr[i][j] = 200; //时间阈值增加200ms,以准备下次执行

}

}

else //按键弹起时复位阈值时间

{

TimeThr[i][j] = 1000; //恢复1s的初始阈值时间

}

}

}

}

void KeyScan()

{

unsigned char i;

static unsigned char keyout = 0; //矩阵按键扫描输出索引

static unsigned char keybuf[4][4] = { //矩阵按键扫描缓冲区

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}

};

//将一行的4个按键值移入缓冲区

keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;

keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;

keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;

keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

//消抖后更新按键状态

for (i=0; i<4; i ) //每行4个按键,所以循环4次

{

if ((keybuf[keyout][i] & 0x0F) == 0x00)

{ //连续4次扫描值为0,即4*4ms内都是按下状态时,可认为按键已稳定的按下

KeySta[keyout][i] = 0;

KeyDownTime[keyout][i] = 4; //按下的持续时间累加

}

else if ((keybuf[keyout][i] & 0x0F) == 0x0F)

{ //连续4次扫描值为1,即4*4ms内都是弹起状态时,可认为按键已稳定的弹起

KeySta[keyout][i] = 1;

KeyDownTime[keyout][i] = 0; //按下的持续时间清零

}

}

//执行下一次的扫描输出

keyout ; //输出索引递增

keyout &= 0x03; //索引值加到4即归零

switch (keyout) //根据索引,释放当前输出引脚,拉低下次的输出引脚

{

case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;

case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;

case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;

case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;

default: break;

}

}


不管你以后遇到的是什么样的按键,这种扫描按键状态的思路,提供你参考学习,以后再也不要在实际工程中用delay函数来消抖了,那样会让领导一看你就是新手,用的方法很low,自然给你定薪水待遇的时候,印象分就会低。如果你写出这种实用的实际开发的程序给领导一看,领导很可能对你另眼相看。

,