STC51-键盘检测

        键盘分为编码键盘和非编码键盘。键盘上闭合键的识别由专用的硬件编码器实现, 并产生键编码号或键值的称为编码键盘, 如计算机键盘。而靠软件编程来识别的键盘称为非编码键盘, 在单片机组成的各种系统中, 用的较多的是非编码键盘。非编码键盘又分为独立键盘和行列式(又称矩阵式)键盘。

1 独立键盘检测

        键盘实际上就是一组按键,在单片机外围电路中,通常用到的按键都是机械弹性开关,当开关闭合时,线路导通,开关断开时,线路断开,下图是几种单片机系统常见的按键。

        弹性小按键被按下时闭合,松手后自动断开;自锁式按键按下时闭合且会自动锁住,只有再次按下时才弹起断开。通常我们把自锁试按键当做开关使用,比如实验板上的电源开关就使用自锁按键。单片机的外围输入控制用小弹性按键较好,单片机检测按键的原理是:单片机的I/O口既可作为输出也可作为输入使用,当检测按键时用的是它的输入功能,我们把按键的一端接地,另一端与单片机的某个I/O口相连,开始时先给该I/O口赋一高电平,然后让单片机不断地检测该I/O口是否变为低电平,当按键闭合时,即相当于该I/O口通过按键与地相连,变成低电平,程序一旦检测到I/O口变为低电平则说明按键被按下,然后执行相应的指令。
        按键的连接方法非常简单,如下图左所示,右侧I/O端与单片机的任一I/O口相连。按键在被按下时,其触点电压变化过程如下图右所示。

        从上图右可看出,理想波形与实际波形之间是有区别的,实际波形在按下和释放的瞬间都有抖动现象,抖动时间的长短和按键的机械特性有关,一般为5~10ms。通常我们手动按下键然后立即释放,这个动作中稳定闭合的时间超过20ms。因此单片机在检测键盘是否按下时都要加上去抖动操作,有专用的去抖动电路,也有专用的去抖动芯片,但通常我们用软件延时的方法就能很容易解决抖动问题,而没有必要再添加多余的硬件电路。
        用示波器跟踪不同类型的开关,得到下图的波形,观察波形可以帮助我们对抖动现象有一个直观的了解。下图是一个小的按钮开关在闭合时的抖动现象,水平轴2ms/Div, 抖动间隙大约为10ms,在达到稳定状态前一共有6次变化,频率随时间升高。

在这里插入图片描述

        下图是一个小型继电器在闭合时的抖动现象,水平轴2ms/Div,抖动间隙大约为8ms,在达到稳定状态前一共有13次变化。注意在开始和结束时,几个小的脉冲后伴随较高的频率。

        编写单片机的键盘检测程序时,一般在检测按下时加入去抖延时,检测松手时就不用加了。按键检测流程图如下图所示。

在这里插入图片描述

        实验板上独立按键与单片机链接的原理图如下图所示:

        下面通过一个实例来讲解独立键盘的具体操作方法, 在实验板上实现如下描述。
        用数码管的前两位显示一个十进制数,变化范围为00~59,开始时显示00,每按下S2键一次,数值加1;每按下S3键一次,数值减 1;每按下S4键一次,数值归零;按下S5键一次, 利用定时器功能使数值开始自动每秒加1,再次按下S5键,数值停止自动加1,保持显示原数。新建文件Temp.c, 程序代码如下:
        共阳极数码管:

#include <reg52.h>         //52系列单片机头文件
#define uchar unsigned char
#define uint unsigned int
sbit key1 = P3^4;
sbit key2 = P3^5;
sbit key3 = P3^6;
sbit key4 = P3^7;
sbit dula = P2^5;		   //申明U1锁存器的锁存端
sbit wela = P2^6;		   //申明U2锁存器的锁存端
uchar code table[]={
0xC0, /*/"0"*/
0xF9, /*/"1"*/
0xA4, /*/"2"*/
0xB0, /*/"3"*/
0x99, /*/"4"*/
0x92, /*/"5"*/
0x82, /*/"6"*/
0xF8, /*/"7"*/
0x80, /*/"8"*/
0x90, /*/"9"*/
0x88, /*/"A"*/
0x83, /*/"B"*/
0xC6, /*/"C"*/
0xA1, /*/"D"*/
0x86, /*/"E"*/
0x8E, /*/"F"*/
0x89, /*/"H"*/
0xC7, /*/"L"*/
0xC8, /*/"n"*/
0xC1, /*/"u"*/
0x8C, /*/"P"*/
0xA3, /*/"o"*/
0xBF, /*/"-"*/
0xFF, /*/熄灭*/
0xFF /*/自定义*/
};
void delayms(uint xms);	  
uchar num,numt0;  
void dispaly(uchar numdis)  //显示子函数
{
	uchar shi,ge;
	shi=numdis/10;
	ge=numdis%10;

	dula=1;
	P0=table[shi];  //送段选数据
	dula=0;
	
	P0=0xff	;	  //送位选数据前关闭所有显示,防止打开位选锁存时
	
    wela=1;	      //原来段选数据通过位选锁存器造成混乱
    P0=0x01	;	  //送位选数据
    wela=0;		  //关闭U2锁存器
	delayms(5); //延时

	dula=1;
	P0=table[ge];  //送段选数据
	dula=0;
	
	P0=0xff	;	  //送位选数据前关闭所有显示,防止打开位选锁存时
	
    wela=1;	      //原来段选数据通过位选锁存器造成混乱
    P0=0x02	;	  //送位选数据
    wela=0;		  //关闭U2锁存器
	delayms(5); //延时
}
void init()  //初始化函数
{
  TMOD=0x01;  //设置定时器0为工作方式1(0000 0001)
  TH0=(65536-45872)/256;//装初值50ms一次中断
  TL0=(65536-45872)%256;//装初值50ms一次中断
  EA=1;		//开总中断
  ET0=1;	//开定时器0中断
}
void keyscan()
{
   if(key1==0)
   {
      delayms(10);
	  if(key1==0)
	  {
	    num++;
		if(num==60)	 //当到60时重新归0
		  num=0;
		while(~key1);//等待按键释放
		}
	}
   if(key2==0)
   {
      delayms(10);
	  if(key2==0)
	  {
		if(num==0)	 //当到0时重新归60
		  num=60;
		  num--;
		while(~key2);//等待按键释放
		}
	}
   if(key3==0)
   {
      delayms(10);
	  if(key3==0)
	  {
		num=0;		 //清零
		while(~key3);//等待按键释放
		}
	}
   if(key4==0)
   {
      delayms(10);
	  if(key4==0)
	  {
		while(~key4);//等待按键释放
		TR0=~TR0;//启动或停止定时器0
		}
	}
}
void T0_time()interrupt 1
{
  TH0=(65536-45872)/256;//重装初值
  TH0=(65536-45872)%256;
  numt0++;				//num每加一次判断一次是否到了20次
  if(numt0==20)
  {
    numt0=0;
	num++;
	if(num==60)
	num=0;
	}
}
void main()
{
	init();
	while(1)
	{
	  keyscan();
	  dispaly(num);
	  }
	}

void delayms(uint xms)
{
	uint i,j;
	for(i=xms;i>0;i--)
		for(j=110;j>0;j--);
}

        分析如下:
        1)上例中我们将定时器初始化部分、键盘扫描部分、数码管显示部分等分别写成独立的函数,在主函数中只需方便地直接调用它们就可以了,这样可使程序看上去简洁、明了,修改也很方便。
        2)大家在写程序时一定要注意代码的层次感,一级和一级之间用一个Tab 键隔开,尤其是初写程序的学员,不注意书写格式,程序从头到尾不加任何注释,级与级之间没有任何空格,当程序有问题,回头查询起来很不方便,大家从一开始就要养成良好的书写习惯,具体书写格式可参照本书例程。
        3)在键盘扫描程序中"delayms(10);" 即是去抖延时。在确认按键被按下后,程序中还有语句"while(!key,1);",它的意思是等待按键释放,若按键没有释放则key1始终为0,那么!key1即始终为1,程序就一直停止在这个while语句处,直到按键释放,key1变成了1,才退出这个while语句。通常我们在检测单片机的按键时,要等按键确认释放后才去执行相应的代码。若不加按键释放检测。由于单片机执行代码的速度非常快,而且是循环检测按键,所以当按下一个键时,单片机会在程序循环中多次检测到键被按下,从而造成错误的结果,大家可不加按键释放检测代码,编译程序下载后测试,当按下S2键时,会看到数码管上的数值变化很快,而且没有规律。

