详解单片机的按键检测与控制
按键在单片机控制系统中起到人机交互的作用,通过按键可以输入数据、命令和各种参数,按键侧键测处理是单片机系统设计和开发中一个重要的内容,关系到整个系统的交互性能和稳定性。按键处理形式在单片机系统中有两种形式:直接按键和矩阵编码键盘,下面分别对这两种按键检测电路的特点及编程思路和方法进行分析和介绍。
按键抖动问题产生的原因及解决方法
按键的抖动问题是指按键的触点在闭合和断开瞬间由于接触情况不稳定,从而导致电压信号的抖动现象(由按键的机械特性造成,不可避免)。图8-1所示为一次按键的抖动过程,在按键的前沿和后沿都会有5~10ms的抖动。
图8-1 按键抖动示意图
对于时钟是微秒级的单片机而言,键盘的抖动有可能造成单片机对一次按键的多次处理。为了提高系统的稳定性,我们必须采用有效的方式消除抖动。
去除抖动可以采用硬件方式和软件方式。硬件方式一般是在按键与单片机的输入通道上安装硬件去抖电路(如RS触发器)。软件方式的实现方法是:当查询到电路中有按键按下时,先不进行处理,而是先执行10~20ms的延时程序,延时程序结束后,再次查询按键状态,若此时按键仍为按下状态,则视为按键被按下。
按键检测电路及应用
1. 独立式按键
独立式键盘相互独立,每个按键占用一根I/O口线,每根I/O口线上的按键工作状态不会影响其他按键的工作状态,CPU可直接读取该I/O线的高/低电平状态。这种按键硬件、软件结构简单,判键速度快,使用方便,但占用I/O口线较多,适用于按键数量较少的系统中。
独立连接式键盘连接如图8-2所示。当没有键被按下时,所有的数据输入线均为高电平;当任意一个按键被按下时,与之相连的数据输入线将变为低电平;通过相应指令,可以判断是否有键被按下。
图8-2 独立式键盘接口设计
【例8-1】 利用单片机的P1.0~P1.34个I/O口检测4个按键的触发信息,以实现不同功能的控制。
硬件电路参见图8-2所示,C51参考程序如下:
2. 矩阵式按键
在单片机系统中,当按键数量较多时,为了减少IO口的使用,通常将按键排列成矩阵型式。例如下例中的16个按键,被排列成了如图8-3所示的4X4矩阵方式。该矩阵式键盘由4根行线和4根列线组成,每个行线和列线的交叉点是一个按键。
【例8-2】 将矩阵式键盘的按键值通过数码管显示出来。
电路连接如图8-3所示(电源和震荡电路未标出)。
图8-3 矩阵式键盘按键显示电路图
如何判断被按下的键值呢?
分析如下:
根据下面的电路图,如果已知P1.0端口被置为低电平“0”,那么当按键K0被按下时,可以肯定P1.4端口的电平也变为了低电平“0”。基于这个原理,总结矩阵键盘识别按键的步骤如下:
a)首先判断是否有按键被按下
本例中矩阵键盘中P1端口低4位连接的是列线,高4位连接的是行线。将全部行线置为低电平“0”,全部列线置为高电平“1”。然后检测列线的状态。只要有一根列线的电平为低,则表示有按键被按下。否则没有按键按下。
b)按键消抖
当判断到有按键被按下后,还要进行消抖处理,以确认真正有按键被按下。
c)按键识别
当确认有键被按下后,采用逐行扫描的方法来确定是哪一个按键被按下。先扫描第一行,即将第一行输出低电平“0”,然后读取列值,那一列出现低电平“0”,则说明该列与第一行交叉处的按键被按下。如果读入的列值全部为"1",说明与第一行连接的按键均没有被按下。那么接下来开始扫描第二行,以此类推。直到完成全部行线的扫描。
C51程序如下:
矩阵式键盘与I/O接口应用
【例8-3】 四位数字密码锁
四位数字密码锁功能:通过键盘输入密码,当输入密码与内置密码相同时,继电器动作,表示密码锁解开。为了简化功能,该密码锁只使用12个键(即4x3键盘),其中S1~S9为1~9数字键,S10为0数字键,S11为“*”键作为确认键使用,而S12为“#”键作为复位键,键盘接口电路如图8-4所示,键值布局如表8-1所示。
图8-4 行列式键盘电路连接图
表8-1 键盘布局表
单片机上电时,数码管显示“0000”,此时输入数字,数码管将显示按键值,数字逐个向左递增,四次输入完毕,四个数码管显示输入的数,此时按“*”号按键将启动比较,若输入数字与内设密码相同,继电器动作。如不同,则系统复位等待重新输入密码。当按“#”号键,系统复位,数码管显示“0000”。
在上面键扫描程序基础上增加的源程序如下:
一种实用的单片机按键检测方式
《手把手教你学51单片机》第八课讲解按键相关内容的时候,介绍了一种状态检测扫描按键的办法,既可以检测按键,又可以有效消抖。但是部分同学以前没有接触过类似的思想和方式,所以可能接受起来有点难度,这里我再详细给讲解分析一下,如果教程第八课你已经彻底明白,那么这里可以不用再学习了,如果还模模糊糊,可以再巩固一下。
1、独立按键
常用的按键电路有两种形式,独立式按键和矩阵式按键,独立式按键比较简单,它们各自与独立的输入线相连接,如下图所示。
4条输入KeyIn1、KeyIn2、KeyIn3、KeyIn4接到单片机的IO口上,当按键K1按下时,+5V通过电阻R1然后再通过按键K1最终进入GND形成一条通路,那么这条线路的全部电压都加到了R1这个电阻上,KeyIn1这个引脚就和GND等电位,是个低电平。当松开按键后,线路断开,就不会有电流通过,那么KeyIn1和+5V就应该是等电位,是一个高电平。我们就可以读取通过KeyIn1这个IO口的高低电平来判断是否有按键按下,独立按键的原理还是很简单的。
2、矩阵按键
在某一个系统设计中,如果需要使用很多的按键时,做成独立按键会大量占用IO口,因此我们引入了矩阵按键的设计方式,如下图所示,用了8个IO口实现了16个按键检测的电路。
上图,一共有4组按键,我们只看其中一组,如下图所示。大家认真看一下,如果KeyOut1输出一个低电平,KeyOut1就相当于是GND,是否相当于4个独立按键呢。当然这时候KeyOut2、KeyOut3、KeyOut4都必须输出高电平,它们都输出高电平才能保证与它们相连的三路按键不会对这一路产生干扰,大家可以对照两张原理图分析一下。
同理,可以将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混合的状态,那就认为当前的状态既不是弹起,也不是按下,可能是在刚按下,可能是在抖动过程,也可能是在快抖动完毕,总之状态是不确定的,这个时候我们就不做判断,既不认为他是按下,也不认为他是弹起,如下图所示,一个按键从弹起到按下,再到弹起的过程。
同学们注意,我们读的是连续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,自然给你定薪水待遇的时候,印象分就会低。如果你写出这种实用的实际开发的程序给领导一看,领导很可能对你另眼相看。
相关问答
51 单片机 , 按键 控制流水灯程序,懂的进!八... _作业帮[最佳回答]假如,你的程序在执行key_scan();是触发了两次中断导致num>20,你的显示函数将永远被跳过,你应该写成if(num>20)��{���num=0;���display();�.....
51 单片机 怎么定义 按键 ?在51单片机中,可以通过以下步骤定义按键。首先,选择一个IO口作为按键输入口,将按键连接到该IO口上。然后,在程序中设置该IO口为输入模式。接着,通过读取该...
51 单片机 识别四个独立 按键 是短按还是长按,解决方案?识别长按,先要确定一个时间,超过这个时间为长按,否则即为短按。当按一下键后,开始计时,同时判断确定时间到否?到了,执行长按的程序,等待按键释放。在...识...
单片机 的 按键 开关抖动的产生原因,抖动的特点和如何消除抖动 ...开关按钮,但信号的传导并不是单纯的由1断开/闭合时会有抖动,信号如图示会在HIGH和LOW之间抖动。这种抖动对人来说是感觉不到的,但对单片机来说,则是...
在做 单片机 按钮控制流水灯,我想达到按一下自己循环,再按一下...#include"reg52.h"#defineuintunsignedint#defineucharunsignedcharsbitK1=P3^2;//独立...
求一个51 单片机 的程序:一个 按键 控制led灯闪烁,就是按一下灯闪,再按灯就灭了?LEDBITP1.0KEYBITP1.1ORG0000HLJMPMAINORG0030HMAIN:CLR00HMAIN1:JBKEY,LOOPJNBKEY,$CPL00H...
单片机 三个 按键 独立控制三个led灯汇编语言?51单片机的IO口接按键,实现外部触发(外部中断,高低电平,上下降沿),触发后再通过IO控制LED的开关即可。51单片机的IO口接按键,实现外部触发(外部中断,高低电平...
51 单片机 ,一个 按键 怎么控制两个灯轮流闪动,看详细说明?先设置一个位变量,表征现在是启动还是停止。再设置一个位变量,表征应该是LED1亮还是LED2亮。然后在检测到按键的同时,启动定时器,取反第一个变量,计时五秒...
52 单片机 独立 按键 控制LED流水灯?泻药不加译码器或者锁存器的话,可以用12个引脚+4个三极管(当然还有必要的电阻)实现,如果你的单片机有大电流io口的话,4个三极管也可以省了,12个引脚可以...
单片机 的矩阵 按键 怎么输入两位数,例如,先输入2,数码管显示2,再输入3,数码管显示23,有类似程序更好?我的思路是,把你输入的数字先保存在一个数组里,然后实时的显示该数组的内容即可。#defineMAX_NUM2ucharnum_pos=0;//输入数字的个数计数。...我的思路...