STC51入门笔记(郭天祥C语言)---第四节:键盘检测原理及应用实现

版权声明: https://blog.csdn.net/qq_38351824/article/details/89135034

       声明:本篇文章只是个人知识盲区、知识弱点、重点部分的归纳总结,望各位大佬不喜勿喷。梳理顺序是按照书籍的实际顺序梳理,转载请注明出处。


作者:sumjess


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

一、独立键盘检测

      键盘实际上就是一组按键,在单片机外围电路中,通常用到的按键都是机械弹性开关, 当开关闭合时, 线路导通, 开关断开时, 线路断开, 下图是几种单片机系统常见的按键。
在这里插入图片描述在这里插入图片描述
      弹性小按键被按下时闭合, 松手后自动断开; 自锁式按键按下时闭合且会自动锁住, 只有再次按下时才弹起断开。通常我们把自锁试按键当做开关使用, 比如 TX-l C 实验板上的电源开关就使用自锁按键。单片机的外围输入控制用小弹性按键较好, 单片机检测按键的原理是:单片机的I/O口既可作为输出也可作为输入使用, 当检测按键时用的是它的输入功能, 我们把按键的一端接地,另一端与单片机的某个I/O口相连,开始时先给该I/O口赋一高电平, 然后让单片机不断地检测该I/O口是否变为低电平, 当按键闭合时, 即相当于该I/O口通过按键与地相连, 变成低电平, 程序一旦检测到I/O口变为低电平则说明按键被按下, 然后执行相应的指令。
      按键的连接方法非常简单, 如下图左所示, 右侧I/O端与单片机的任一I/O口相连。按键在被按下时, 其触点电压变化过程如下图右所示。
在这里插入图片描述
      从上图右可看出, 理想波形与实际波形之间是有区别的, 实际波形在按下和释放的瞬间都有抖动现象, 抖动时间的长短和按键的机械特性有关, 一般为 5-- 10ms。通常我们手动按下键然后立即释放, 这个动作中稳定闭合的时间超过 20ms。因此单片机在检测键盘是否按下时都要加上去抖动操作,有专用的去抖动电路,也有专用的去抖动芯片,但通常我们用软件延时的方法就能很容易解决抖动问题,而没有必要再添加多余的硬件电路。
      用示波器跟踪不同类型的开关, 得到图 4.1.4 和图 4.1.5 的波形, 观察波形可以帮助我们对抖动现象有一个直观的了解。图 4.1.4 是一个小的按钮开关在闭合时的抖动现象, 水平轴2ms/Div, 抖动间隙大约为10ms, 在达到稳定状态前一共有 6 次变化, 频率随时间升高。
在这里插入图片描述
      图 4.1.5 是一个小型继电器在闭合时的抖动现象, 水平轴 2ms/Div, 抖动间隙大约为 8ms,在达到稳定状态前一共有 13 次变化。注意在开始和结束时,几 个小的脉冲后伴随较高的频率。
在这里插入图片描述
      编写单片机的键盘检测程序时,一般在检测按下时加入去抖延时,检测松手时就不用加了。按键检测流程图如图 4.1.6 所示。
在这里插入图片描述
在这里插入图片描述
注意:Easy Board家的板子原理图如下,连得引脚是一样的
在这里插入图片描述
      实验板上键盘区最下面一行S2~S5为 4 个独立键盘, 与单片机的P3.4-P3.7 分别相连, 如下图所示。
在这里插入图片描述
      下面通过一个实例来讲解独立键盘的具体操作方法, 在TX- IC 实验板上实现如下描述。
      【例 4.1.1】用数码管的前两位显示一个十进制数,变化范围为 00~59, 开始时显示 00, 每按下S2 键一次, 数值加 1; 每按下S3 键一次, 数值减 1;每按下 S4 键一次, 数值归零; 按下 S5 键一次, 利用定时器功能使数值开始自动每秒加 1, 再次按下 S5 键, 数值停止自动加 1, 保持显示原数。新建文件 Sumjess2.2_1.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)例 4.1.1 中我们将定时器初始化部分、键盘扫描部分、数码管显示部分等分别写成独立的函数,在主函数中只需方便地直接调用它们就可以了,这样可使程序看上去简洁、明了,修改也很方便。
      (2)大家在写程序时一定要注意代码的层次感,一级和一级之间用一个 Tab 键隔开, 尤其是初写程序的学员,不注意书写格式,程序从头到尾不加任何注释,级与级之间没有任何空格,当程序有问题,回头查询起来很不方便,大家从一开始就要养成良好的书写习惯,具体书写格式可参照本书例程。
      (3)在键盘扫描程序中 " delayms(10); " 即是去抖延时。在确认按键被按下后,程序中还有语句" while(!key,1);", 它的意思是等待按键释放,若按键没有释放则key1始终为 0 , 那么!key1即始终为 1 ,程序就一直停止在这个while 语句处,直到按键释放,key1变成了 1,才退出这个while语句。通常我们在检测单片机的按键时,要等按键确认释放后才去执行相应的代码。若不加按键释放检测,由千单片机执行代码的速度非常快,而且是循环检测按键,所以当按下一个键时,单片机会在程序循环中多次检测到键被按下,从而造成错误的结果,大家可不加按键释放检测代码, 编译程序下载后测试, 当按下S2 键时,会看到数码管上的数值变化很快,而且没有规律。

二、矩阵键盘检测:

      独立键盘与单片机连接时,每一个按键都需要单片机的一个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口送出低电平。检测时,先送一列为低电平,其余几列全为高电平(此时我们确定了列数),然后立即轮流检测一次各行是否有低电平,若检测到某一行为低电平(这时我们又确定了行数),则我们便可确认当前被按下的键是哪一行哪一列的,用同样方法轮流送各列一次低电平,再轮流检测一次各行是否变为低电平,这样即可检测完所有的按键,当有键被按下时便可判断出按下的键是哪一个键。当然我们也可以将行线置低电平,扫描列是否有低电平。这就是矩阵键盘检测的原理和方法。
      TX-lC实验板上16个矩阵按键与单片机连接图如下图所示。
在这里插入图片描述
      TX-l C 实验板上键盘区上面4行即为 16 个矩阵键盘,8条线分别与单片机的P3 口相连, 如下图所示。

在这里插入图片描述
      从上上图可知,矩阵键盘的4行分别与单片机的P3.0~P3.3相连,矩阵键盘的4列分别与单片机的P3.4-P3.7相连。
      【例4.2.1】在实验板上实现如下描述:实验班上电时,数码管不显示,顺序按下矩阵键盘后,在数码管上依次显示0~F,6个数码管同时静态显示即可,新建文件Sumjess2.2_2.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)判断被按下的是该行第几列的键,我们用到了switchcase语句,关于该语句请看下一个知识点。在判断列线时我们再将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语句。

知识点:switch-case语句

      **(1)**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”);

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

猜你喜欢

转载自blog.csdn.net/qq_38351824/article/details/89135034