第4章 键盘输入及中断
4.1概述
键盘是计算机最基本、最重要的输入设备之一,作为微型计算机,单片机也不例外。 键盘的工作任务大体可分为以下三项:①按键识别,即判断有无按键按下;②求键值,即在确认有按键按下时,具体判断是哪个按键被按下,并求得相应的键值;③执行操作,即依据键值,执行对应的操作或任务。
键盘可分为编码键盘和非编码键盘两大类。我们熟悉的台式计算机或笔记本电脑使用的键盘属于编码键盘,它由多个按键与专用驱动芯片组合而成,其按键识别和键值的获得,全部依靠硬件来完成。而单片机应用系统中使用的键盘,绝大多数属于非编码键盘,其按键与单片机直接连接,而按键识别和键值的获得,必须通过单片机的软件来完成。图4-1是单片机应用系统中经常使用的非编码键盘的实物图,其中图(a)是一个按钮,在单片机术语中被称为“按键”;图(b)是将16个按键排成4行4列的矩阵形式,在单片机的术语中被称为行列式键盘或矩阵键盘。本章重点介绍按键和矩阵键盘。
图4-1 单片机应用系统中常用的非编码键盘
中断,是单片机应用系统开发中一个非常重要的概念,也是单片机最重要的功能之一。本章在引入中断概念之后,将中断与键盘、中断与数码管结合,借助这些综合性的应用实例,介绍51单片机的外部中断、定时/计数器中断的特点及具体使用方法。
4.2按键
按键,是指每个按键占用一根I/O口线,如图4-2所示,8个按键占用了8根I/O引脚。图(a)中每个按键连接一个上拉电阻,用来保证在无键按下时,单片机I/O口引脚处于可靠的高电平状态,以防止各种干扰可能引起的误操作。51单片机引脚悬空时即为高电平状态,所以在某些要求不高的场合,上拉电阻也可以不接,如图4-2(b)所示。
(a) (b)
图4-2 按键的连接方式
67
按键适用于按键数量较少的场合,其特点是各按键相互,电路配置灵活,软件结构简单。当按键数量较多时,I/O口线的占用就较多,电路结构的复杂度也会提高。
4.2.1按键的结构
单片机系统中常用的按键是轻触按键,其实物如图4-3(a)所示,在电路中的符号如图4-3(b)所示,图4-3(c)则是其内部结构图。
1324
图4-3(a)轻触按键实物图 图4-3(b)轻触按键电气符号 图4-3(c)轻触按键结构图
不难发现,轻触按键电路符号为2个引脚,而实物却是4个引脚。从其结构图4-3(c)可以看出,实物按键的4个引脚分为两组,引脚1和2为一组,引脚3和4为一组,每组引脚在按键内部彼此连通。实际应用时,从两组引脚中各任意选择一只引脚,组合使用即可。例如,选择引脚1和3,或者选择引脚2和4,来作为开关按钮均可。在单片机硬件电路设计中,为保证整个按键在电路安装或焊接时的机械强度,并允许大电流通过,四个引脚一般都会使用。
4.2.2按键实例1——按键计数
功能说明:两个按键K1和K2, K1每按下一次,数码管上显示的数值就加1;K2每按下一次,数码管上显示的数值就减1。数码管显示的数值范围是0~9。当数码管当前显示数值为9时,再按下K1键,数码管显示数值0;当数码管当前显示数值为0时,再按下K2键,数码管显示数值9。
硬件说明:
1、 硬件电路连接如图4-4所示。按键选用轻触按键,按键K1和K2分别连接单片机的P3.2和P3.3两只引脚;
2、数码管选用共阴极数码管,使用74HC573驱动段码数据,公共端直接接地。
图4-4 按键计数硬件连接图
软件说明:
1、通过不断读取并查看按键连接端口引脚P3.2和P3.3的状态,实现按键识别; 2、按键识别后,通过设置计数器来记录按键次数,并将数据及时送到P0口显示。
程序清单如下: #include 68 #define uint unsigned int #define uchar unsigned char sbit k1=P3^2; sbit k2=P3^3; uchar code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; char num=0; //************************************************************ void delay(uchar i) //延时子函数 { uchar j,k; for(k=0;k//************************************************************ main() { while(1) { if(k1==0) //读按键K1状态,若为0,初步判断按键K1按下 { delay(10); //延时去抖动 if(k1==0) //再次判断,仍为0,说明按键K1确实按下 { num++; //计数器值加1 if(num==10) //若计数器值等于10,则清0,保证0~9显示 num=0; while(k1==0); //等待按键释放 P0=table[num]; } } if(k2==0) { delay(10); if(k2==0) { num--; if(num==-1) num=9; while(k2==0); //等待按键释放 P0=table[num]; } } } } 69 C语言知识:if语句 程序设计中的三大结构分别是顺序结构、选择结构和循环结构。选择结构也称为分支结构,if语句属于选择结构。在程序中,if语句用来判定给定的条件是否满足,并根据判定结果(逻辑真或假),在所给两种操作中,选择其中一种操作来执行。 1.if语句的三种形式 (1)if语句的第一种形式为: if(表达式) 语句 其执行流程如图4-5所示。 例如:if(x>y) x--; 表达式真语句假 图4-5 if语句形式1的执行流程 (2) if语句的第二种形式为: if(表达式) 语句1 else 语句2 其执行流程如图4-6所示。 真表达式假语句1语句2 图4-6 if语句形式2的执行流程 例如:if(x>y) x--; else x++; (3) if语句的第三种形式为: if(表达式1) 语句1 70 else if(表达式2) 语句2 else if(表达式3) 语句3 …… else if(表达式m) 语句m else 语句m+1 其执行流程如图4-7所示。 表达式1假假假表达式2表达式m真语句1真语句2真语句m语句m+1 图4-7 if语句形式3的执行流程 例如: if(number>500) NUM=1; else if(number>300) NUM=2; else if(number>100) NUM=3; else if(number>50) NUM=4; else NUM=0; 说明: 1) if语句中的‚表达式‛,一般为关系表达式或逻辑表达式,但并不仅局限于这两种。需要注意的是,C语言中,0代表‚假‛,非0则代表‚真‛。 例如:if(‘A’) i++;语句中,i++;一定会被执行,原因是字符‘A’作为表达式时,被判断的是该字符对应的ASCII码值,‘A’的ASCII码值为十进制数65,自然是非0的,所以表达式’A’的结果为逻辑‚真‛。 2)else子句不能单独使用,它是if语句的一部分,必须与if配对使用。 3)在if和else的语句1或者语句2中,可以只包含一条操作语句,也可以包含多条。如果是多条操作语句,这些多条操作语句必须用大括号(花括号)括起来, 构成所谓的语句块或复合语句。复合语句中的每条语句也许都用分号结束,但复合语句的大括号外不再需要分号。 例如: if(x+y>z&&y+z>a&&z+x>y) { 71 s=2*(x+y+z); area=sqrt(s*(s-x)*(s-y)*(s-z)); display(area); } else printf(“it is not a triangle”); 2.if语句的嵌套 if语句的嵌套,就是if语句的if块或else块中,又包含一个或多个if语句。其一般形式为: if(„) if(„) 语句1; else 语句2; else if(„) 语句3; else 语句4; 在实际程序编写中,应当注意if与else的配对关系:else总是与它上面最近的、且未配对的if配对;尤其是在if/else子句数目不相等(if子句数量大于或等于else子句数量)时,应特别注意if与else的配对。 在if语句使用中,经常会出现与编程者预期结果不同的现象,通常可以用下面两种方法解决if与else的匹配问题: (1)利用‚空语句‛占位,使if子句数量与else子句数量相同。所谓‚空语句‛,就是什么功能都没有、仅仅由一个分号构成的语句。在程序设计中,其作用就是为了使程序结构完整、规范,不出现歧义,避免不必要的错误。例如: if(„) if(„) 语句1 else ; //此行中的分号即为空语句 else if(„) 语句2 else 语句3 (2)利用复合语句。将没有else子句的if语句用大括号括起来,构成复合语句,使原有的语句1或者语句2从整体上查看时,呈现出语句块结构。例如: if(„) { if(„) 语句1 } 72 else { if(„) 语句2 else 语句3 } 在单片机C语言编程时,常有“if„else包打天下”的说法,虽有些夸张,但也从一个侧面反映了if语句应用的广泛性与功能的多样性。我们建议读者熟练运用if语句,同时尽量掌握多种语句形式,灵活运用。比如,本章后面实例中出现的switch语句,就可以在很多场合替换if语句,更有层次分明、可读性好的优点。 单片机知识:按键的去抖动 我们常用的按键都是机械按键,如图4-8(a)所示,键在按下或释放时,由于机械弹性的影响,触点会发生机械抖动,与按键连接的芯片引脚就会发生如图4-8(b)所示电平波动或抖动现象。这种抖动可能造成CPU错误判定外部引脚上电平的高低,从而导致误操作或者一次按键多次处理的问题。因此,实际使用中,要通过某种方法消除抖动产生的不良后果,这就是按键的去抖动。目前,较为常用的有硬件去抖动和软件去抖动。 图4-8 按键操作和按键抖动 图4-8(b)中,前沿抖动或后沿抖动持续的时间一般为5~10ms。由于抖动现象不可避免,势必引起电平信号波动, 1.硬件去抖动 图4-9 硬件消抖电路 硬件去抖动,就是使用硬件电路消除按键的抖动。常用的硬件电路如图4-9所示,其中图(a)是用两个与非门构成的双稳态触发器来消除按键抖动;图(b)是使用单稳态集成触发器74121来消除按键抖动;图(c)是利用电阻和电容构成的RC滤波电路去抖动,图(c)电路结构简单,实用效果好,运用较多。 2.软件去抖动 73 为了便于理解软件去抖动的原理,此处,我们分析一次完整按键的过程细节,如图4-8(b)所示。首先,按键被按下,由于机械弹性原因出现前沿抖动,抖动持续时间一般为5~10ms;前沿抖动结束后,引脚电平就稳定在低电平状态;低电平持续一段时间后,按键被松开弹起,此时又出现后沿抖动,抖动持续时间一般也是5~10ms;后沿抖动结束,一次完整的按键操作随之结束。 软件去抖动,就是首先检测引脚电平是否为低电平,若检测到低电平,说明按键可能被按下,但还不能完全确定按键是否真被按下;然后延时5~10ms,目的就是要避开前沿抖动这一时段;延时结束后,按理引脚电平已稳定在低电平状态,此时再检测引脚电平,如果是低电平,则可以确认按键的确被按下。相反,若延时之后,再次检测到的电平是高电平,则说明刚才检测到的低电平是误判,按键没有真正被按下。如果确认按键的确被按下,则CPU执行相关按键按下的处理程序;否则CPU不予理睬,继续执行原有程序。 由以上分析可见,软件去抖动的核心,就是延时及延时前后两次连续的检测和判断。软件去抖动的程序示例如下: if(k2==0) //若检测到低电平,初步判断按键被按下,但还未完全确认 { delay(10); //延时10ms去抖动 if(k2==0) //延时后再次检测,若检测到低电平,则可以确认按键的确被按下 num--; //有按键按下,则进行相关处理 } 4.2.3按键实例2——多个按键识别 功能要求:实现8个按键K0~K7的识别,并用一位数码管显示被按下按键对应的键号。 硬件说明: 1、 硬件连接原理图如图4-10所示。8个按键分别与单片机P2口的8只引脚连接,8个按键的公共端接地。 2、采用共阴极数码管显示键号,通过74HC573驱动段码数据,公共端直接接地; 图4-10 按键多键识别硬件连接图 软件说明:按键的程序设计,一般都采用查询方式,即逐位查询每根I/O口引脚线的电平状态。如果软件去抖动之后,检测到某根I/0口线引脚依旧为低电平,则可以确认与该I/O端口引脚连接的按键被按下,之后就可以转入该按键对应的处理程序。 程序清单如下: 74 #include #define uint unsigned int #define uchar unsigned char uchar code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f}; uchar num; //************************************************************ void delay(uchar i) //延时函数 { uchar j,k; for(k=0;k//************************************************************ uchar keyscan() { uchar b; P2=0xff; //P2口作为输入口,置全1 b=P2; if(b==0) //循环判断是否有键按下 { delay(10); //有键按下,延时10ms去抖 if(b==0) { b=P2; //读按键状态 b=~b; //按键状态取反 } } switch(b) //根据键值调用不同的处理函数 { case 0x01: num=1;break; //取得按键1键值 case 0x02: num=2;break; //取得按键2键值 case 0x04: num=3;break; //取得按键3键值 case 0x08: num=4;break; //取得按键4键值 case 0x10: num=5;break; //取得按键5键值 case 0x20: num=6;break; //取得按键6键值 case 0x40: num=7;break; //取得按键7键值 case 0x80: num=8;break; //取得按键8键值 default: break; } return num; } main() { while(1) 75 { P0=table[keyscan()]; } } C语言知识:switch语句 如前所述,嵌套的if语句可以实现多分支,但分支过多,if语句嵌套的层数就多,致使程序冗长,可读性变差,此时,可考虑使用switch语句实现。 C语言中,switch语句是多分支选择语句,其一般形式为: switch(表达式) { case 常量表达式1:语句1 case 常量表达式2:语句2 „ „ „ case 常量表达式n:语句n default: 语句n+1 } switch语句中,各个case语句可以通过使用break语句终止。 说明: (1)switch括号内的表达式,允许为任何类型。 (2)当‚表达式‛的值与某个case后面的常量表达式的值相等时,就执行此case后面的语句。如果表达式的值与所有常量表达式都不相等,就执行default后面的语句。 (3)各个常量表达式的值必须互斥,不允许相同,否则会出现逻辑矛盾。 (4)case,default出现的顺序不影响执行结果。 (5)执行完一个case后面的语句后,流程控制转移到下一个case中的语句继续执行。此时,‚case 常量表达式‛只起到语句标号的作用,并不在此处进行条件判断。执行一个分支后,可以使用break语句,使流程跳出switch结构,即终止switch语句的执行(最后一个分支可以不用break语句)。 (6)case后面如果有多条语句,不必用大括号括起来。 (7)多个case可以共用一组执行语句,此时应注意break使用的位臵。 4.2.4按键实例3——一键多功能按键识别 功能要求:用一个按键实现多个按键的功能。具体要求为:系统上电的时候,仅接在P2.0引脚上的发光二极管D1在闪烁,其它发光二极管D2、D3和D4都熄灭;如果按一下开关K,即开关K被按下了一次,此时接在P2.1管脚上的发光二极管D2开始闪烁,而D1、D3和D4都熄灭;如果再按一下开关K,即开关K已累计被按下了2次,此时,接在P2.2管脚上的发光二极管D3开始闪烁,而D1、D2和D4都熄灭;如果再按下开关K,即开关K已累计被按下了3次,此时,接在P2.3管脚上的发光二极管D4开始闪烁,而D1、D2和D3都熄灭;如果再次按下开关K,即开关K已累计被按下了4次,此时D1开始闪烁,而D2、D3和D4都熄灭,„„,依次类推,随着按键次数的增加,四个发光二极管被轮流选中,闪烁发光。 硬件说明: 电路原理图如图4-11所示,开关K接在P2.7引脚上, P2口的低四位分别接有四个发光二极管D0,D1,D2和D3。 76 图4-11 一键多功能识别硬件连接图 软件说明: 从上面的要求可以看出,发光二极管D1到D4轮流闪烁的时段受开关K控制,给发光二极管单独闪烁的各个时段分配不同的ID号:仅当D1在闪烁时,ID=0;仅当D2在闪烁时,ID=1;仅当D3在闪烁时,ID=2;仅当D4在闪烁时,ID=3。很显然,每次按下开关K时,ID值加1,根据当前ID号,选择不同的任务去执行,而ID号则以数值3封顶,即ID号加1等于4时,ID号就清0。本实例的程序设计框图如图4-12所示。 初始化ID=0识别按键K是否成功ID号加1ID=4吗?ID=0根据ID号执行相应模块ID=0ID=1ID=2ID=3DI闪烁D2闪烁D3闪烁D4闪烁 图4-12一键多功能程序流程 程序清单如下: #include 77 sbit D2=P2^1; sbit D3=P2^2; sbit D4=P2^3; unsigned char ID=0; //************************************************************ void delay(unsigned char i) //延时1ms { unsigned char j,k; for(k=0;k//************************************************************ void delay02s(void) //延时200ms=0.2s函数 { unsigned char x; for(x=20;x>0;x--) delay(10); } //************************************************************ main() { while(1) { if(K==0) { delay(10); if(K==0) { ID++; //确定有按键按下,每次ID加1记录 if(ID==4) //当ID值为4时,重新开始记录 ID=0; while(K1==0); } } switch(ID) { case 0: D1=~D1; delay02s(); break; case 1:D2=~D2; delay02s(); break; case 2:D3=~D3;delay02s(); break; case 3:D4=~D4;delay02s(); break; default:break; } } } 78 4.3 矩阵键盘 4.3.1矩阵键盘 在按键电路中,每一个按键都要占一根I/O口引脚。按键数量较多时,如果依旧采用按键,必然使很多I/O口引脚被占用。由于51单片机I/O口引脚数量原本就有限,这样留做它用的I/O口引脚数量就会更少,以致具体应用中出现I/O口引脚不够用的尴尬局面。所以,在按键数量较多的情况下,就不宜再使用按键,取而代之的便是矩阵键盘。 矩阵键盘由多个行线和多个列线交织组成,其中使用的按键依旧是按键,但各个按键都位于行线和列线的交叉点上,如图4-13所示。此图所示矩阵键盘,共有16个按键,分别位于4根行线和4根列线的交叉点上。从图中可以看到,若依照按键的形式连接,16个按键就需要16根I/O口引脚线;若采用矩阵键盘,则仅占用8根I/O口引脚线,节省下来的I/O引脚线数量还是相当可观的。在节省了I/O引脚、硬件电路得以优化的同时,也导致软件编程复杂度的提高。 图4-13 矩阵键盘结构 实际应用中,常见的矩阵键盘有两种,如图4-14所示, (a)普通矩阵键盘 (b)薄膜矩阵键盘 图4-14 常用矩阵键盘实物 图4-14(a)为按键与设计好的PCB电路构成的矩阵键盘。制作时,要先设计PCB印刷电路板,再在PCB板上焊接按键、电阻和接口等元件,使用时直接将接口与单片机I/O连接即可。 图4-14(b)为薄膜键盘,它是近年来国际流行的一种集装饰性与功能性为一体的矩阵键盘。由于薄膜键盘具有可定制、体积小、厚度薄、重量轻、密封性强、防潮、防尘、耐酸碱、抗震、使用寿命长、耐弯折等优点,已广泛应用在智能化电子测量仪器、医疗仪器、计算机控制、数控机床、邮电通讯、复印机、电冰箱、微波炉、电风扇、洗衣机、电子游戏机等各类工业及家用电器产品及相关领域。 无论PCB矩阵键盘还是薄膜矩阵键盘,其电路原理是一样的。为了方便读者制作验证,图4-15给出了矩阵键盘的电路原理图。 79 图4-15 矩阵键盘电路原理图 4.3.2 矩阵键盘应用实例 功能要求:4×4矩阵键盘连接到51单片机的P2口,P2.0-P2.3作为列线,P2.4-P2.7作为行线;某一按键被按之后,在数码管上显示其对应的序号。矩阵键盘面板序号的排列如图4-16所示。选择51单片机的P0口作为共阴极数码管段码的输出口,公共端直接接地。硬件连接如图4-17所示。 图4-16 矩阵键盘面板序号排列 图4-17 矩阵按键硬件连接图 软件说明: 1、4×4矩阵键盘工作原理 矩阵键盘的工作原理与按键类似。按键工作时,按键一端直接接地(该端一直处于低电平),单片机只要直接检测I/O口引脚是否出现低电平,就可以确定按键是否按下。但对于矩阵键盘,由于其行线和列线均连接到I/O口的引脚上,没有可供直接检测的低电平,所以,必 80 须人为地编程,从I/O口输出低电平到相关引脚,再检测其它引脚上是否出现低电平,从而判断是否有键按下。 以下以本例硬件连接为基础,如图4-17所示,具体说明矩阵键盘扫描的原理。 首先,单片机从P2口输出数据0xFE,此时,仅有P2.0引脚对外输出低电平,而P2.0引脚连接矩阵键盘的第1列,所以目前第一列四个按键的公共端呈现低电平状态。如果此时第一列恰好有键按下,则四条行线中,必然有一条行线,由于与第一列低电平的接通,而处于低电平状态,即P2口的高四位(对应四条行线)数据中必然有一个0(低电平),即P2口的高四位不可能等于二进制数1111。当P2口的高四位数据不等于二进制数1111时,说明第一列“可能”有键按下,此处的“可能”是考虑到按键抖动的原因。在初步判断“可能”有键按下之后,接下来的处理自然是软件去抖动了。在延时5~10ms之后,再经第二次检测,若检测到P2口的高四位依然不等于二进制数1111,则可以肯定第一列的确有按键按下。在确定第一列的确有键按下之后,接下来的任务,就是要找到按下的按键究竟是这一列(第一列)的哪一行?因为四条行线对应数值不是二进制数1111,且这四位中只有一条行线是低电平,对应数字0,即需要找到P2口高四位中唯一的一个低电平0,究竟是这四位中的哪一位。列举P2口高四位中仅有一个低电平0的情形,只有4种,即1110,1101,1011和0111。依据硬件连接,显然,如果P2口高四位是1110,则按键必然位于第一行(如图4-17所示,第一行连接P2.4引脚)。由于前面已经圈定是第一列中的某一个按键按下,而通过P2口高四位是1110,确定按键位于第一行,综合这两点可知,按下的按键就是第一行、第一列的这个按键。同理,如果P2口的高四位是1101,则按下的按键就是第二行、第一列的那个按键;如果P2口的高四位是1011,则按下的按键就位于第三行、第一列;如果P2口的高四位是0111,则按下的按键就位于第四行、第一列。至此,如果按下的按键位于第一列,则通过上述查看P2口高四位的值,就可以完全确定按下的按键位于哪一行,从而准确定位其行与列的具体位置。 以上仅仅是对第一列按键的分析,如果按下的按键位于第二列、第三列或者第四列,那又该如何处理呢?回顾刚刚关于第一列的处理方法,我们首先是让单片机从P2口输出数据0xFE,而输出数据0xFE的目的就是让第一列四个按键的公共端处于低电平状态。那么,可否让第二列四个按键的公共端处于低电平状态呢?显然,只要让单片机从P2口输出数据0xFD就能做到这一点,所以,在P2口输出0xFD时,如果此时第二列恰好有按键按下,则四条行线对应P2口的高四位也不可能是二进制数1111,同样的道理,通过查询P2口高四位中的那一个低电平0的具体位置,就可以准确知道按下的按键位于第二列的第几行了。第三列、第四列的处理思路完全一样,分析第三列时,单片机从P2口输出0xFB;分析第四列时,单片机从P2口输出0xF7,后续的处理方法都一样,就是去抖动之后,通过查询P2口高四位中唯一的一个低电平0的具体位置(按键所在行),就可确定按下按键的具体位置。 以上是逐列,即一列挨着一列处理,现在只要让这四列的处理过程快速地循环起来,一遍又一遍地逐列快速扫描,16个按键中任意一个按键按下时,都可以得到其准确的位置。这就是矩阵键盘的工作原理。 2、本例中,当某一按下的按键被准确判断出其行列位置时,将所在行列对应的键盘序号值(如图4-16所示)送给变量keynum,然后将该序号值送P0口,通过数码管显示出来。 3、每一列处理中,在将所得键盘序号送P0口显示之后,程序模块 while(t!=0xf0) { t=P2; t=t&0xf0; } 的作用是等待按下的按键释放。显然,当按下的按键没有释放时,P2口的高四位一定不等于二 81 进制数1111,即十六进制数F;相反,如果按下的按键释放,则四条行线被全部悬空拉高,此时while循环的条件t!=0xf0逻辑为假,则程序结束此while循环,继续向下执行,单片机接着给下一列送低电平数据„„。 本例4×4矩阵键盘的程序清单如下: #include #define uchar unsigned char uchar keynum=0x00; uchar code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; void delay(void) { uchar i; for(i=0;i<=200;i++); } void key44scan(void) { uchar t; P2=0xfe; t=P2; t=t&0xf0; if(t!=0xf0) { delay(); t=P2; t=t&0xf0; if(t!=0xf0) { t=P2; switch(t) { case 0xee:keynum=1;break; case 0xde:keynum=4;break; case 0xbe:keynum=7;break; case 0x7e:keynum=15;break; } P0=table[keynum]; while(t!=0xf0) { t=P2; t=t&0xf0; } } } P2=0xfd; t=P2; 82 t=t&0xf0; if(t!=0xf0) { delay(); t=P2; t=t&0xf0; if(t!=0xf0) { t=P2; switch(t) { case 0xed:keynum=2;break; case 0xdd:keynum=5;break; case 0xbd:keynum=8;break; case 0x7d:keynum=0;break; } P0=table[keynum]; while(t!=0xf0) { t=P2; t=t&0xf0; } } } P2=0xfb; t=P2; t=t&0xf0; if(t!=0xf0) { delay(); t=P2; t=t&0xf0; if(t!=0xf0) { t=P2; switch(t) { case 0xeb:keynum=3;break; case 0xdb:keynum=6;break; case 0xbb:keynum=9;break; case 0x7b:keynum=14;break; } P0=table[keynum]; while(t!=0xf0) 83 { t=P2; t=t&0xf0; } } } P2=0xf7; t=P2; t=t&0xf0; if(t!=0xf0) { delay(); t=P2; t=t&0xf0; if(t!=0xf0) { t=P2; switch(t) { case 0xe7:keynum=10;break; case 0xd7:keynum=11;break; case 0xb7:keynum=12;break; case 0x77:keynum=13;break; } P0=table[keynum]; while(t!=0xf0) { t=P2; t=t&0xf0; } } } } //*************************************************** main() { while(1) { key44scan(); } } 在很多单片机应用开发系统中,按键或者键盘都充当菜单的角色。通过按键选择不同的菜单,可以使单片机应用系统切换到不同的功能模块,从而完成多种复杂的结构和功能。 84 4.4中断 4.4.1什么是中断 1、中断的概念 生活中有时会出现这样的情形:假定你正在家中观看一部精彩的DVD影片,突然电话铃声响起,为了不错过影片的每一个情节,你不得不暂停影片的播放,转而去接听电话。电话是同学打来的,他有问题要向你咨询,你不得不坐下来,耐心地给同学解释问题。谁知刚解释到一半,突然响起急促的敲门声,无奈的你只好告知同学有人敲门,让他别挂断电话,你马上回来。你前去开门一看究竟,原来是邮递员送来邮政快件。签收完快件回到屋内,你又拿起放在桌上的电话,接着给同学解释问题。等到给同学的问题解释清楚了,挂断了电话,你才如释重负地回到沙发上继续观看被暂停的影片„„。这一过程若用图形来描述,则如图4-18所示。 中断响应执行程序看影片来电话继续看影片DVD有人敲继门续接电话签收快件中断请求断点继续执行程序执行中断程序1执行中断程序2图4-18 日常生活中的中断 图4-19 单片机中的中断 生活中,大家经常会遇到类似的情况,手头正在干的工作,被其它突发的紧急事件打断,不得已只能先处理紧急事件,等紧急事件处理完之后,再继续先前的工作。这样的过程在单片机中被称为中断。对于单片机中的中断,可以这样定义:当单片机CPU正在处理某项任务时,外界或内部发生了紧急事件,迫使CPU暂停正在处理的工作,转而去处理这个紧急事件;处理完紧急事件以后,再回到原任务被暂停的地方,继续执行原来被中断的任务。对应生活中如图4-18所示中断示例,我们给出了如图4-19所示单片机中断模型。 结合图4-18和图4-19,我们可以进一步了解单片机中跟中断有关的几个重要概念。向CPU提出中断请求的源头,称为中断源,比如电话铃响起、敲门声。CPU暂停正在执行的任务,转去执行新的中断请求任务,称为中断服务,比如接电话、开门签收快件。一个中断请求,打断目前正在进行的中断服务,称之为中断嵌套,比如接电话期间的敲门声打断了接电话。 再简单介绍一下优先级的概念。优先级可以用来表征事情轻重缓急的程度,相对紧急的事件优先级高,相对不紧急的事件优先级低。可以设想,在看DVD电影期间,如果电话铃和敲门声同时响起,暂停影片的播放后,是先接电话还是先开门呢?这就涉及到优先级问题。如果电话依旧是同学打来咨询问题的,敲门的不是邮递员,而是邻居,他敲门是因为他家着火了,需要你的帮助。显然,这种情况下,救火比解释问题紧急得多,所以救火的优先级应比接电话的优先级别高,先去帮邻居救火是必然的首选。 2、单片机使用中断的意义 从以上实例分析可见,单片机中的中断,与日常生活中的中断,在处理的方式方法上是完全一致的。仅就单片机而言,使用中断的意义有以下几点: (1)实时处理。在单片机控制系统中,对于环境、参数等发生的各类变化,可能需要CPU立 DVD高级别中断返回中断高级别中断打断低级别中断形成中断嵌套 85 即响应或处理,延迟响应或处理也许会导致灾难性后果。此时,实时性变得异常重要和突出。但环境等因素的变化何时出现或发生,却无法提前预知。通过中断方式,可以让CPU在变化发生时,及时响应并做出处理,达到实时处理的目的。 (2)异常处理。单片机系统在运行过程中,经常会出现断电、程序受干扰出错、运算溢出等异常或故障情况,利用中断,可让故障源向CPU提出中断请求,由CPU对故障或异常进行处理,使系统恢复正常,并继续运行。 (3)提高效率。一般情况下,与外设相比,CPU的执行速度是很快的,而外设则比较慢。在CPU与外设进行信息交换时,就存在快速CPU与慢速外设间,信息传输速度不相匹配的矛盾。采用中断技术,能实现一个CPU分时与多个外设通信,宏观的“同时”和“并行”工作模式,在CPU提高工作效率方面是不言而喻的。 4.4.2 MCS-51单片机的中断源 一般而言,MCS-51单片机有五个中断源,它们分别是: 1、外部中断0; 2、外部中断1; 3、定时/计数器0中断; 4、定时/计数器1中断; 5、串行口发送或接收中断。 下面通过实例4.4.3和实例4.4.4,详细介绍如何使用外部中断0和外部中断1。对于定时/计数器中断、串行口中断,则分别在本章4.5节和第9章给予详述。 4.4.3外部中断实例1 功能说明:主程序控制的数码管,以大约1秒的时间间隔,循环显示数字0~9。当外部中断0发生时, 数码管先暂停显示(此时,数码管上显示的数字是0~9中的某一个);接着,P2口的8只发光二极管,以200ms的时间间隔,整体闪烁2次;接下来,数码管从暂停的数字开始,继续循环显示数字0~9。当外部中断1发生时,数码管先暂停显示(此时,数码管上显示的数字是0~9中的某一个);接着,P2口8只发光二极管流水灯般点亮,并循环3次;接下来,数码管从暂停的数字开始,继续循环显示数字0~9。 硬件说明: 1、硬件连接电路如图4-20所示,按键K1和K2,分别连接到51单片机P3.2和P3.3两只引脚上,作为外部中断0和外部中断1的输入源; 2、P0口作为共阴极数码管段码输出口,依旧使用74HC573来驱动,公共端直接接地; 3、8只发光二极管连接到单片机P2口。 86 图4-20 外部中断实例1硬件连接图 程序清单如下: #include #define uint unsigned int #define uchar unsigned char uchar code tab[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; uchar code tab1[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; uchar num=0,a,b; //************************************************************ void delay(uint i) //延时函数 { uint j,k; for(k=0;k//************************************************************ void init() //初始化子函数 { IT0=1; //设置外部中断0为边沿触发 EX0=1; //开外部中断0 IT1=1; //设置外部中断1为边沿触发 EX1=1; //开外部中断1 EA=1; //开总中断允许位 } //************************************************************ main() //主函数 { init(); while(1) 87 { num++; if(num==10) num=0; P0=tab[num]; delay(1000); } //软件延时1秒 } //************************************************************ void int0(void) interrupt 0 //外部中断0中断处理程序 { P2=0; delay(200); P2=0xff; delay(200); P2=0; delay(200); P2=0xff; delay(200); } //************************************************************ void int1(void) interrupt 2 //外部中断1中断处理程序 { for(b=0;b<3;b++) { for(a=0;a<8;a++) { P2=tab1[a]; delay(50); } } } 单片机知识:外部中断 51单片机的5个中断源中,有2个是外部中端源,即外部中断0和外部中断1。之所以称之为外部中断,是因为引起CPU中断的中断源信号来自单片机的外部。其中,外部中断0的中断源信号,从51单片机的P3.2引脚进入单片机,而外部中断1的中断源信号则从51单片机的P3.3引脚进入单片机。至于中断源信号,也不是任意一个信号从P3.2和P3.3进入单片机都能引起中断,具体而言就是,如果中断源信号是电平信号,则必须是低电平信号;如果中断源信号是脉冲边沿信号,则必须是从高电平变化到低电平的下降沿信号。具体使用时,可通过设臵相关寄存器,来决定选用低电平信号还是下降沿信号。 51单片机的外部中断要被响应,必须要有从外部进入单片机的适当的中断源信号,还要设臵一系列相关寄存器,只有准确地设臵这些寄存器,才能使外部中断源信号引起中断。设臵寄存器,就是将寄存器的相关位臵1或者清0。51单片机中,与外部中断相关的寄存器有三个:中 88 断允许寄存器IE,定时器控制寄存器TCON,中断优先级寄存器IP。以下详细说明其各位含义及设臵方法。 1、 中断允许寄存器IE 中断允许或禁止的具体含义是:只有在中断允许的前提下,中断请求信号才可以被响应;如果中断被禁止(未允许),即使有中断请求信号,CPU也会臵之不理,不予响应。 在51单片机中断系统中,中断允许或禁止,是由中断允许寄存器IE(IE为特殊功能寄存器,包括4个I/O口在内,51单片机共有21个特殊功能寄存器)控制的。IE寄存器是一个8位寄存器,其各位具体含义如表4-1所列,其中位6和位5保留未使用;其余6位中,与外部中断有关的位是位0、位2和位7,其功能及含义分述如下;而与定时计数器相关的位3和位2,留待下一节再详述;位4则要留待第9章再叙述。 表4-1 IE寄存器 位7 位6 位5 位4 位3 位2 位1 位0 EA ES ET1 EX1 ET0 EX0 ◆位0——EX0:外部中断0中断允许位。EX0=1,允许外部中断0中断;EX0=0,禁止外部中断0中断。 ◆位2——EX1:外部中断1中断允许位。EX1=1,允许外部中断1中断;EX1=0,禁止外部中断1中断。 ◆位7——EA:中断允许总控制位。EA=1,允许所有中断源中断;EA=0,禁止所有中断源中断。 有关IE寄存器,需要特别说明的是,51单片机的中断允许实行两级开关管理:总开关和分开关,且总开关和分开关之间是逻辑与的关系。总开关只有一个(IE的位7),属于CPU级别;分开关有5个(IE的位0至位4),对应5个中断源。对于某一具体中断而言,只有总开关和该中断对应的分开关都闭合允许时(相应位臵1),该中断才被允许;相反,总开关或分开关中,只要有一个是断开的(相应位清0),则该中断就被禁止。外部中断0的分开关,就是IE的位0;外部中断1的分开关,就是IE的位2。可见,若要允许外部中断0,则需要将IE的位7和位0同时臵1才可以;同理,若要允许外部中断1,则需要将IE的位7和位2同时臵1才可以。 51单片机系统复位时,IE寄存器各位都被清0,即禁止所有中断。 2、 定时器控制寄存器TCON TCON是定时/计数器控制寄存器,它也是8位寄存器,如表4-2所列。其中,与外部中断0和外部中断1有关的4位是位0至位3,即低4位,其功能及含义分述如下;而高4位则与定时/计数器相关,在定时/计数器一节中有详细说明。 表4-2 TCON寄存器 位7 位6 位5 位4 位3 位2 位1 位0 TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0 ◆位0——IT0:外部中断0触发方式位,该位用于设臵引起外部中断0的请求信号,是低电平的电平信号,还是脉冲信号的下降沿信号。若外部中断0的请求信号,是脉冲信号的下降沿信号,则设臵IT0=1;若外部中断0的请求信号,是低电平的电平信号,则设臵IT0=0。在采用低电平的电平信号触发时,输入到P3.2引脚的外部中断源信号,必须一直保持低电平有效,直至该中断被响应为止;并且在中断响应返回之前,必须将电平拉高,变成高电平,否则将会导致中断的再次请求发生。 ◆位1——IE0:外部中断0中断请求标志位,该位是一个供软硬件查询的状态位,用于表征外部中断0是否有中断请求。如果软硬件查询到的结果是IE0=1,则表明外部中断0有中断请求;相反,如果软硬件查询到的结果是IE0=0,则表明外部中断0没有中断请求。在软硬件查询到的结果是IE0=1,表明外部中断0有中断请求时,若该中断一旦被响应,则IE0被自动 清0,即编程者不需要用软件刻意将其清0。 ◆位2——IT1:外部中断1触发方式位。此位功用完全同于IT0,只是针对外部中断1而言。 ◆位3——IE1:外部中断1中断请求标志位。此位功用完全同于IE0,只是针对外部中断1而言。 在外部中断中,该寄存器的位0和位2用于设臵,臵1和清0分别用于指定中断信号的类别(电平或是边沿);而位1和位3,则是供软硬件查询的(硬件查询即为中断,软件也可查询)。 3、中断优先级寄存器IP (1)中断优先级寄存器IP 51单片机中,中断的优先级别只有两级:高优先级(值为1)和低优先级(值为0)。对于各个中断源,编程者若不做任何有关优先级的设臵,即保持系统上电复位时优先级的默认值0(系统上电复位时,所有中断源的优先级的值,都被初始化为0),则所有中断源都处于低优先级。如果,编程者有意根据中断源事件的轻重缓急,为某一个或某几个中断源设臵优先级别,则可以将紧急的中断源事件设臵为高优先级(赋值为1),相对不紧急的中断源事件设臵为低优先级(赋值为0)。这种设臵,就是在中断优先级寄存器IP中进行的。IP寄存器是一个8位寄存器,如表4-3所列,其中位7、位6和位5保留未用,位0到位4则依次对应于51单片机的5个中断源:外部中断0,定时/计数器0中断,外部中断1,定时/计数器1中断和串行口中断。将IP寄存器的相关位臵1,就可将其对应的中断源设臵为高优先级;将IP寄存器的相关位清0(默认值就是0),就可将其对应的中断源设臵为低优先级。例如,如果想设臵外部中断0为高优先级,就将位0臵1。 表4-3 IP寄存器 位7 位6 位5 位4 位3 位2 位1 位0 - - - PS PT1 PX1 PT0 PX0 (2)中断优先级规则 优先级规则,是51单片机中有关中断处理顺序、或者说中断排队的规定。之所以要排队,原因是中断源有5个之多,中断请求的发生在时间上有先后次序之分,即使多个中断同时发出请求,但中断请求的轻重缓急亦有高优先级和低优先级之别,所以,中断排队是非常关键和重要的。如果中断源请求只有一个,则无所谓排队不排队。以下所述排队规则,主要针对多个中断源的情形,且假定中断都是被允许的(两级开关都允许)。具体规则如下: ①如果多个中断请求的发生,在时间上有先后次序,且所有发生的中断请求都处于同一级别(要么全部低优先级,要么全部高优先级),则中断排队依照时间顺序,先来者排前,后到者靠后。 ②如果多个中断请求的发生,时间上有先后,且优先级别不完全相同(部分优先级低,部分优先级高),则依如下规则:若低优先级的中断先期到达,并已开始执行任务,后来到达的中断又恰好是高优先级中断(若后来者是同优先级中断,则不能打断正在执行的同级),若低优先级中断还未处理结束,则高优先级中断会打断优先级别低的中断,这就是中断嵌套;高优先级中断执行完响应服务程序之后,低优先级中断接着执行前面未完成的任务。相反,多个中断请求的发生,时间上有先后,优先级别不完全相同,高优先级中断先期到达并已开始执行任务,则后来的低优先级中断只能耐心等待(级别低还来得迟),等到高优先级中断执行结束,并且再无其它高优先级中断时,此低优先级中断才可以被执行。 ③如果多个中断请求同时发生,且都处于同一级别(要么全部低优先级,要么全部高优先级),则中断的排队,依照外部中断0最优先,然后依次为定时/计数器0中断,外部中断1,定时/计数器1中断和串行口中断的顺序,即串行口中断排在最后。排队次序即中断响应或者执行次序。 90 ④如果多个中断请求同时发生,且优先级别不完全相同(部分优先级低,部分优先级高),则高优先级的中断源排队靠前,低优先级中断排队靠后。 C语言知识:中断服务子函数 在单片机C语言中,中断服务子函数即为中断处理程序,它具有子函数的基本格式,但也有其特有的格式要求,具体格式框架如下: void 程序名(void) interrupt m [using n] { 中断处理程序; } 此格式中,需要说明的有以下几点: 1、中断处理程序或中断服务子函数不能有返回值,即返回值为空(void);同时,该函数也不能有参数,所以函数名后的参数列表中也为空(void); 2、在函数头中,区别该子函数不同于一般意义子函数的标志是,其函数名及参数表列之后,紧跟了‚interrupt+空格+数字‛项,关键字‚interrupt‛的汉语意思就是‚中断‛,而其后的数字则是中断号。51系列单片机的中断号如表4-4所示。使用时,依据中断源的不同,关键字‚interrupt‛后面的数字也对应不同。 3、函数头中最后的可选项‚using n‛,其中的n可取值为0-3,表示寄存器组号。此项可以忽略不写,编译器会自动分配寄存器组给中断服务子函数。故此处亦不做过多介绍,有兴趣的读者可参看其它教材或资料。 4、此中断服务子函数的函数体,与一般子函数的函数体并无差异,由于无返回值,自然无 return语句。 表4-4 51单片机中断源对应的中断号 中断源 外部中断0 定时/计数器T0 外部中断1 定时/计数器T1 串行口中断 中断号 0 1 2 3 4 4.4.4外部中断实例2 功能要求: 1、主程序控制的数码管(连接在P0口)以1秒时间为间隔,循环显示数字0~9。 2、外部中断0被设置为低优先级,边沿触发方式。在主程序执行期间,若P3.2引脚有外部中断0发生,则主程序暂停(被中断),主程序控制的数码管暂停在某个数值上;而外部中断0控制的数码管(连接在P2口)则从数字0开始,依次显示数字0~9,显示三遍后结束。接着,主程序控制的数码管从暂停处继续循环显示数字0~9。 3、外部中断1被设置为高优先级,边沿触发方式。在主程序执行期间,若P3.3引脚有外部中断1发生,则主程序暂停(被中断),主程序控制的数码管暂停在某个数值上;而外部中断1控制的数码管(连接在P1口)则从数字0开始,依次显示数字0~9,显示三遍后结束。接着,主程序控制的数码管从暂停处继续循环显示数字0~9。 4、在保持以上外部中断0和1设置不变的条件下,在主程序执行期间,首先是P3.2引脚有外部中断0发生,则主程序暂停(被中断)显示,主程序控制的数码管暂停在某个数值上;而外部中断0控制的数码管则从数字0开始,依次显示数字0~9。原计划显示三遍0~9,但在显示一遍或 91 二遍时(不足3遍),P3.3引脚有外部中断1发生。此时,外部中断0控制的数码管暂停显示(高优先级中断打断了低优先级中断)。外部中断1控制的数码管开始显示数字0~9,显示三遍后,停止显示。外部中断0控制的数码管从暂停处继续显示,直至完成暂停前后共计三遍的显示。随后,主程序控制的数码管继续显示。 硬件说明: 1、 硬件连接电路如图4-21。按键K1和K2,分别连接到单片机P3.2和P3.3引脚上,作为外部中断0和外部中断1的输入源; 2、主程序控制的共阴极数码管,连接到单片机的P0口,段码使用74HC573驱动,公共端直接接地; 3、 外部中断0控制的共阴极数码管,连接到单片机的P2口,段码使用74HC573驱动,公共端直接接地; 4、外部中断1控制的共阴极数码管,连接到单片机的P1口,段码使用74HC573驱动,公共端直接接地; 图4-21 中断优先数码显示硬件连接图 程序清单如下: #include #define uint unsigned int #define uchar unsigned char uchar code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; uchar num=0,a,b,c; //************************************************************ void delay(uint i) //延时函数 { uint j,k; 92 for(k=0;k//************************************************************ void init() //初始化子函数 { PX0=0; //设置为低优先级 IT0=1; //设置外部中断0为边沿触发 EX0=1; //开外部中断0 PX1=1; //设置为高优先级 IT1=1; //设置外部中断1为边沿触发 EX1=1; //开外部中断1 EA=1; //开总中断允许位 } //************************************************************ main() //主函数 { init(); while(1) { num++; if(num==10) num=0; P0=table[num]; delay(1000); //软件延时1秒 } } //************************************************************ void int0(void) interrupt 0 //外部中断0的中断处理程序 { for(a=0;a<2;a++) for(c=0;c<10;c++) { P2=table[c]; delay(500); } } //************************************************************ void int1(void) interrupt 2 //外部中断1的中断处理程序 { for(b=0;b<2;b++) for(c=0;c<10;c++) { P1=table[c]; 93 } delay(500); } 此实例中,如果将P3.2和P3.3两只引脚同时连接到同一个按键K上。在前述优先级设置不变的情况下,在主程序执行期间,按下按键K时,主程序暂停,高优先级控制的数码管(连接在P1口)首先显示,等其显示结束后,低优先级中断控制的数码管再显示,等低优先级中断控制的数码管显示结束,主程序接着显示。此变化旨在说明,若优先级不同的两个中断同时发生,首先响应高优先级中断,然后才是低优先级中断。 4.5定时/计数器 4.5.1定时/计数器基本概念 1、什么是定时/计数器 从本质而言,定时/计数器的核心是计数器,之所以具有定时功能,原因是,如果被计数的对象是脉冲信号,则计数值乘以计数一个脉冲信号对应的时间(往往是机器周期),得到的结果便是时间值,也就是定时的时间。单片机应用系统中,很多场合需要实现精确定时或延时控制,在频率测量、脉宽测量、信号发生、信号检测,串行通信中波特率发生器等具体应用中,也会涉及到定时/计数功能,所以定时/计数器就成为单片机及其应用系统中不可或缺的基本组成部分。 2、 51单片机的定时/计数器 51单片机内部有两个定时/计数器,分别是T0和T1。如果T0和T1被用作计数器,则被计数的脉冲信号不仅必须来自于单片机外部,分别从P3.4引脚(对应T0)和P3.5(对应T1)引脚输入,而且,其最高频率还不能超过时钟频率的1/24。如果T0和T1被作为定时器使用,则T0和T1通过对片内机器周期的脉冲个数进行计数,从而实现定时功能。此处重点介绍T0和T1的定时器功能及使用方法。 4.5.2定时器应用实例1 第三章中,我们曾用for循环延时的方法,实现电子钟的时间显示,但误差较大。究其原因,主要是for循环在精确延时时,难以实现有效控制。若要实现精确延时,定时器无疑是最合适不过的选择了。 功能要求:设计一个使用数码管显示时间的电子钟。显示格式是“HH-MM-SS”,其中“HH”表示小时,“MM”表示分钟,“SS”表示秒。 硬件说明:本例使用硬件电路如图4-22所示。 94 图4-22 数码管电子钟硬件电路图 软件说明: 1、本例对定时/计数器的选择及设置,全部集中在主函数前面的初始化子函数init()中。正如程序清单中注释的说明,语句TMOD=0x01;即选定定时/计数器0,将它用作定时器(不是计数器),并设置其工作在方式1(16位)。接下来的两条语句TH0=(65536-50000)/256;和TL0=(65536-50000)%256;用于给定时器0的计数寄存器TH0和TL0装载初值,此种装载初值方法看似古怪,但其优点显而易见,就是从该赋值语句中能明确得知计数器的计数值,此例中的计数值即为50000。考虑到硬件电路中,外接晶振是12MHz,所以,立即可以得到此定时器的定时时间就是50000×1μs=50000μs=50ms。之所以选择50ms定时,究其原因,在外接12MHz晶振、定时器工作在方式1(16位)时,其最大定时时间是65536×1μs=65536μs=65.536ms,此最大定时时间距离电子钟所需的1秒定时还差很多,但可以通过多次毫秒级定时,实现1秒定时。此例选择20次50ms定时,从而实现1s定时,即50ms×20=1000ms=1s。接下来的两条语句ET0=1;和EA=1;用于允许定时/ 计数器0的溢出中断。最后一条语句TR0=1;用于启动定时器0,让定时器0的计数器开始加1计数。 2、在定时器0的中断服务子函数中,出现了与初始化子函数完全相同的两条初值装载语句TH0=(65536-50000)/256;和TL0=(65536-50000)%256;,这样处理的原因是,当一次50ms的定时完成时,即定时器0的计数器计满溢出(计数器0的当前计数值为0),定时器0的中断标志位TF0被置1。在定时器0溢出中断允许的前提下,CPU响应了该中断请求,程序就进入了该中断服务子函数(进入中断服务子函数后,TF0立即被清0)。由于需要多次50ms的定时,所以计数器0的初值需要重新装载,否则计数器0将从0开始反复加1,直至计满溢出,这样做的结果,将导致本次定时时间变成65.536ms,不再是50ms了。 程序清单如下: #include #define uchar unsigned char #define uint unsigned int 95 unsigned char duanca[10]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; unsigned char weica[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; sbit LW=P2^0; sbit LD=P2^1; uint hour=0,minute=0,second=0,second0=0; //************************************************************ void init() { TMOD=0x01; //设置定时器0为工作方式1 TH0=(65536-50000)/256; //定时50ms定时器高8位赋初值 TL0=(65536-50000)%256; //定时50ms定时器低8位赋初值 ET0=1; //定时器0中断允许 EA=1; //总中断允许 TR0=1; //启动定时器0 } //************************************************************ main() { uchar i; init(); while(1) { for(i=0;i<=7;i++) { LW=1; P0=weica[i]; LW=0; LD=1; switch(i) { case 0:P0=duanca[hour/10];break; case 1:P0=duanca[hour%10];break; case 2:P0=0xbf;break; case 3:P0=duanca[minute/10];break; case 4:P0=duanca[minute%10];break; case 5:P0=0xbf;break; case 6:P0=duanca[second/10];break; case 7:P0=duanca[second%10];break; default:break; } LD=0; } } } 96 //************************************************************ void timer0(void) interrupt 1 //定时器0中断服务程序 { TH0=(65536-50000)/256; //定时50ms定时器高8位重新赋初值 TL0=(65536-50000)%256; //定时50ms定时器低8位重新赋初值 second0++; if(second0==20) { second0=0; second++; if(second==60) { second=0; minute++; if(minute==60) { minute=0; hour++; if(hour==24) hour=0; } } } } 单片机知识:定时/计数器 1、机器周期与外接晶振频率的关系 在单片机应用系统中,常用的晶振如图4-23所示,其上的数值就是其振荡频率,单位是MHZ。其中,11.0592MHz的晶振主要用于串行通信,有兴趣的读者可参阅本教材第9章中相关解释或说明。在定时/计数器部分,以6MHz和12MHz晶振的使用最为普遍。 图4-23 常用晶振外形 前已述及,定时器其实是对机器周期的脉冲个数进行计数的计数器,其计数值乘以机器周期,即得定时时间。显然,计数值若相同,但机器周期值不同,对应的定时时间值也必然不同。而机器周期与单片机外接晶振频率密切相关,其具体关系是:机器频率等于外接晶振频率的 97 1/12,即机器周期等于12除以外接晶振频率,单位是秒。如果外接晶振频率是6MHz,则机器周期为12/6MHz=2μs;如果外接晶振频率是12MHz,则机器周期为12/12MHz=1μs;如果外接晶振频率是24MHz,则机器周期为12/24MHz=0.5μs。对于12MHz外接晶振而言,如果计数值是50000,则定时时间就是50000×1μs=50000μs=50ms。 2、定时器的工作原理 51单片机的定时器是基于其加1计数器的。所谓加1计数器,就是每过一个机器周期的时间,该计数器的当前计数值就加1。对于51单片机,其内部的计数器为16位,最大计数值可 16 达2-1=65535。这个16位的计数器是由两个8位的计数器合并而成,具体就是:定时/计数器T0的16位计数器,是由8位的计数器TH0和8位的计数器TL0合并而成;定时/计数器T1的16位计数器,是由8位的计数器TH1和8位的计数器TL1合并而成。合并时,TH作为16位计数器的高8位,TL作为16位计数器的低8位。 在具体解释定时器工作原理之前,有必要分清楚两个概念:计数器的当前值和计数器的计数值。简言之,计数器的当前值就是计数器的当前计数值,该值由于反复不断地被加1而逐渐变大,直至加满溢出;相反,计数器的计数值则是计数器在某一段时间内计量机器周期的个数。此二者之间有本质差异。例如,假设计数器计满100便溢出,如果计数器的当前值是60,则经过一个机器周期之后,计数器的当前值就变成61,再经过一个机器周期之后,计数器的当前值就变成62,……。当计满溢出时,计数器的当前值变成了0,此时的计数值则为100-60=40。 定时器开始定时工作之前,首先要给定时器的当前值赋值一个初始值,该初始值一般被称为初值。定时器开始工作之后,计数器的当前值就从初值开始,每过一个机器周期的时间,当前值就加1。伴随着一个又一个机器周期的流逝,计数器的当前值就反复不断地加1;当计数器当前值计满溢出时(比如,计数器的当前值经反复不断地加1,达到十六进制数0xFFFF时,再加1便溢出了,即计数器的当前值因为溢出变成了0),单片机内部定时/计数器的中断标志位就会臵1。由于该中断请求信号是因计数器的当前值计满溢出引起的,所以定时/计数器的中断一般被称为溢出中断。此时,在中断允许的条件下,CPU就会响应该定时/计数器的中断请求,接下来就可以执行定时/计数器的中断服务子函数了。当然,编程者也可以通过编程,用软件去查询定时/计数器的中断标志位是否为1,从而确定本次定时器的定时工作是否已经完成,如果完成(标志位为1),也可使程序转去执行相应的服务程序。 一次定时完成之后,如果还需要第二次、甚至更多次同样长时间的定时,则需要将定时器的初始值重新赋值或装载到计数器的当前值中。原因是,上一次计数器的当前值计满溢出时,已使计数器的当前值变成了0;如果不重新装载初始值,计数器将从当前值0开始,反复不断地加1,直至加满溢出,这与当前值从某一初值开始,反复不断加1到溢出,有着明显不同,定时的时间自然是不正确或者说是错误的。 综上所述,51单片机的定时器可以工作在中断方式,也可以工作在查询方式。无论工作在哪种方式之下,定时器一旦开始工作,其计数器的当前值就会从装载的初始值开始,反复不断地加1,直至加满溢出;而定时时间,则需要通过计数值(计数值=溢出值-初值)乘以机器周 16 期得到。例如,假设单片机外接晶振是12MHz,而计数器的溢出值是2=65536。当定时器的初值是15536时,则定时一次的时间就是(65536-15536)×1μs=50000×1μs=50ms。 3、与定时器有关的寄存器 前已述及,51单片机中的定时器,可以工作在查询和中断两种方式下。如果定时器工作在查询方式,则其使用和控制主要涉及定时器方式寄存器TMOD和定时器控制寄存器TCON;如果定时器工作在中断方式,则还要涉及中断允许寄存器IE和中断优先级寄存器IP。在上节内容中,已就中断优先级寄存器IP做过详细介绍,此处不再赘述,请读者参见上节相关内容,以下就另外3个寄存器中与定时器有关的部分做具体说明。 (1)中断允许寄存器IE 98 如果定时器工作在中断方式下,则当定时器的当前值计满溢出时,就会触发定时器溢出中断。此时,中断允许寄存器IE中与定时器有关的位7、位3和位1,如果被设臵为1(允许),则CPU可能会响应该定时器的中断请求(此处的‚可能‛是考虑到存在中断优先级问题)。IE寄存器如表4-5所示。其中的位7、位3、位1的功能及含义如下: 表4-5 IE寄存器 位7 位6 位5 位4 位3 位2 位1 位0 EA ES ET1 EX1 ET0 EX0 ◆位1——ET0:定时/计数器0中断允许位。ET0=1,允许定时/计数器0中断;ET0=0,禁止定时/计数器0中断。 ◆位3——ET1:定时/计数器1中断允许位。ET1=1,允许定时/计数器1中断;ET1=0,禁止定时/计数器1中断。 ◆位7——EA:中断允许总控制位。EA=1,允许所有中断源中断;EA=0,禁止所有中断源中断。 (2)定时器控制寄存器TCON 在外部中断一节中,我们已就定时器控制寄存器TCON中,与外部中断有关的低4位做过详细介绍;TCON的高4位,则与定时/计数器0和定时/计数器1密切关联,如表4-6所列,此处作以重点介绍。此4位的功能及含义如下: 表4-6 TCON寄存器 位7 位6 位5 位4 位3 位2 位1 位0 TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0 ◆位4——TR0:定时/计数器0运行控制位。TR0=1,启动定时/计数器0;TR0=0,停止定时/计数器0。 ◆位5——TF0:定时/计数器0溢出中断标志位,该位是供软硬件查询的状态位,用于表征定时/计数器0是否有溢出中断请求。如果软硬件查询到的结果是TF0=1,则表明定时/计数器0有溢出中断请求;相反,如果查询到的结果是TF0=0,则表明定时/计数器0没有溢出中断请求。在软硬件查询到的结果是TF0=1,表明定时/计数器0有溢出中断请求时,若该中断一旦被响应,则TF0被自动清0,即编程者不需要用软件刻意将其清0。 ◆位6——TR1:定时/计数器1运行控制位。TR1=1,启动定时/计数器1;TR1=0,停止定时/计数器1。 ◆位7——TF1:定时/计数器1溢出中断标志位,该位是供软硬件查询的状态位,用于表征定时/计数器1是否有溢出中断请求。如果软硬件查询到的结果是TF1=1,则表明定时/计数器1有溢出中断请求;相反,如果查询到的结果是TF1=0,则表明定时/计数器1没有溢出中断请求。在软硬件查询到的结果是TF1=1,表明定时/计数器1有溢出中断请求时,若该中断一旦被响应,则TF1被自动清0,即编程者不需要用软件刻意将其清0。 (3)定时器方式寄存器TMOD 定时器方式寄存器TMOD用于设臵定时/计数器0和定时/计数器1的工作方式等内容,它是一个8位的寄存器,高4位和低4位对应相同,如表4-7所示。其中,低4位用于设臵定时/计数器0的相关项,是定时/计数器0的方式控制字;而高4位则用于设臵定时/计数器1的相关项,是定时/计数器1的方式控制字。其各位的功能及含义如下: 表4-7 TMOD寄存器 位7 GATE 位6 C/T 位5 M1 位4 M0 位3 GATE 位2 C/T 位1 M1 位0 M0 T1方式控制字 T0方式控制字 99 ◆位1和位0——M1M0:定时/计数器0工作方式位。位1和位0这两个二进制位组合起来,对应定时/计数器0有四种工作方式可供选择——方式0,方式1,方式2和方式3。M1M0具体组合与四种工作方式的对应关系如表4-8所示。 表4-8 工作方式选择表 M1 M0 0 0 0 1 1 0 1 1 方 式 0 1 2 3 说 明 13位定时/计数器,TL存放低5位,TH存放高8位 16位定时/计数器 初值自动装载的8位定时/计数器 T0被分为两个8位计数器;T1在方式3时停止工作(无中断重装8位计数器) 方式0是13位定时/计数器,此处所说13位,是指定时/计数器0的最大计数数值为13 2-1=8191,即当定时/计数器0的计数当前值达到8191时,再加上1就会溢出;同理,方式1 16 中所说16位定时/计数器的含义是,其定时/计数器0的最大计数数值为2-1=65535,再加上1就会溢出;方式2是初值自动装载的8位定时/计数器,其中的8位是指定时/计数器0的最大 8 计数数值为2-1=255,当定时/计数器0的计数当前值达到255时,再加上1就会溢出。对于初值自动装载功能,可暂时不用细究,本节后面将给予详细说明;方式3比较特殊,因使用较少,此处不再赘述,有兴趣的读者请参阅其它资料。 ◆位2——C/T:定时或计数方式选择位。C/T=1,定时/计数器0被用作计数器;C/T=0,定时/计数器0被用作定时器。 ◆位3——GATE:定时/计数器0运行控制位,简称门控位。GATE=0,只要TCON寄存器的位4(TR0位)臵1,就可以启动定时/计数器0;GATE=1,在TCON寄存器的位4(TR0位)臵1的前提下,还需P3.2引脚为高电平,才能启动定时/计数器0。 位5和位4、位6、位7的功能与含义,分别与位1和位0、位2、位3对应相同,不同之处仅仅是,此高四位是针对定时/计数器1而言的。 4.5.3定时器应用实例2 功能要求:设计一个使用数码管显示时间的电子钟。显示格式是“HH-MM-SS”,其中“HH”表示小时,“MM”表示分钟,“SS”表示秒。 硬件说明:本例使用图4-22所示硬件电路。 软件说明: 1、本例使用定时器1,并使其工作在方式2(初值自动装载的8位定时/计数器)。在外接12MHz晶振时,方式2的最大定时时间是28μs=256us=0.256ms。此例选用5000次0.2ms的定时,同样可实现1秒定时。 2、由于定时器1工作在方式2,而方式2是初值自动装载的8位定时/计数器。所以在初值装载时,TH1和TL1都被赋值相同的值56(语句TH1=256-200;和TL1=256-200;)。同时,在定时器1的中断服务子函数中,也无需再重装初值。因为当TL1计数器的当前值计满溢出时(TL1的当前值为0),单片机内部会自动将TH1的值赋值给TL1,此过程类似于赋值语句TL1=TH1; ,TL1就被重新装载了初值。 程序清单如下: #include #define uchar unsigned char #define uint unsigned int unsigned char duanca[10]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; unsigned char weica[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; 100 sbit LW=P2^0; sbit LD=P2^1; uint hour=0,minute=0,second=0,second0=0; //************************************************************ void init() { TMOD=0x20; //设置定时器1为工作方式2 TH1=256-200; //定时0.2ms定时器高8位赋初值 TL1=256-200; //定时0.2ms定时器低8位赋初值 ET1=1; //开定时器0中断 EA=1; //开总中断 TR1=1; //启动定时器1 } //************************************************************ main() { uchar i; init(); while(1) { for(i=0;i<=7;i++) { LW=1; P0=weica[i]; LW=0; LD=1; switch(i) { case 0:P0=duanca[hour/10];break; case 1:P0=duanca[hour%10];break; case 2:P0=0xbf;break; case 3:P0=duanca[minute/10];break; case 4:P0=duanca[minute%10];break; case 5:P0=0xbf;break; case 6:P0=duanca[second/10];break; case 7:P0=duanca[second%10];break; default:break; } LD=0; } } } //************************************************************ void timer1(void) interrupt 3 //定时器1中断服务程序 101 { } second0++; if(second0==5000) { second0=0; second++; if(second==60) { second=0; minute++; if(minute==60) { minute=0; hour++; if(hour==24) hour=0; } } } 单片机知识:定时/计数器初值的计算与装载 对于51单片机而言,定时/计数器的初值计算和装载,是定时/计数器使用中的一个重要环节。在单片机C语言中,计算并装载定时/计数器的初值,就是给8位计数器TH和TL赋值(对定时/计数器0,就是对TH0和TL0赋值;对定时/计数器1,则是对TH1和TL1赋值)。以下以定时器0为例,针对定时器0工作在方式0、方式1和方式2三种情形,说明如何计算和装载初值。定时器1与定时器0类似,可以参照实现。此处假定单片机外接晶振是12MHz,一个机器周期就是1μs(如果单片机外接晶振是6MHz,则一个机器周期就是2μs了)。 1、方式0 方式0是13位定时/计数器,但计数器TH0和TL0组合之后是16位,则方式0的13位究竟占用16位计数器中的哪些位呢?答案是,TH0的8位和TL0的低5位。显然,TL0的高3位无效或者说未使用。 13 对于13位的定时/计数器,其最大计数数值即为2-1=8192-1=8191。在12MHz晶振之下,最大的定时时间是8192×1μs=8.192ms; 在6MHz晶振之下,最大的定时时间是8192×2μs=16.384ms。 在方式0,初值的装载语句如下: TH0=(8192-计数值)/32; TL0=(8192-计数值)%32; 其中的计数值是以μs为单位的,其相当于定时值;用32去整除和以32为模求余数的原 5 因是,TL0计数器的高3位无效,TL0的低5位有效(2=32)。 例如,如果计划定时5ms,则定时时间为5ms=5000μs。在12MHz晶振之下,计数器的计数值就为5000μs/1μs=5000,则语句TH0=(8192-5000)/32;和TL0=(8192-5000)%32;就可实现初值的装载。 中断方式下,在需要连续多次定时的情况下,在中断服务程序中,一般都需要重新装载初 102 值。 2、方式1 方式1是16位定时/计数器,TH0的8位和TL0的8位全部被使用。 16 对于16位的定时/计数器,其最大计数数值即为2-1=65536-1=65535。在12MHz晶振之下,最大的定时时间是65536×1μs=65.536ms; 在6MHz晶振之下,最大的定时时间是65536×2μs=131.072ms。 在方式1,初值的装载语句如下: TH0=(65536-计数值)/256; TL0=(65526-计数值)%256; 其中的计数值是以μs为单位的,其相当于定时值;用256去整除和以256为模求余数的 8 原因是,TL0计数器的8位全部有效(2=256)。 例如,如果计划定时50ms,则定时时间为50ms=50000μs。在12MHz晶振之下,计数器的计数值就为50000μs/1μs=50000,则语句TH0=(65536-50000)/256;和TL0=(65536-50000)%256;就可实现初值的装载。 中断方式下,在需要连续多次定时的情况下,在中断服务程序中,一般都需要重新装载初值。 3、方式2 方式2是初值自动装载的8位定时/计数器,TH0和TL0分别单独使用,但被装载同样的初值。 8 对于8位的定时/计数器,其最大计数数值即为2-1=256-1=255。在12MHz晶振之下,最大的定时时间是256×1μs=256μs; 在6MHz晶振之下,最大的定时时间是256×2μs=512μs。 在方式2,初值的装载语句如下: TH0=256-计数值; TL0=256-计数值; 其中的计数值是以μs为单位的,其相当于定时值。 例如,如果计划定时200μs,则在12MHz晶振之下,计数器的计数值就为200μs/1μs=200,则语句TH0=256-200;和TL0=256-200;就可实现初值的装载。 由于具有初值自动装载功能,所以在中断方式下,若需要连续多次定时,在中断服务程序中,就不再需要重新装载初值了。 4.5.4数码管动态显示时钟 功能要求:设计一个使用数码管显示时间的电子钟。显示格式是“HH-MM-SS”,其中“HH”表示小时,“MM”表示分钟,“SS”表示秒。 硬件说明:本例使用图4-22所示硬件电路。 软件说明: 1、本例使用定时器1并使其工作在方式1,定时50ms,连续定时20次即为1秒; 2、本例未使用中断,改用软件查询定时/计数器1的溢出标志位TF1。如果TF1置1,则50ms定时完成,同时将TF1清零,开始下一个50ms的定时。就这样,连续20次的50ms定时,就可完成1秒钟的定时。 程序清单如下: #include #define uchar unsigned char #define uint unsigned int unsigned char duanca[10]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; 103 unsigned char weica[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; sbit LW=P2^0; sbit LD=P2^1; uint hour=0,minute=0,second=0,second0=0; //************************************************************ void delay1s(void) { uint i; for(i=0;i<20;i++) { TH1=(65536-50000)/256; TL1=(65536-50000)%256; TR1=1; while(!TF1); TF1=0; } } main() { uchar i; while(1) { for(i=0;i<=7;i++) { LW=1; P0=weica[i]; LW=0; LD=1; switch(i) { case 0:P0=duanca[hour/10];break; case 1:P0=duanca[hour%10];break; case 2:P0=0xbf;break; case 3:P0=duanca[minute/10];break; case 4:P0=duanca[minute%10];break; case 5:P0=0xbf;break; case 6:P0=duanca[second/10];break; case 7:P0=duanca[second%10];break; default:break; } LD=0; delay1s(); second++; if(second==60) 104 { second=0; minute++; if(minute==60) { minute=0; hour++; if(hour==24) hour=0; } } } } } 思考题 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 简述键盘的用途和分类。 机械式按键组成的键盘应如何消除按键抖动? 按键和矩阵按键各有什么特点?它们各自适用于哪些场合? 按键如何工作? 画出4×4矩阵键盘的连线图。 简述矩阵键盘的扫描原理。 简述中断、中断源、中断优先级及中断嵌套的含义。 MCS-51单片机的中断源有哪几个?各个中断的源的优先级如何设置?同一优先级时,各个中断源的排序如何确定? 简述MCS-51单片机的中断响应过程。 MCS-51单片机外部中断有哪两种触发方式?如何选择?对外部中断源的触发脉冲或电平各有何具体要求? MCS-51单片机应用系统中,如果有多个外部中断源申请中断,应怎样进行处理? 试说明IE寄存器及IP寄存器中各位的功能。 MCS-51单片机定时/计数器的定时功能和计数功能有何异同?其应用场合是怎样的? 简述定时器的工作原理。 当单片机外接晶振频率为6MHz,定时/计数器工作在方式1时,其最短定时时间和最长定时时间各是多少? MCS-51单片机定时器的工作方式有哪些?如何进行选择和设定? MCS-51单片机中,定时/计数器的查询方式和中断方式有何异同? 如何设定定时器的初值? 中断服务子函数的一般形式是什么?各个中断源的中断号各是多少? 105 因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- 69lv.com 版权所有 湘ICP备2023021910号-1
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务