单片机独立按键与矩阵按键原理图

独立按键

通常的按键分为独立式按键和矩阵式按键两种,独立式按键比较简单,并且与独立的输入线相连接,如图13-1所示图13-1 独立式按键电路图

单片机独立按键与矩阵按键原理图

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

这个电路中按键的原理我们清楚了,但是实际上在我们的单片机IO口内部,也有一个上拉电阻的存在。我们的按键是接到了P2口上,P2口上电默认是准双向IO口,我们来简单了解一下这个准双向IO口的电路,如图13-2所示。图13-2 准双向IO口结构图

单片机独立按键与矩阵按键原理图

首先说明一点,就是我们现在绝大多数单片机的IO口都是使用MOS管而非三极管,但用在这里的MOS管其原理和三极管是一样的,因此在这里我用三极管替代它来进行原理讲解,把前面讲过的三极管的知识搬过来,一切都是适用的,有助于理解。

图13-2方框内的电路都是指单片机内部部分,方框外的就是我们外接的上拉电阻。这个地方大家要注意一下,就是当我们要读取外部按键信号的时候,首先单片机必须得给个‘1’,也就是高电平,这样我们才能正常的读取外部的按键信号,我们来分析一下缘由。

当内部输出是高电平,经过一个反向器变成低电平,NPN三极管不会导通,那么单片机IO口从内部来看,由于上拉电阻R的存在,所以是一个高电平。当外部没有按键按下将电平拉低的话,VCC也是+5V,他们之间虽然有2个电阻,但是没有压差,就不会有电流,线上所有的位置都是高电平,这个时候我们就可以正常读取到按键的状态了。

当内部输出是个低电平,经过一个反相器变成高电平,NPN三极管导通,那么单片机的内部IO口就是个低电平,这个时候,外部虽然也有上拉电阻的存在,但是两个电阻是并联关系,不管按键是否按下,单片机的IO口上输入到单片机内部的状态都是低电平,我们就无法正常读取到按键的状态了。

这个和水流其实很类似的。内部和外部,只要有一边是低电位,那么电流就会顺流而下,由于只有上拉电阻,下边没有电阻分压,直接到GND上了,所以不管另外一边是高还是低,那电位肯定就是低电位了。

这里得到一个结论,这种具有上拉的准双向IO口,如果要正常读取外部信号的状态,必须首先得保证自己输出的电平是‘1’,如果输出‘0’,则无论外部信号是高是低,这个引脚读进来的都是低。

矩阵按键

我们在使用按键的时候有这样一种使用经验,当需要多个按键的时候,如果做成独立按键会大量占用IO口,因此我们引入了矩阵按键,如图13-3所示,使用了8个IO口来实现16个按键。图13-3 矩阵按键

单片机独立按键与矩阵按键原理图

其实独立按键理解了,矩阵按键也简单,我们来分析一下。图13-3中,一共有4组按键,我们只看其中一组,如图13-4所示。大家认真看一下,当KeyOut1输出一个低电平,KeyOut2、KeyOut3、KeyOut4这三个输出高电平时,是否相当于4个独立按键呢。图13-4 矩阵按键变独立按键

单片机独立按键与矩阵按键原理图

我们先用一个简单的程序来实现这4个独立按键的使用。

#include

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

sbit LED9 = P0^7;

sbit LED8 = P0^6;

sbit LED7 = P0^5;

sbit LED6 = P0^4;

sbit KEY1 = P2^4;

sbit KEY2 = P2^5;

sbit KEY3 = P2^6;

sbit KEY4 = P2^7;

void main(void)

{

//选择独立LED进行显示

P0 = 0xFF; //初始化P0

ADDR0 = 0;

ADDR1 = 1;

ADDR2 = 1;

ADDR3 = 1;

ENLED = 0;

P2 = 0xF7; //选中第一行按键以进行扫描

while(1)

{

//将按键扫描引脚的值传递到LED上

LED9 = KEY1; //按下时为0,对应的LED点亮

LED8 = KEY2;

LED7 = KEY3;

LED6 = KEY4;

}

}

这个程序可以实现当按下K1、K2、K3或者K4任何一个按键或者多个按键的时候,我们对应赋值的小灯就会点亮,松开按键的时候,小灯就熄灭。

从这里可以看出来,其实独立按键本身就是矩阵按键中的一种情况而已。

13.3按键消抖

绝大多数情况下,我们按按键是不能一直按住的,所以我们通常是判断按键从按下到弹起两种状态发生变化了,就认为是有按键按下。

