《51单片机入门:矩阵按键的应用》

文章目录

  • 1.按键
  • 1.1.独立按键
  • 1.2.矩阵按键
  • 2.按键的扫描与抖动
  • 2.1.独立按键的扫描
  • 2.2.按键抖动与消抖
  • 2.3.矩阵按键的扫描
  • 3.简易计算器
  • 1.按键

    1.1.独立按键

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

    这个电路中按键的原理我们清楚了,但是实际上在我们的单片机 IO 口内部,也有一个上拉电阻的存在。我们的按键是接到了 P2 口上,P2 口上电默认是准双向 IO 口,我们来简单了解一下这个准双向 IO 口的电路。
    方框内的电路都是指单片机内部部分,方框外的就是我们外接的上拉电阻和按键。

    这个地方大家要注意一下,就是当我们要读取外部按键信号的时候,单片机必须先给该引脚写“1”,也就是高电平,这样我们才能正确读取到外部按键信号,我们来分析一下缘由。

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

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

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

    从上面的分析就可以得出一个结论,这种具有上拉的准双向 IO 口,如果要正常读取外部信号的状态,必须首先得保证自己内部输出的是 1,如果内部输出 0,则无论外部信号是 1 还是 0,这个引脚读进来的都是 0。

    1.2.矩阵按键

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

    如果独立按键理解了,矩阵按键也不难理解,那么我们一起来分析一下。图 8-8 中,一共有 4 组按键,我们只看其中一组。大家认真看一下,如果 out1 输出一个低电平,KeyOut1 就相当于是 GND,是否相当于 4 个独立按键呢。当然这时候 out2、out3、out4 都必须输出高电平,它们都输出高电平才能保证与它们相连的三路按键不会对这一路产生干扰。

    2.按键的扫描与抖动

    2.1.独立按键的扫描

    原理搞清楚了,那么下面我们就先编写一个独立按键的程序,把最基本的功能验证一下。
    原理图如下:

    #include<reg52.h>
    
    sbit LED1 = P2^0 ;
    sbit LED2 = P2^1 ;
    sbit LED3 = P2^2 ;
    sbit LED4 = P2^3 ;
    sbit KEY1 = P3^0 ;
    sbit KEY2 = P3^1 ;
    sbit KEY3 = P3^2 ;
    sbit KEY4 = P3^3 ;
    
    void main()
    {
    	P2 = 0 ; 
    	while(1)
    	{
    		LED1 = ~KEY1 ;  //按下时为 0,对应的 LED 点亮
    		LED2 = ~KEY2 ;
    		LED3 = ~KEY3 ;
    		LED4 = ~KEY4 ;
    	}
    }
    

    当按键按下时,对应按键的输入引脚是 0,对应小灯控制信号也是 0,于是灯就亮了,这说明上述关于按键检测的理论都是可实现的。

    绝大多数情况下,按键是不会一直按住的,所以我们通常检测按键的动作并不是检测一个固定的电平值,而是检测电平值的变化,即按键在按下和弹起这两种状态之间的变化,只要发生了这种变化就说明现在按键产生动作了。

    程序上,我们可以把每次扫描到的按键状态都保存起来,当一次按键状态扫描进来的时候,与前一次的状态做比较,如果发现这两次按键状态不一致,就说明按键产生动作了。当上一次的状态是未按下而现在是按下,此时按键的动作就是“按下”;当上一次的状态是按下而现在是未按下,此时按键的动作就是“弹起”。显然,每次按键动作都会包含一次“按下”和一次“弹起”,我们可以任选其一来执行程序,或者两个都用,以执行不同的程序也是可以的。下面就用程序来实现这个功能,程序只取按键 4 为例。

    我们先建立如下的原理图:

    #include<reg52.h>
    
    sbit KEY1 = P3^0 ;
    sbit KEY2 = P3^1 ;
    sbit KEY3 = P3^2 ;
    sbit KEY4 = P3^3 ;
    
    unsigned char code LedChar[] = { //共阳数码管显示字符转换表
     0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
     0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
    };
    
    void main()
    {
    	bit backup = 1; //定义一个位变量,保存前一次扫描的按键值
    	unsigned char cnt = 0; //定义一个计数变量,记录按键按下的次数
    	P0 = ~LedChar[cnt]; //显示按键次数初值
    	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; //更新备份为当前值,以备进行下次比较
    		}
    	}
    }
    
    

    先来介绍出现在程序中的一个新知识点,就是变量类型——bit,这个在标准 C 语言里边是没有的。51 单片机有一种特殊的变量类型就是 bit 型。比如 unsigned char 型是定义了一个无符号的 8 位的数据,它占用一个字节(Byte)的内存,而 bit 型是 1 位数据,只占用 1 个位(bit)的内存,用法和标准 C 中其他的基本数据类型是一致的。它的优点就是节省内存空间,8 个bit 型变量才相当于 1 个 char 型变量所占用的空间。虽然它只有 0 和 1 两个值,但也已经可以表示很多东西了,比如:按键的按下和弹起、LED 灯的亮和灭、三极管的导通与关断等等,联想一下已经学过的内容,它是不是能用最小的内存代价来完成很多工作呢?

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

    2.2.按键抖动与消抖

    虽然这个问题在《51单片机入门——数码管》的后半部分有讲到过,在此还是在说一下。

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

    硬件消抖就是在按键上并联一个电容,如下图所示:

    利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。但实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度,所以实际中使用的并不多(基本用于单片机的复位按键)。

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

    #include<reg52.h>
    
    sbit KEY1 = P3^0 ;
    sbit KEY2 = P3^1 ;
    sbit KEY3 = P3^2 ;
    sbit KEY4 = P3^3 ;
    
    unsigned char code LedChar[] = { //共阳数码管显示字符转换表
     0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
     0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
    };
    
    void delay();
    
    void main()
    {
    	bit backup = 1; //定义一个位变量,保存前一次扫描的按键值
    	unsigned char cnt = 0; //定义一个计数变量,记录按键按下的次数
    	P0 = ~LedChar[cnt]; //显示按键次数初值
    	while (1)
    	{
    		if (KEY4 != backup) //当前值与前次值不相等说明此时按键有动作
    		{
    			delay(10); // 延时大约10ms
    			if (backup == 0) //如果前次值为 0,则说明当前是由 0 变 1,即按键弹起
    			{
    				cnt++; //按键次数+1
    				if (cnt >= 10)
    				{ //只用 1 个数码管显示,所以加到 10 就清零重新开始
    					cnt = 0;
    				}
    				P0 = ~LedChar[cnt]; //计数值显示到数码管上
    			}
    			backup = KEY4; //更新备份为当前值,以备进行下次比较
    		}
    	}
    }
    
    void delay(unsigned char n)
     {
     	unsigned char i j;
    	for(i = 0 ; i < 123 ; i ++)
    	{
    		for(j = 0 ; j < 123 ; j ++) ;
    	}
     }
    
    

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

    在此我给出两种解决办法:

    1.添加一个变量

    void key()
     {
    	if(key0 == 0 && a == 0)
    	{
    		a = 1;
    	}
    	if(a == 1 && key0 == 1)
    	{
    		a = 0;
    		............. // 需要执行的代码
    	}
     }
    

    2.我们启用一个定时中断,每 2ms 进一次中断,扫描一次按键状态并且存储起来,连续扫描 8 次后,看看这连续 8 次的按键状态是否是一致的。

    8 次按键的时间大概是 16ms,这 16ms 内如果按键状态一直保持一致,那就可以确定现在按
    键处于稳定的阶段,而非处于抖动的阶段,如下图:

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

    #include<reg52.h>
    
    sbit KEY1 = P3^0 ;
    sbit KEY2 = P3^1 ;
    sbit KEY3 = P3^2 ;
    sbit KEY4 = P3^3 ;
    
    unsigned char code LedChar[] = { //共阳数码管显示字符转换表
     0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
     0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
    };
    
    void main()
    {
    	bit backup = 1; //定义一个位变量,保存前一次扫描的按键值
    	unsigned char cnt = 0; //定义一个计数变量,记录按键按下的次数
    	P0 = ~LedChar[cnt]; //显示按键次数初值
    	TMOD = 0x01; //设置 T0 为模式 1
    	TH0 = 0xF8; //为 T0 赋初值 0xF8CD,定时 2ms
    	TL0 = 0xCD;
    	ET0 = 1; //使能 T0 中断
    	EA = 1; //使能总中断
    	while (1)
    	{
    		if (KeySta != backup) //当前值与前次值不相等说明此时按键有动作
    		{
    			if (backup == 0) //如果前次值为 0,则说明当前是由 0 变 1,即按键弹起
    			{
    				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)
    	{ //连续 8 次扫描值都为 0,即 16ms 内都只检测到按下状态时,可认为按键已按下
    		KeySta = 0;
    	}
    	else if (keybuf == 0xFF)
    	{ //连续 8 次扫描值都为 1,即 16ms 内都只检测到弹起状态时,可认为按键已弹起
    		KeySta = 1;
    	}
    	else
    	{} //其它情况则说明按键状态尚未稳定,则不对 KeySta 变量值进行更新
    }
    

    这个算法是我们在实际工程中经常使用按键所总结的一个比较好的方法,介绍给大家,今后都可以用这种方法消抖了。当然,按键消抖也还有其它的方法,程序实现更是多种多样,大家也可以再多考虑下其它的算法,拓展下思路。

    2.3.矩阵按键的扫描

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

    前边讲过,按键按下通常都会保持 100ms 以上,如果在按键扫描中断中,我们每次让矩阵按键的一个 out 输出低电平,其它三个输出高电平,判断当前所有 Int 的状态,下次中断时再让下一个out 输出低电平,其它三个输出高电平,再次判断所有 Int,通过快速的中断不停的循环进行判断,就可以最终确定哪个按键按下了,这个原理是不是跟数码管动态扫描有点类似?数码管我们在动态赋值,而按键这里我们在动态读取状态。至于扫描间隔时间和消抖时间,因为现在有 4 个 out 输出,要中断 4 次才能完成一次全部按键的扫描,显然再采用 2ms 中断判断 8 次扫描值的方式时间就太长(248=64ms),那么我们就改用 1ms 中断判断 4 次采样值,这样消抖时间还 16ms(144)。下面就用程序实现出来,程序循环扫描板子上的 K1~K16 这 16 个矩阵按键,分离出按键动作并在按键按下时把当前按键的编号显示在一位数码管上(用 0~F 表示,显示值=按键编号 -1)。

    #include<reg52.h>
    
    typedef unsigned char uchar ;
    typedef unsigned int uint ;
    typedef unsigned long ulong ;
    
    sbit out1 = P2^7 ;
    sbit out2 = P2^6 ;
    sbit out3 = P2^5 ;
    sbit out4 = P2^4 ;
    sbit int1 = P2^3 ;
    sbit int2 = P2^2 ;
    sbit int3 = P2^1 ;
    sbit int4 = P2^0 ;
    
    uchar ledChar[] = {//共阳极数码管0~F
    	0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
    	0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
    };
    
    uchar keySta[4][4] = {//矩阵按键的当前状态 1为高电平 ,0为低电平
    	{1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} 	
    };
    
    void KeyDriver() ;
    
    void main()
    {
    	EA = 1 ;
    	TMOD = 0x01 ;
    	TH0 = 0xfc ;
    	TL0 = 0x67 ;
    	ET0 = 1 ;
    	TR0 = 1 ;
    	P0 = ~ledChar[0] ;
    	while(1)
    	{
    		KeyDriver() ;	
    	}
    }
    
    /* 按键驱动函数,检测按键动作,调度相应动作函数*/
    void KeyDriver()
    {
    	uchar i , j ;
    	static char backup[4][4] = {
    		{1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1}
    	} ;
    	for (i = 0 ; i < 4 ; i ++)
    	{
    		for (j = 0 ; j < 4 ; j ++)
    		{
    			if (keySta[i][j] != backup[i][j])
    			{
    				if (backup[i][j] != 0)
    				{
    					P0 = ~ledChar[i*4+j]; //将编号显示到数码管
    				}
    				backup[i][j] = keySta[i][j] ;
    			}
    		}
    	}
    }
    
    /* 按键扫描函数 */
    void KeyScan()
    {
    	uchar i ;
    	static uchar keyout = 0 ;
    	static uchar keybuf[4][4] = {
    		{0xff , 0xff , 0xff , 0xff} , {0xff , 0xff , 0xff , 0xff} , 
    		{0xff , 0xff , 0xff , 0xff} , {0xff , 0xff , 0xff , 0xff} 
    	} ;
    	keybuf[keyout][0] = (keybuf[keyout][0] << 1) | int1 ;
    	keybuf[keyout][1] = (keybuf[keyout][1] << 1) | int2 ;
    	keybuf[keyout][2] = (keybuf[keyout][2] << 1) | int3 ;
    	keybuf[keyout][3] = (keybuf[keyout][3] << 1) | int4 ; 
    	for (i = 0 ; i < 4 ; i ++)
    	{
    		if ((keybuf[keyout][i] & 0x0f) == 0x00)
    		{
    			keySta[keyout][i] = 0 ;
    		}
    		else if ((keybuf[keyout][i] & 0x0f) == 0x0f)
    		{
    			keySta[keyout][i] = 1 ;
    		}
    	}
    	keyout ++ ;
    	keyout = keyout & 0x03 ;
    	switch(keyout)
    	{
    		case 0: out4 = 1 ; out1 = 0 ; break ;
    		case 1: out1 = 1 ; out2 = 0 ; break ;
    		case 2: out2 = 1 ; out3 = 0 ; break ;
    		case 3: out3 = 1 ; out4 = 0 ; break ;
    		default: break ;
    	}
    }
    
    /* T0 中断服务函数,用于按键状态的扫描并消抖 */
    void InterruptTimer0() interrupt 1
    {
    	TH0 = 0xfc ;
    	TL0 = 0x67 ;
    	KeyScan() ;
    }
    

    这个程序完成了矩阵按键的扫描、消抖、动作分离的全部内容,希望大家认真研究一下,彻底掌握矩阵按键的原理和应用方法。在程序中还有两点值得说明一下。

    首先,可能你已经发现了,中断函数中扫描 Int 输入和切换 out 输出的顺序与前面提到的顺序不同,程序中我首先对所有的 int 输入做了扫描、消抖,然后才切换到了下一次的 out 输出,也就是说我们中断每次扫描的实际是上一次输出选择的那行按键,这是为什么呢?因为任何信号从输出到稳定都需要一个时间,有时它足够快而有时却不够快,这取决于具体的电路设计,我们这里的输入输出顺序的颠倒就是为了让输出信号有足够的时间(一次中断间隔)来稳定,并有足够的时间来完成它对输入的影响,当你的按键电路中还有硬件电容消抖时,这样处理就是绝对必要的了,虽然这样使得程序理解起来有点绕,但它的适应性是最好的,换个说法就是,这段程序足够“健壮”,足以应对各种恶劣情况。

    其次,是一点小小的编程技巧。注意看 keyout = keyout & 0x03;这一行,在这里我是要让keyout 在 0~3 之间变化,加到 4 就自动归零,按照常规你可以用前面讲过的 if 语句轻松实现,但是你现在看一下这样程序是不是同样可以做到这一点呢?因为 0、1、2、3 这四个数值正好占用 2 个二进制的位,所以我们把一个字节的高 6 位一直清零的话,这个字节的值自然就是一种到 4 归零的效果了。看一下,这样一句代码比 if 语句要更为简洁吧,而效果完全一样。

    3.简易计算器

    学到这里,我们已经掌握了一种显示设备和一种输入设备的使用,那么是不是可以来做点综合性的实验了。好吧,那我们就来做一个简易的加法计算器,用程序实现从板子上标有0~9 数字的按键输入相应数字,该数字要实时显示到数码管上,用标有向上箭头的按键代替加号,用标有向左箭头的按键代替减号,用标有向下箭头的按键代替乘号,用标有向右箭头的按键代替除号。虽然这远不是一个完善的计算器程序,但作为初学者也足够你研究一阵子了。

    首先,本程序相对于之前的例程要复杂得多,需要完成的工作也多得多,所以我们把各个子功能都做成独立的函数,以使程序便于编写和维护。大家分析程序的时候就从主函数和中断函数入手,随着程序的流程进行就可以了。大家可以体会体会划分函数的好处,想想如果还是只有主函数和中断函数来实现的话程序会是什么样子。

    其次,大家可以看到我们再把矩阵按键扫描分离出动作以后,并没有直接使用行列数所组成的数值作为分支判断执行动作的依据,而是把抽象的行列数转换为了一种叫做标准键盘键码(就是电脑键盘的编码)的数据,然后用得到的这个数据作为下一步分支判断执行动作的依据,为什么多此一举呢?有两层含义:第一,尽量让自己设计的东西(包括硬件和软件)向已有的行业规范或标准看齐,这样有助于别人理解认可你的设计,也有助于你的设计与别人的设计相对接,毕竟标准就是为此而生的嘛。第二,有助于程序的层次化而方便维护与移植,比如我们现在用的按键是 44 的,但如果后续又增加了一行成了 45 的,那么由行列数组成的编号可能就变了,我们就要在程序的各个分支中查找修改,稍不留神就会出错,而采用这种转换后,我们则只需要维护 keyCodeMap 这样一个数组表格就行了,看上去就像是把程序的底层驱动与应用层的功能实现函数分离开了,应用层不用关心底层的实现细节,底层改变后也无需在应用层中做相应修改,两层程序之间是一种标准化的接口。这就是程序的层次化,而层次化是构建复杂系统的必备条件,那么现在就先通过简单的示例来学习一下吧。

    #include<reg52.h>
    
    typedef unsigned char uchar ;
    typedef unsigned int uint ;
    typedef unsigned long ulong ;
    
    sbit out1 = P2^0 ;
    sbit out2 = P2^1 ;
    sbit out3 = P2^2 ;
    sbit out4 = P2^3 ;
    sbit int1 = P2^4 ;
    sbit int2 = P2^5 ;
    sbit int3 = P2^6 ;
    sbit int4 = P2^7 ;
    
    uchar ledChar[] = {//共阳极数码管0~F
    	0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
    	0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
    };
    
    uchar ledBuff[8] = {// 数码管显示缓冲区
    	0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 
    } ;
    
    uchar keySta[4][4] = {//矩阵按键的当前状态 1为高电平 ,0为低电平
    	{1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} 	
    };
    
    uchar code keyCodeMap[4][4] = {// 矩阵按键编号到标准键盘键码的映射表
    	{0x31 , 0x32 , 0x33 , 0x26} , // 数字键1、数字键2、数字键3、向上键
    	{0x34 , 0x35 , 0x36 , 0x25} , // 数字键4、数字键5、数字键6、向左键
    	{0x37 , 0x38 , 0x39 , 0x28} , // 数字键7、数字键8、数字键9、向下键
    	{0x30 , 0x1b , 0x0d , 0x27}	  // 数字键0、ESC键、回车键、向右键
    } ;
    
    void KeyDriver() ;
    
    void main()
    {
    	EA = 1 ;
    	TMOD = 0x01 ;
    	TH0 = 0xfc ;
    	TL0 = 0x67 ;
    	ET0 = 1 ;
    	TR0 = 1 ;
    	ledBuff[0] = ~ledChar[0] ;
    	while(1)
    	{
    		KeyDriver() ;	
    	}
    }
    
    /* 将一个无符号长整型数字显示数码管上,num-待显示数字 */
    void ShowNumber(ulong num)
    {
    	signed char i ;
    	uchar buf[8] ;
    	for (i = 0 ; i < 8 ; i ++)
    	{
    		buf[i] = num % 10 ;
    		num = num / 10 ;
    	}
    	for (i = 7 ; i >= 1 ; i --)
    	{
    		if(buf[i] == 0)
    		{
    			ledBuff[i] = 0x00 ;
    		}
    		else 
    			break ;
    	}
    	for ( ; i >= 0 ; i --)
    	{
    		ledBuff[i] = ~ledChar[buf[i]] ;
    	}
    }
    
    /* 按键动作函数,根据键码执行相应的操作 , keyCode-按键键码 */
    void KeyAction(uchar keyCode)
    {
    	static ulong result = 0 ; //用于保存运算结果
    	static ulong addend = 0 ; //用于保存输入的加数
    	static ulong subtract = 0 ; // 用于保存输入的减数
    	static ulong multiplication = 1 ; //用于保存输入的乘数
    	static ulong division = 1 ; // 用于保存输入的除数
    	static uchar suspend = 0 ; // 用于判断使用哪种法则
    	if ((keyCode >= 0x30) && (keyCode <= 0x39))
    	{
    		result = (result * 10) + (keyCode - 0x30) ;
    		ShowNumber(result) ;
    	}
    	else if (keyCode == 0x26) //加法运算
    	{
    		addend = result ;
    		result = 0 ;
    		suspend = 1 ;
    		ShowNumber(addend) ;
    	}
    	else if (keyCode == 0x25) //减法运算
    	{
    		subtract = result ;
    		result = 0 ;
    		suspend = 2 ;
    		ShowNumber(subtract) ;	
    	}
    	else if (keyCode == 0x28) //乘法运算
    	{
    		multiplication = result ;
    		result = 0 ;
    		suspend = 3 ;
    		ShowNumber(multiplication) ;	
    	}
    	else if (keyCode == 0x27) //除法运算
    	{
    		division = result ;
    		result = 0 ;
    		suspend = 4 ;
    		ShowNumber(division) ;
    	}
    	else if (keyCode == 0x0d)
    	{
    		if (suspend == 1)
    		{
    			result = addend + result ;
    			addend = 0 ;	
    		}
    		else if (suspend == 2)
    		{
    			if (subtract > result)
    			{
    				result = subtract - result ;	
    			}
    			else 
    			{
    				result =result - subtract ;	
    			}
    		}
    		else if (suspend == 3)
    		{
    			result = multiplication * result ;	
    		}
    		else if (suspend == 4)
    		{
    			result = division / result ;	
    		}
    		ShowNumber(result) ;
    	}
    	else if (keyCode == 0x1b)
    	{
    		result = 0 ; 
    		addend = 0 ; 
    		subtract = 0 ; 
    		multiplication = 1 ;
    		division = 1 ;
    		ShowNumber(result) ;
    	}
    }
    
    /* 按键驱动函数,检测按键动作,调度相应动作函数*/
    void KeyDriver()
    {
    	uchar i , j ;
    	static char backup[4][4] = {
    		{1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1} , {1 , 1 , 1 , 1}
    	} ;
    	for (i = 0 ; i < 4 ; i ++)
    	{
    		for (j = 0 ; j < 4 ; j ++)
    		{
    			if (keySta[i][j] != backup[i][j])
    			{
    				if (backup[i][j] != 0)
    				{
    					KeyAction(keyCodeMap[i][j]) ;
    				}
    				backup[i][j] = keySta[i][j] ;
    			}
    		}
    	}
    }
    
    /* 按键扫描函数 */
    void KeyScan()
    {
    	uchar i ;
    	static uchar keyout = 0 ;
    	static uchar keybuf[4][4] = {
    		{0xff , 0xff , 0xff , 0xff} , {0xff , 0xff , 0xff , 0xff} , 
    		{0xff , 0xff , 0xff , 0xff} , {0xff , 0xff , 0xff , 0xff} 
    	} ;
    	keybuf[keyout][0] = (keybuf[keyout][0] << 1) | int1 ;
    	keybuf[keyout][1] = (keybuf[keyout][1] << 1) | int2 ;
    	keybuf[keyout][2] = (keybuf[keyout][2] << 1) | int3 ;
    	keybuf[keyout][3] = (keybuf[keyout][3] << 1) | int4 ; 
    	for (i = 0 ; i < 4 ; i ++)
    	{
    		if ((keybuf[keyout][i] & 0x0f) == 0x00)
    		{
    			keySta[keyout][i] = 0 ;
    		}
    		else if ((keybuf[keyout][i] & 0x0f) == 0x0f)
    		{
    			keySta[keyout][i] = 1 ;
    		}
    	}
    	keyout ++ ;
    	keyout = keyout & 0x03 ;
    	switch(keyout)
    	{
    		case 0: out4 = 1 ; out1 = 0 ; break ;
    		case 1: out1 = 1 ; out2 = 0 ; break ;
    		case 2: out2 = 1 ; out3 = 0 ; break ;
    		case 3: out3 = 1 ; out4 = 0 ; break ;
    		default: break ;
    	}
    }
    
    /* 数码管扫描函数 */
    void LedScan()
    {
    	static uchar i = 0 ;
    	switch (i)
    	{
    		case 0: P3 = 0x7f ; i ++ ; P0 = ledBuff[0] ; break ;
    		case 1: P3 = 0xbf ; i ++ ; P0 = ledBuff[1] ; break ;
    		case 2: P3 = 0xdf ; i ++ ; P0 = ledBuff[2] ; break ;
    		case 3: P3 = 0xef ; i ++ ; P0 = ledBuff[3] ; break ;
    		case 4: P3 = 0xf7 ; i ++ ; P0 = ledBuff[4] ; break ;
    		case 5: P3 = 0xfb ; i ++ ; P0 = ledBuff[5] ; break ;
    		case 6: P3 = 0xfd ; i ++ ; P0 = ledBuff[6] ; break ;
    		case 7: P3 = 0xfe ; i = 0 ; P0 = ledBuff[7] ; break ;
    		default: break ;
    	}
    }
    
    void InterruptTimer0() interrupt 1
    {
    	TH0 = 0xfc ;
    	TL0 = 0x67 ;
    	KeyScan() ;
    	LedScan() ;
    }
    
    物联沃分享整理
    物联沃-IOTWORD物联网 » 《51单片机入门:矩阵按键的应用》

    发表评论