2 矩阵键盘检测

        独立键盘与单片机连接时,每一个按键都需要单片机的一个I/O口,若某单片机系统需较多按键,如果用独立按键便会占用过多的I/O口资源。单片机系统中I/O口资源往往比较宝贵,当用到多个按键时,为了节省I/O口线,我们引入矩阵键盘。
        我们以4X4矩阵键盘为例讲解其工作原理和检测方法。将16个按键排成4行4列,第一行将每个按键的一端连接在一起构成行线,第一列将每个按键的另一端连接在一起构成列线,这样便一共有4行4列共8根线,我们将这8根线连接到单片机的8个I/O口上,通过程序扫描键盘就可检测16个键。用这种方法我们也可实现3行3列9个键、5行5列25个键、6行6列36个键等。
        无论是独立键盘还是矩阵键盘,单片机检测其是否被按下的依据都是一样的,也就是检测与该键对应的I/O口是否为低电平。独立键盘有一端固定为低电平,单片机写程序检测时比较方便。而矩阵键盘两端都与单片机I/O口相连,因此在检测时需人为通过单片机I/O口送出低电平。检测时,先送一列为低电平,其余几列全为高电平(此时我们确定了列数),然后立即轮流检测一次各行是否有低电平,若检测到某一行为低电平(这时我们又确定了行数),则我们便可确认当前被按下的键是哪一行哪一列的,用同样方法轮流送各列一次低电平,再轮流检测一次各行是否变为低电平,这样即可检测完所有的按键,当有键被按下时便可判断出按下的键是哪一个键。当然我们也可以将行线置低电平,扫描列是否有低电平。这就是矩阵键盘检测的原理和方法。
        实验板上16个矩阵按键与单片机连接图如下图所示。

        实验板上键盘区上面4行即为 16 个矩阵键盘,8条线分别与单片机的P3 口相连。
        从上图可知,矩阵键盘的4行分别与单片机的P3.0~P3.3相连,矩阵键盘的4列分别与单片机的P3.4~P3.7相连。
        在实验板上实现如下描述:实验板上电时,数码管不显示,顺序按下矩阵键盘后,在数码管上依次显示0~F,6个数码管同时静态显示即可,新建文件Temp.c,程序代码如下:

#include <reg52.h>         //52系列单片机头文件
#define uchar unsigned char
#define uint unsigned int
//sbit key1 = P3^4;
//sbit key2 = P3^5;
//sbit key3 = P3^6;
//sbit key4 = P3^7;
sbit dula = P2^5;		   //申明U1锁存器的锁存端
sbit wela = P2^6;		   //申明U2锁存器的锁存端
uchar code table[]={
0xC0, /*/"0"*/
0xF9, /*/"1"*/
0xA4, /*/"2"*/
0xB0, /*/"3"*/
0x99, /*/"4"*/
0x92, /*/"5"*/
0x82, /*/"6"*/
0xF8, /*/"7"*/
0x80, /*/"8"*/
0x90, /*/"9"*/
0x88, /*/"A"*/
0x83, /*/"B"*/
0xC6, /*/"C"*/
0xA1, /*/"D"*/
0x86, /*/"E"*/
0x8E, /*/"F"*/
0x89, /*/"H"*/
0xC7, /*/"L"*/
0xC8, /*/"n"*/
0xC1, /*/"u"*/
0x8C, /*/"P"*/
0xA3, /*/"o"*/
0xBF, /*/"-"*/
0xFF, /*/熄灭*/
0xFF /*/自定义*/
};
void delayms(uint xms);	  
uchar num,numt0;  
void dispaly(uchar num)  //显示子函数
{
	P0=table[num];  //送段选数据
	dula=1;
	dula=0;
	}