程序上,我们可以把每次按键状态都存储起来,当下一次按键状态读进来的时候,与当前按键状态做比较,如果发现这两次按键状态不一致,就说明按键发生动作了,当上一次的状态是未按下、现在是按下,此时的按键动作就是“按下”;当上一次的状态是按下、现在是未按下,此时的按键动作就是“弹起”。显然,每次按键动作都会包含一次“按下”动作和一次“弹起”动作,我们可以任选一个动作来执行程序,或者两个都用以执行不同的程序也是可以的。下面还是用程序来直观的看一下。

#include

sbit KEY1 = P2^4;

sbit KEY2 = P2^5;

sbit KEY3 = P2^6;

sbit KEY4 = P2^7;

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

unsigned char code LedChar[] = {

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

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

}; //数码管真值表

void main(void)

{

bit backup = 1; //按键值备份,保存前一次的扫描值

unsigned char counter = 0; //计数器记录按键按下的次数

//选择最右边的数码管进行显示

P0 = LedChar[counter];

ADDR0 = 0;

ADDR1 = 0;

ADDR2 = 0;

ADDR3 = 1;

ENLED = 0;

//选中第一行按键以进行扫描

P2 = 0xF7;

while(1)

{

if (KEY4 != backup) //只取KEY4为例,当前值与前一次值不相等时,说明按键有动作

{

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

{

counter++; //计数器+1

if (counter >= 10)

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

counter = 0;

}

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

}

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

}

}

}

在这个程序中,我们以K4为例,按一次按键,就会产生“按下”和“弹起”两个动态的动作,我们选择在“弹起”时对数码管进行加1操作。理论是如此,大家可以多按几次,是不是会发生这样一种现象:有的时候我明明只按了一下按键,但数字却加了不止1,而是2或者更多?但是我们的程序并没有任何逻辑上的错误,这是怎么回事呢?于是我们就得来说说按键抖动和消抖了。

通常按键所用的开关都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动,如图13-5所示。图13-5 按键抖动状态图

单片机独立按键与矩阵按键原理图

按键稳定闭合时间长短是由操作人员决定的,通常都会在100ms以上,刻意快速按的话能达到40-50ms左右,很难再低了。抖动时间是由按键的机械特性决定的,一般是都会在10ms以下,为了确保程序对按键的一次闭合或者一次断开只响应一次,必须进行按键的消抖处理。当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。按键消抖可分为硬件消抖和软件消抖。

硬件消抖就是在按键上并联一个电容,如图13-6所示,利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。但实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度。所以实际中使用的并不多。图13-6 电容消抖

单片机独立按键与矩阵按键原理图

在绝大多数情况下,我们是用软件即程序来实现消抖的。最简单的消抖原理,就是当检测到按键状态变化后,先等待一个10ms左右的延时瞬间,让抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就刻意确认按键已经稳定的动作了。将上边的程序稍加改动,如下所示。

#include

sbit KEY1 = P2^4;

sbit KEY2 = P2^5;

sbit KEY3 = P2^6;

sbit KEY4 = P2^7;

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

unsigned char code LedChar[] = {

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

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

}; //数码管真值表

void delay(void); //延时函数声明

void main(void)

{

bit keybuf = 1; //按键值暂存,临时保存按键的扫描值

bit backup = 1; //按键值备份,保存前一次的扫描值

unsigned char counter = 0; //计数器记录按键按下的次数

//选择最右边的数码管进行显示

P0 = LedChar[counter];

ADDR0 = 0;

ADDR1 = 0;

ADDR2 = 0;

ADDR3 = 1;

ENLED = 0;

//选中第一行按键以进行扫描

P2 = 0xF7;

while(1)

{

keybuf = KEY4; //只取KEY4为例,把当前扫描值暂存

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

{

delay(); //延时大约10ms

if (keybuf == KEY4) //判断扫描值有没有发生改变,即按键抖动

{

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

{

counter++; //计数器+1

if (counter >= 10)

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

counter = 0;

}

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

}

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

}

}

}

}

void delay(void)

{

unsigned int i = 1000;

while (i--); //通过debug的KEIL软件延时方式计算得出大概是10ms

}

这个程序用了一个简单的算法实现了按键的消抖。作为这种很简单的演示程序,我们可以这样来写,但是实际工程开发的时候,我们的程序量很大,各种状态值也很多,我们while(1)的这个主循环要不停的扫描各种状态值是否有发生变化的,如果程序中间加了这种delay延时操作后,很可能某一事件发生了,但是我们程序还在进行delay延时操作中,当这个事件发生完了,我们还在delay操作中,当我们delay完事再去检查的时候,已经晚了,已经检测不到那个事件了。为了避免这种情况的发生,我们要尽量缩短while(1)循环一次所用的事件,而需要进行长时间延时的操作,必须想其它的办法来处理。

那么我们如何处理这种延时问题呢?其实除了这种简单的延时,我们还有更优异的方法来处理按键抖动问题。举个例子:我们启用一个定时中断,每2ms进一次中断,扫描一次按键状态并且存储起来,连续扫描8次后,看看这连续8次的按键状态是否是一致的。8次按键的时间大概是16ms,这16ms内如果按键状态一直保持一种状态,那就可以确定现在按键是稳定的阶段,并非处于抖动的阶段,如图13-7图13-7 按键连续判断

假如左边时间是起始0时刻,每经过2ms左移一次,每移动一次,判断当前连续的8次按键状态是不是全1或者全0,如果是全1则判定为弹起,如果是全0则判定为按下,如果0和1交错,就认为是抖动,不做任何判定。想一下,这样是不是比简单的延时更加可靠?

利用这种方法,就可以避免通过直接延时按键消抖占用CPU时间,而是转化成了一种按键状态判定而非按键过程判断,我们只对当前按键的连续16ms的8次状态进行判断,而不再关心它在这16ms内都做了什么事情,我们来看看这个程序怎么写。

#include

sbit KEY1 = P2^4;

sbit KEY2 = P2^5;

sbit KEY3 = P2^6;

sbit KEY4 = P2^7;

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

unsigned char code LedChar[] = {

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

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

};

bit KeySta = 1; //当前按键状态

void main(void)

{

bit backup = 1; //按键值备份,保存前一次的值

unsigned char counter = 0; //计数器记录按键按下的次数

//选择最右边的数码管进行显示

P0 = LedChar[counter];

ADDR0 = 0;

ADDR1 = 0;

ADDR2 = 0;

ADDR3 = 1;

ENLED = 0;

//选中第一行按键以进行扫描

P2 = 0xF7;

//配置T0工作在模式1,定时2ms

TMOD = 0x01;

TH0 = 0xF8;

TL0 = 0xCD;

TR0 = 1;

ET0 = 1;

EA = 1;

while(1)

{

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

{

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

{

counter++; //计数器+1

if (counter >= 10)

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

counter = 0;

}

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

}

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

}

}

}

void InterruptTimer0() interrupt 1

{

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

TH0 = 0xF8; //溢出后进入中断重新赋值

TL0 = 0xCD;

keybuf = (keybuf

if (keybuf == 0x00)

{ //当连续8次扫描值都为0,即16ms内都只检测到按下状态时,可认为按键已按下

KeySta = 0; //按键状态值为按下

}

else if (keybuf == 0xFF)

{ //当连续8次扫描值都为1,即16ms内都只检测到弹起状态时,可认为按键已弹起

KeySta = 1; //按键状态值为弹起

}

else

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

}

这个算法是我们在工程中经常使用按键所总结的一个比较好的方法,介绍给大家,今后都可以用这种方法消抖了。当然,按键消抖也还有其它的方法,程序实现更是多种多样,大家也可以再多考虑下其它的算法,拓展下思路。这个程序有一个新知识点,就是bit类型的变量,这个在标准C语言里边是没有的。51单片机有一种特殊的变量类型就是bit型,比如unsigned char型是定义了一个无符号的8位的数据,它占用一个字节(Byte)的内存,而bit型是1位数据,只占用1个位(bit)的内存,用法和标准C中其他的基本数据类型是一致的。它的优点就是节省内存空间,8个bit型变量才相当于1个char型变量所占用的空间。虽然它只有0和1两个值,但也已经可以表示很多东西了,比如:按键的按下和弹起、LED灯的亮和灭、三极管的导通与关断、开关的闭合与断开,联想一下已经学过的内容,它是不是能用最小的内存代价来完成很多工作呢?

13.4矩阵按键

我们讲了独立按键,大家已经简单认识了矩阵按键是什么样子了。矩阵按键相当于4组每组各4个独立按键,一共是16个按键。那我们如何区分这些按键呢?想一下我们生活所在的地球,要想确定我们所在的位置,就要借助经纬线,而矩阵按键就是通过行线和列线来确定哪个按键被按下。在程序中我们是如何进行的呢?

前边讲过,我们的按键按下通常都会保持100ms以上的,那我们程序上就每次快速的让矩阵按键的KeyOut其中一个输出低电平,其他三个输出高电平,判断当前列的按键的状态,下次再让另外一个KeyOut输出低电平,另外三个高电平,再次判断列,通过程序快速执行不断的循环判断,就可以最终确定有哪个按键按下,这个是不是和我们动态刷新数码管有点类似?数码管我们在动态赋值,而按键这里我们在动态读取状态。消抖方式依然采取检测连续状态的方式,只是我们现在连续检测4次就可以了。看下我们的程序,这个程序是按下我们的16个按键K1~K16,对应在最右边的数码管显示0~F,大家学一下矩阵按键的基本用法和矩阵按键消抖的方法。

#include

sbit KEY_IN_1 = P2^4; //矩阵按键的扫描输入引脚1

sbit KEY_IN_2 = P2^5; //矩阵按键的扫描输入引脚2

sbit KEY_IN_3 = P2^6; //矩阵按键的扫描输入引脚3

sbit KEY_IN_4 = P2^7; //矩阵按键的扫描输入引脚4

sbit KEY_OUT_1 = P2^3; //矩阵按键的扫描输出引脚1

sbit KEY_OUT_2 = P2^2; //矩阵按键的扫描输出引脚2

sbit KEY_OUT_3 = P2^1; //矩阵按键的扫描输出引脚3

sbit KEY_OUT_4 = P2^0; //矩阵按键的扫描输出引脚4

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

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}, //bit类型不能定义数组,因此定义成unsigned char