void matrixkeyscan()
{
    uchar temp,key;
	P3=0xfe;
	temp=P3;
	temp=temp&0xf0;
	if(temp!=0xf0)
	{
	   delayms(10);
	   temp=P3;
	   temp=temp&0xf0;
	   if(temp!=0xf0)
	   {
     	  temp=P3;
		  switch(temp)
		  {
		    case 0xee:
			     key=0;
				 break;
		    case 0xde:
			     key=1;
				 break;
		    case 0xbe:
			     key=2;
				 break;
		    case 0x7e:
			     key=3;
				 break;
		 }
		 while(temp!=0xf0)//等待按键释放
		 {
		   temp=P3;
		   temp=temp&0xf0;
		   }
		   dispaly(key);//显示
		 }
		}
	   P3=0xfd;
	   temp=P3;
	   temp=temp&0xf0;
	   if(temp!=0xf0)
	   { 
	     delayms(10);
		 temp=P3;
		 temp=temp&0xf0;
		 if(temp!=0xf0)
		 {
     	  temp=P3;
		  switch(temp)
		  {
		    case 0xed:
			     key=4;
				 break;
		    case 0xdd:
			     key=5;
				 break;
		    case 0xbd:
			     key=6;
				 break;
		    case 0x7d:
			     key=7;
				 break;
		 }
		 while(temp!=0xf0)//等待按键释放
		 {
		   temp=P3;
		   temp=temp&0xf0;
		   }
		   dispaly(key);//显示
		 }
		}
	   P3=0xfb;
	   temp=P3;
	   temp=temp&0xf0;
	   if(temp!=0xf0)
	   { 
	     delayms(10);
		 temp=P3;
		 temp=temp&0xf0;
		 if(temp!=0xf0)
		 {
     	  temp=P3;
		  switch(temp)
		  {
		    case 0xeb:
			     key=8;
				 break;
		    case 0xdb:
			     key=9;
				 break;
		    case 0xbb:
			     key=10;
				 break;
		    case 0x7b:
			     key=11;
				 break;
		 }
		 while(temp!=0xf0)//等待按键释放
		 {
		   temp=P3;
		   temp=temp&0xf0;
		   }
		   dispaly(key);//显示
		 }
		}		
	   P3=0xf7;
	   temp=P3;
	   temp=temp&0xf0;
	   if(temp!=0xf0)
	   { 
	     delayms(10);
		 temp=P3;
		 temp=temp&0xf0;
		 if(temp!=0xf0)
		 {
     	  temp=P3;
		  switch(temp)
		  {
		    case 0xe7:
			     key=12;
				 break;
		    case 0xd7:
			     key=13;
				 break;
		    case 0xb7:
			     key=14;
				 break;
		    case 0x77:
			     key=15;
				 break;
		 }
		 while(temp!=0xf0)//等待按键释放
		 {
		   temp=P3;
		   temp=temp&0xf0;
		   }
		   dispaly(key);//显示
		 }
		}		
}		

void main()  //zhu函数
{
   P0=0;
   dula=1;
   dula=0;
   P0=0xc0;
   wela=1;
   wela=0;
   while(1)
   {
	 matrixkeyscan();
   }
}
void delayms(uint xms)
{
	uint i,j;
	for(i=xms;i>0;i--)
		for(j=110;j>0;j--);
}

        分析如下:
        1)进入主函数后,首先关闭所有数码管的段选,也就是不让数码管显示任何数字,接着位选中所有的数码管,以后再次操作数码管时只需要送段选数据即可,因为题目要求所有数码管都显示,接着进入while()大循环不停的扫描键盘是否有被按下。
        2)在检测矩阵键盘时我们用到这样几条语句:

P3=0xfe;
temp=P3;
temp=temp&0xf0;
if(temp!=0xf0)
{
     delayms(10);
     temp=P3;
     temp=temp&0xf0;
     if(temp!=0xf0)
      {
...

        上面这几句扫描的是第一行按键,搞明白这几句后,其他的都一样,每句解释如下:
                "P3=0xfe;"将第1行线置低电平,其余行线全部为高电平。
                "temp=P3;"读取P3口当前状态值赋给临时变量temp, 用千后面计算
                "temp=temp&0xf0;"将temp与0xf0进行”与“运算,然后再将结果赋给temp,主要目的是判断temp的高4位是否有0,如果temp的高4位有0,那么与0xf0’'与“运算后结果必然不等于0xf0;如果temp的高4位没有0,那么它与0xf0"与“运算后的结果仍然等于0xf0。temp的高4位数据实际上就是矩阵键盘的4个列线,从而我们可通过判断temp与0xf0"与“运算后的结果是否为0xf0来判断出第一行按键是否有键被按下。
                "if(temp!=0xf0)"将temp和上面P3口数据与0xf0"与“运算后的结果进行比较,如果temp不等于0xf0,说明有键被按下。
                "delayms(10);"延时去抖操作。
                "temp=P3;"重新读一次P3口数据
                "temp=temp&0xf0;"重新进行一次“与“运算
                "if(temp!=0xf0)"如果temp仍然不等千0xf0,这次确认第一行确实有键被按下了
        3)判断被按下的是该行第几列的键,我们用到了switch-case语句,关于该语句请看下一个知识点。在判断列线时我们再将P3口数据读一次,“temp=P3;” 如果读回P3口的值为0xee,则说明行线1与列线1都为低电平,那么它们的交叉处是第1个按键,如果读回P3口的值为0xde,则说明行线1与列线2都为低电平,那么它们的交叉处是第2个按键,用同样的方法可检测第一行的所有键,每检测到有按键被按下后我们可以将这个键的键值赋给一个变量,用来后期处理。用同样方法检测其他几行便可检测到矩阵键盘的所有按键。
        4)在判断完按键序号后,我们还需要等待按键被释放,检测释放语句如下:

while(temp!=0xf0)   //等待按键释放
{
    temp=P3;temp=temp&0xf0;
}

        不断地读取P3口数据,然后和0xf0"与“运算,只要结果不等于0xf0,则说明按键没有被释放,直到释放按键,程序才退出该while语句。

2.1 switch-case语句

        if语句一般用来处理两个分支。处理多个分支时需使用if-else-if结构,但如果分支较多,则嵌套的if语句层就越多,程序不但庞大而且理解也比较困难。因此,C语言又提供了一个专门用于处理多分支结构的条件选择语句,称为switch语句,又称开关语句。使用switch语句可直接处理多个分支(当然包括两个分支)。其一般形式为:

switch(表达式)
{
      case 常量表达式1: (注意这里,常量表达式l后面是冒号而不是分号)
            语句1;
            break;
      case 常量表达式2:
            语句2;
            break;
            ••••••
      case常量表达式n:
            语句n;
            break;
      default:
            语句n+1;
            break;
}

        switch语句的执行流程是:首先计算switch后面圆括号中表达式的值,然后用此值依次与各个case后的常量表达式比较,若switch后面圆括号中表达式的值与某个case后面的常量表达式的值相等,就执行此case后面的语句,当执行遇到break语句就退出switch语句;若圆括号中表达式的值与所有case后面的常量表达式都不等,则执行default后面的语句n+1,然后退出switch语句,程序转向switch语句后面的下一个语句。如下程序,可以根据输入的考试成绩的等级,输出百分制分数段:

switch(grade)
{
    case’A’: /注意,这里是冒号:并不是分号;/
            printf(“85-100\n”);
            break; /每一个case语句后都要跟一个break用来退出switch语句/
    case’B’: /每一个case后的常量表达式必须是不同的值,以保证分支的唯一性/
            printf(“70-84\n”);break;
    case’C’:
            printf(“60-69\n”);break;
    case’D’:
            printf("<60\n");break;
    default:
            printf(“error!\n”);
}

        如果在case后面包含多条执行语句时,case后面不需要像if语句那样加大括号,进入某个case后,会自动顺序执行本case后面的所有语句。
        default总是放在最后,这时default后不需要break语句,并且default部分也不是必须的,如果没有这一部分,当switch后面圆括号中表达式的值与所有case后面的常量表达式的值都不相等时,则不执行任何一个分支,而是直接退出switch语句。此时,switch语句相当于一个空语句。如上面的矩阵键盘扫描程序,当没有任何键按下时,单片机不执行case中的任何语句。
        在switch-case语句中,多个case可以共用一条执行语句,如:

...
case’A’:
case’B’:
case’C’:
printf(">60\n");
break;
...

        在A,B,C三种情况下,均执行相同的语句,即输出">60"。
        最开始那个例子中,如果把每个case后的break删除掉,则当 greak='A’时,程序从"printf(“85-100\n”);"开始执行,输出结果为:

85-100
70-84
60-69
<60
error

        这是因为case后面的常量表达式实际上只起语句号作用,而不起条件判断作用,即“只是开始执行处的入口标号”。因此,一旦与switch后面圆括号中表达式的值匹配就从此标号处开始执行,而且执行完一个case后面的语句后,若没遇到break语句,就自动进入下一个继续执行,而不再判断是否与之匹配,直到遇到break语句才停止执行,退出switch语句。因此,若想执行一个case分支之后立即跳出switch语句,就必须在此分支的最后添加一个break语句。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值