{1, 1, 1, 1},

{1, 1, 1, 1},

{1, 1, 1, 1}

};

void main(void)

{

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}

};

//选择最右边的数码管进行显示

P0 = 0xFF;

ADDR0 = 0;

ADDR1 = 0;

ADDR2 = 0;

ADDR3 = 1;

ENLED = 0;

//配置T0工作在模式1,定时1ms

TMOD = 0x01;

TH0 = 0xFC;

TL0 = 0x67;

TR0 = 1;

ET0 = 1;

EA = 1;

while(1)

{

//检索按键状态的变化

for (i=0; i

{

for (j=0; j

{

if (backup[j] != KeySta[j]) //判断按键动作

{

if (backup[j] == 0) //判断按键弹起

{

P0 = LedChar[i*4+j]; //执行按键动作

}

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

}

}

}

}

}

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]

keybuf[keyout][1] = (keybuf[keyout][1]

keybuf[keyout][2] = (keybuf[keyout][2]

keybuf[keyout][3] = (keybuf[keyout][3]

//消抖后更新按键状态

for (i=0; i

{

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

{ //连续4次扫描值为0,即16ms(4*4ms)内都只检测到按下状态时,可认为按键已按下

KeySta[keyout] = 0;

}

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

{ //连续4次扫描值为1,即16ms(4*4ms)内都只检测到弹起状态时,可认为按键已弹起

KeySta[keyout] = 1;

}

}

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

keyout++;

keyout &= 0x03; //用跟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;

}

}

这个程序是一个比较简单的按键程序,但是大家要把按键消抖和矩阵按键检测机制充分理解透彻,这块内容今后就是你的一个技术积累了。

13.5按键、数码管简单加法运算

这一小节内容只有一个程序,使用我们的矩阵按键实现计算器中简单的整数加法运算,这是我们第一次做一个算的上的综合性程序,实现了按键和数码管以及C语言灵活运用的一个例程。作为初学者针对这种程序的学习方式是,先从头到尾读一到三遍,边读边理解,然后边抄边理解,彻底理解透彻后,自己尝试独立写出来。完全采用记忆模式来学习这种例程,一两个例程你感觉不到什么提高,当这种例程背过上百八十个的时候,厚积薄发的感觉就会体现出来了。同时,在抄读的过程中注意学习我们程序的编程规范,尽量规整一些。

#include

sbit KEY_IN_1 = P2^4; //矩阵按键的扫描输入引脚1

sbit KEY_IN_2 = P2^5; //矩阵按键的扫描输入引脚2

sbit KEY_IN_3 = P2^6; //矩阵按键的扫描输入引脚3

sbit KEY_IN_4 = P2^7; //矩阵按键的扫描输入引脚4

sbit KEY_OUT_1 = P2^3; //矩阵按键的扫描输出引脚1

sbit KEY_OUT_2 = P2^2; //矩阵按键的扫描输出引脚2

sbit KEY_OUT_3 = P2^1; //矩阵按键的扫描输出引脚3

sbit KEY_OUT_4 = P2^0; //矩阵按键的扫描输出引脚4

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

unsigned char code LedChar[] = {

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

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

}; //数码管真值表

const unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到PC标准键盘键码的映射表

{ '1', '2', '3', 0x26 }, //数字键1、数字键2、数字键3、向上键

{ '4', '5', '6', 0x25 }, //数字键4、数字键5、数字键6、向左键

{ '7', '8', '9', 0x28 }, //数字键7、数字键8、数字键9、向下键

{ '0', 0x1B, 0x0D, 0x27 } //数字键0、ESC键、 回车键、 向右键

};

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

{1, 1, 1, 1},

{1, 1, 1, 1},

{1, 1, 1, 1},

{1, 1, 1, 1}

}; //由于数组不能定义成bit型,这里定义成unsigned char型

unsigned char LedBuf[6] = { //数码管动态扫描显示缓冲区

0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF

};

void DisplayNum(unsigned long num);

void KeyAction(unsigned char keycode);

void main(void)

{

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}

};

//选择数码管进行显示

P0 = 0xFF;

ADDR3 = 1;

ENLED = 0;

//配置T0工作在模式1,定时1ms

TMOD = 0x01;

TH0 = 0xFC;

TL0 = 0x67;

TR0 = 1;

ET0 = 1;

EA = 1;

while(1)

{

//检索按键状态的变化

for (i=0; i

{

for (j=0; j

{

if (backup[j] != KeySta[j])

{

if (backup[j] == 0) //按键弹起时执行动作

{

KeyAction(KeyCodeMap[j]);

}

backup[j] = KeySta[j];

}

}

}

}

}

void KeyAction(unsigned char keycode)

{

static unsigned long result = 0; //用于保存运算结果

static unsigned long addend = 0; //用于保存输入的加数

if ((keycode>='0') && (keycode

{

addend = (addend*10) + (keycode-'0'); //原数据扩大10倍,由新输入的数字填充其个位

DisplayNum(addend); //运算结果显示到数码管

}

else if (keycode == 0x26) //向上键用作加号,执行加法或连加运算

{

result += addend; //进行加法运算

addend = 0;

DisplayNum(result); //运算结果显示到数码管

}

else if (keycode == 0x0D) //回车键,执行加法运算(实际效果与加号并无区别)

{

result += addend; //进行加法运算

addend = 0;

DisplayNum(result); //运算结果显示到数码管

}

else if (keycode == 0x1B) //Esc键,清零结果

{

addend = 0;

result = 0;

DisplayNum(addend); //清零后的加数显示到数码管

}

}

void DisplayNum(unsigned long num)

{

signed char i;

unsigned char buf[6];

for (i=0; i

{

buf = num % 10;

num /= 10;

}

for (i=5; i>=1; i--) //从最高位起,遇到0即转换为空格,遇到非0即退出

{

if (buf == 0)

{

LedBuf = 0xFF;

}

else

{

break;

}

}

for ( ; i>=0; i--) //剩余低位都如实转换为数字

{

LedBuf = LedChar[buf];

}

}

void InterruptTimer0() interrupt 1

{

unsigned char i;

static unsigned char ledcnt = 0; //数码管扫描计数器

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]

keybuf[keyout][1] = (keybuf[keyout][1]

keybuf[keyout][2] = (keybuf[keyout][2]

keybuf[keyout][3] = (keybuf[keyout][3]

//消抖后更新按键状态

for (i=0; i

{

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

{ //连续4次扫描值为0,即16ms(4*4ms)内都只检测到按下状态时,可认为按键已按下

KeySta[keyout] = 0;

}

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

{ //连续4次扫描值为1,即16ms(4*4ms)内都只检测到弹起状态时,可认为按键已弹起

KeySta[keyout] = 1;

}

}

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

keyout++;

keyout &= 0x03;

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;

}

//执行数码管动态扫描显示

P0 = 0xFF;

switch (ledcnt)

{

case 0: ADDR0=0; ADDR1=0; ADDR2=0; break;

case 1: ADDR0=1; ADDR1=0; ADDR2=0; break;

case 2: ADDR0=0; ADDR1=1; ADDR2=0; break;

case 3: ADDR0=1; ADDR1=1; ADDR2=0; break;

case 4: ADDR0=0; ADDR1=0; ADDR2=1; break;

case 5: ADDR0=1; ADDR1=0; ADDR2=1; break;

default: break;

}

P0 = LedBuf[ledcnt];

ledcnt++;

if (ledcnt >= 6)

{

ledcnt = 0;

}

}