基于Proteus的单片机项目实践教程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

项目3 制作手动计数器

【项目引入】

在运动场上,裁判经常来记录双方的分数;在仓库、码头,需要记录行人或车辆过往的数量统计等。在这些场合中,都需要使用到手动计数器来协助人完成工作,如图3-1所示。本项目就是设计一种简单的手动计数器,利用数码管显示计数值。

图3-1 手按计数器

【知识目标】

● 掌握数码管的动态、静态显示的不同以及电路连接;

● 掌握单片机的外部中断;

● 掌握中断程序的编写;

● 理解中断过程。

【技能目标】

● 掌握Proteus中数码管的共阴、共阳的不同;

● 掌握数码管和单片机的动态连接方法。

3.1 任务描述

利用单片机制作一个手动的0~99计数器,要求设置一个按键手动计数,利用数码管实时显示计数结果。

3.2 准备知识

3.2.1 数码管静态显示

数码管是单片机常用的数字或字符显示部件。

1.数码管的显示原理

(1)LED数码管结构

数码管是由LED发光二极管组合显示字符的显示器件。它使用了8个LED发光二极管,其中7个用于显示字符,1个用于显示小数点,故通常称为7段(也有称为8段)发光二极管数码显示器。

单片机系统常用的数码管有共阳极和共阴极两种类型,两种类型的数码管外形和结构类似,只是数码管内部组成数码段和点的LED接法有区别。共阳极数码管的内部所有LED的正极接在一起为公共极引脚,简称com端;负极分别引出,依次命名为a、b、c、d、e、f、g、dp,简称段值端,如图3-2所示。图3-3所示为共阴极数码管。

图3-2 共阳极数码管

图3-3 共阴极数码管

(2)LED数码管工作原理

使用时,共阳极数码管的公共极接正极,其他引脚分别接驱动电路,数码管显示时低电平有效。由于共阴极数码管内部所有LED的负极接在一起,所以数码管显示时驱动数据高电平有效。数码管可以显示0到9共10个数字,如果加上小数点的显示,驱动一个数码管显示至少需要8位有效数据。驱动数码管显示数字的8位数据编码如表3-1所示。

表3-1 共阳极数码管显示编码

2.数码管的驱动电路

根据LED数码管和单片机的连接方式,数码管的显示方式分为静态显示和动态显示。本节主要讲静态显示。

(1)静态显示原理

数码管静态显示的电路连接图如图3-4所示图中有4位数码管,数码管的公共端COM接固定的高/低电平,每位数码管的段值端a~g和dp端与一个8位的I/O相连。要在某一位数码管上静态显示字符时,只要从对应的I/O口输出其显示编码即可。

图3-4 静态显示电路图

静态显示的特点是数码管恒定地亮,亮度较高,显示某个数值,直到显示字符的编码改变为止。这种显示方式由于太占据I/O线,所以用于1个或较少数码管显示的场合。

(2)举例

例1:设计电路,使1位数码管(共阳)依次循环显示0~F。

根据题意,本例所需的电路只需在单片机的最小系统基础增加一个数码管即可,选择共阳极数码管通过限流电阻接到单片机的P2口,如图3-5所示。电路中需要用排阻来限制数码管每一段电流,以防止驱动电流过大而烧毁器件。

图3-5 1位数码管显示电路

其中P2.0~P2.7口分别接数码管的a~g引脚,共阳极数码管的公共端接高电平,P2的每个端口只要有低电平输出,对应的数码管的那个段就显示。如果要数码管显示某个数值,只要从P2口输出对应的共阳极数码管段值编码即可。例如让数码管显示1,数码管b、c段亮,程序控制P2输出0xbe十六进制编码即可。因此共阳极数码管要显示0~F,只需按顺序把0~F的共阳极数码管段值编码依次从P2口输出即可。

由于0~F的共阳极数码管段值编码毫无规律,所以本程序考虑到运用数组,把0~F的共阳数码管段值编码放在一个数组里面,为了让P2口依次输出0~F数字,让P2口的内容依次在数组中取值即可。程序清单如下:

        /*****************************************************************************/
        #include<reg51.h>
        unsigned char code sz1[ ]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,
        0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};            //0~F的共阳极数码管段值编码数组
        void delay(unsigned int a)                      //时间延迟函数delay()
        {
          unsigned char j;
          while(a--)
          {
            for(j=0;j<120;j++);
          }
        }
         void main (void)
        {
            unsigned char i;                         //变量i作为数组的0~9编号
            while (1)
            {
            for(i=0;i<16;i++)
              {
              P2=sz1[i];                           //输出0~F到共阳七段显示器
              delay(1000);                          //调用时间延迟函数delay()
              }
            }
        }
        /*****************************************************************************/

程序说明:

● 数码管显示0到F数字过程中,数字的变化需要有一定的时间间隔,因此程序还要用到delay()函数。

● 当程序中使用常量数据时,如共阳极数码管数字显示编码、液晶显示器的汉字编码等,一般希望这些数据当程序下载到单片机时存放在单片机的ROM区,对此类数据声明前面需要加上关键字code或const,定义为程序存储器存储类型。

● 为了处理方便,C语言把具有相同类型的若干变量或常量,用一个带下标数组定义。对各个变量的相同操作可以利用循环改变下标值来进行重复的处理,使程序变得简明清晰。带下标的变量由数组名称和用方括号括起来的下标共同表示,称为数组元素。通过数组名和下标可直接访问数组的每个元素,下标必须从0开始。在C语言中使用数组必须先进行定义或声明,一维数组的定义格式为:

        数据类型 数组名[常量表达式]

在程序中,一维数组元素可以直接作为变量或常量直接引用,其引用格式为:

        数组名[下标]

例2:设计电路,使两位数码管显示0~99。

根据题意,本例所需的电路只需在例1的基础上增加一个数码管即可,两个数码管,高位数码管接单片机的P2口,低位数码管接单片机的P3口,如图3-6所示。

图3-6 两位数码管静态显示

此例与例1类似,不同的是两位数码管显示,范围0~99,是以十进制的模式显示,分十位、个位。为了方便,把和P2口相连的数码管定为十位,把和P3口相连的数码管定为个位。

程序清单如下:

            /*****************************************************************************/
                #include <REG51.h>
                unsigned char code sz1[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
                void delay(unsigned int a)
                {
                unsigned char t;
                while(a--)
                {
                for(t=0;t<120;t++);
                }
              }
              void main()
              {
                unsigned char m,i,j;
                while(1)
                {
                for(m=0;m<100;m++)
                  {
                      i=m/10;             //分离出m的十位
                      j=m%10;            //分离出m的个位
                      P2=sz1[i];           //把十位转换为段值送P2口
                                        P3=sz1[j];           //把个位转换为段值送P3口
                                        delay(1000);
                                      }
                                  }
                              }
                              /*****************************************************************************/

程序说明:

此例中的显示要求以十进制形式显示,所以在设计程序中,要把以十六进制加1的变量m转换为十进制数的十位、个位,然后分别进行显示。

3.2.2 数码管动态显示

1.动态显示原理

数码管动态显示的电路连接图如图3-7所示,图中含有4位数码管,每个数码管的公共COM端(位选端)和不同的I/O口相连,每位数码管的段值a~g和dp端(段值端)接在一起,与一个8位的I/O相连。要在某一位数码管上显示字符时,首先和该数码管COM端相连的I/O口有效,然后从对应的I/O口输出其显示编码即可。动态显示特点为:数码管轮流点亮,显示亮度不够,所以通常加驱动电路。由于此种显示方式可以节省I/O线,所以用于多个数码管显示的场合。

图3-7 数码管动态显示

由于段值端是共用的,要想每个数码管显示不同的数值,就必须用动态扫描方式进行显示。首先从与段值端相连的I/O口送出要显示字符的编码,接着让要显示字符的数码管的位选端有效,其他数码管的位选端无效,然后延时一段时间(几毫秒左右),最后关闭所有显示,这样完成一个数码管的显示;其他数码管也按照此方法轮流显示。但由于人的眼睛有视觉暂留效应,捕捉不到这么快的变化,当延时时间设置的合理时,人眼感觉到几个数码管是在稳定地一起显示。

延时时间的长短对数码管显示效果有很大影响。因为人眼睛有视觉停留的效果,只要图像变化不小于24桢看起来就是连续的。电影就是按照这个原理制成的。数码管也一样,只要频率大于24Hz就行了,即扫描一次时间小于40ms。若是多个LED显示的话,则每个LED的显示扫描时间应小于40ms/LED个数。扫描时间太长(扫描太慢),看起来会有闪烁的感觉,或者不能形成有效数字。如果扫描时间太短(扫描太快),就会造成显示为全亮(但亮度不是很高),但是有个别亮度会大一些,一般最小为1ms。

2.数码管驱动电路

在动态显示的电路中,由于几个数码管的同一个段值端连接在一个I/O位线上,而1个I/O位线的驱动能力大概只有10mA左右,无法驱动多个段值端,所以在动态显示的电路中,往往要加入驱动电路,增加I/O口的驱动能力,增大电流。否则在数码管较多时,会出现颜色太暗,有时甚至会缺笔。

在单片机的控制电路中,可以用三极管(8550PNP,8050NPN)、反相器(74LS04)、译码器(74HC138)、驱动器(74LS245)、锁存器(74HC573)等增加I/O口的驱动能力。其中以三极管最为常见,如图3-8所示,为共阳极数码管的三极管驱动电路,利用PNP型三极管作为驱动,设计电路时注意结合三极管电流的流向来连接共阳极或共阴极的数码管。

图3-8 共阳极数码管的三极管驱动电路

3.举例

例3:利用数码管的动态显示,设计两位数码管循环显示0~99。

按照前面的讲解,本例选用三极管作为驱动器件,设计电路如图3-9所示。两位数码管的段值端通过限流电阻和P2口相连,位选端分别和P3.6、P3.7相连。当需要某个数码管显示字符时,只需与位选端相连的I/O口输出高电平,与段值端相连的P2口输出数码管的共阳极段值编码即可。

图3-9 数码管动态显示

程序清单如下:

            /*****************************************************************************/
                #include <REG51.h>
                unsigned char code sz1[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
                sbit seg1=P3^6;
                sbit seg2=P3^7;
                void delay(unsigned int a)
                {
                unsigned char b;
                while(a--)
                {
                for(b=0;b<120;b++);
                }
                }
      void main()
      {
       unsigned char m,i,j,t;
       P3=0xff;
       while(1)
       {
        for(m=0;m<100;m++)
        {
          for(t=0;t<80;t++)
          {
          i=m/10;
          j=m%10;
          P2=sz1[i];              //数码管动态显示1步——送段值
          seg1=0;                //数码管动态显示2步——位选有效
          delay(10);              //数码管动态显示3步——延时
          P3=0xff;               //数码管动态显示4步——关闭
          P2=sz1[j];
          seg2=0;
          delay(10);
          P3=0xff;
      }
    }
    }
  }
  /*****************************************************************************/

程序说明:

● 数码管动态显示编程时可总结为4步:送段值、送位选、延时、关闭。

● 此例中P3.6,P3.7经过三极管驱动接到数码管的位选端,以P3.6为例,P3.6输出低电平,经过三极管电流放大,反相,输出高电平送到数码管的位选端1,位选有效,选中左边数码管。在程序设计中直接用置位/复位指令实现位选有效,在一些数码管较多的情况下,也可以使用移位指令实现由上一个位选信号得到下一个位选信号。

● 此例中delay( )延时时间大概1ms,主程序中使用delay(10),所以数码管的动态扫描时间大概是10ms左右。

● 程序中用到了for(t=0;t<80;t++)循环,主要是控制数码管显示数的快慢。如果去掉,每个数显示太快。

3.2.3 外部中断

1.中断基本概念

在单片机中,当CPU在执行程序时,由单片机内部或外部的原因引起的随机事件要求CPU暂时停止正在执行的程序,而转向执行一个用于处理该随机事件的程序,处理完后又返回被中止的程序断点处继续执行,这一过程就称为中断,如图3-10所示。单片机处理中断的4个步骤:中断请求、中断响应、中断处理和中断返回。

图3-10 中断定义

向CPU发出中断请求的来源,或引起中断的原因称为中断源。中断源要求服务的请求称为中断请求。中断源可分为两大类:一类来自单片机内部,称为内部中断源;另一类来自单片机外部,称为外部中断源。

2.单片机的中断系统

单片机中断系统的结构如图3-11所示,含有5个中断源,并提供两个中断优先级控制,能够实现两级中断服务程序的嵌套。单片机的中断系统是通过4个相关的特殊功能寄存器TCON、SCON、IE和IP来进行管理的。因此用户可以用软件对每个中断的开和关以及优先级的控制作用。

3.单片机中断源(5个)

(1)外部中断

外部中断是由外部原因(如打印机、键盘、控制开关、外部故障)引起的,可以通过两个固定引脚即外部中断0(INT0)和外部中断1(INT1),把外部中断请求信号输入到单片机内。

外部中断0(INT0)请求信号输入引脚即为P3.2,当单片机检测到P3.2引脚上出现有效的中断信号时,向CPU申请中断。

外部中断1(INT0)请求信号输入引脚即为P3.3。当单片机检测到P3.3引脚上出现有效的中断信号时,向CPU申请中断。

(2)内部中断

① 定时中断类。定时中断是由内部定时(或计数)溢出或外部计数溢出引起的,即定时器0(T0)中断和定时器1(T1)中断。

当定时器对单片机内部定时脉冲进行计数而发生计数溢出时,即表明定时时间到,向CPU申请中断;或者当定时器对单片机外部计数脉冲进行计数而发生计数溢出时,即表明计数次数到,向CPU申请中断。

片内定时/计数器T0溢出中断标志位为TF0,当定时/计数器T0发生溢出时,置位TF0,并向CPU申请中断。

片内定时/计数器T1溢出中断标志位为TF1,当定时/计数器T1发生溢出时,置位TF1,并向CPU申请中断。

② 串行口中断类。串行口中断是为接收或发送串行数据而设置的。

串行口中断,包括RI或TI,当发送或接收完一帧数据时,置位RI或TI,并向CPU申请中断。

4.中断优先级

单片机的中断系统具有两级优先级控制,系统在处理时遵循下列基本原则:

① 低优先级的中断源可被高优先级的中断源中断,而高优先级中断源不能被低级的中断源所中断。

② 一种中断源(无论是高优先级或低优先级)一旦得到响应,就不会被同级的中断源所中断。

③ 低优先级的中断源和高优先级的中断源同时产生中断请求时,系统先响应高优先级的中断请求,后响应低优先级的中断请求。

④ 多个同级的中断源同时产生中断请求时,系统按照默认的顺序先后予以响应,5个中断默认优先级如表3-2所示。

表3-2 中断入口地址及优先级排列表

5.中断系统使用的特殊功能寄存器(SFR)

要使用单片机的中断功能,必须掌握几个相关的特殊功能寄存器中特定位的意义及其使用方法。下面分别介绍这几个特殊功能寄存器对中断的具体管理方法。

(1)中断允许控制寄存器IE(interrupt enable),字节地址为A8H

单片机的CPU对中断源的开放或屏蔽(即关闭),是由片内的中断允许寄存器IE控制的。IE的字节地址是A8H,既支持字节操作,又支持位操作。位地址的范围是A8H~AFH。8位中有6位与中断有关,剩下的两位没有定义。其格式如下:

EA为CPU的中断开放标志。EA=0时,CPU屏蔽所有的中断请求,此时即使有中断请求,系统也不会去响应;EA=1时,CPU开放中断,但每个中断源的中断请求是允许还是被禁止,还需由各自的控制位确定。

ES为串行口的中断控制位。ES=1,允许串行口中断;ES=0,禁止串行口中断。

ET1:定时器/计数器1的溢出中断控制位。ET1=1,T1的中断开放,ET1=0,T1的中断被关闭。

EX1为外部中断1的中断控制位。EX1=1,允许外部中断1中断;EX1=0,禁止外部中断1的中断。

ET0为定时器/计数器T0的溢出中断控制位。ET0=1时允许T0中断;ET0=0,禁止T0中断。

EX0为外部中断0的中断控制位。EX0=1,允许外部中断0的中断;EX0=0,禁止外部0的中断。

可见,EA=0时,所有的中断都被屏蔽,此时IE低5位的状态没有任何作用。EA=1时,可以通过对IE低5位的设置来开放或关闭相应的中断。单片机复位后,IE寄存器被清零,所有的中断都被屏蔽。实现相应的中断源允许中断或禁止,可以位寻址,用户根据要求用指令置位或复位,当然也可以采用字节操作来实现。

例如,如图3-12所示,两个外设中断请求信号分别接在P3.2和P3.3上。

图3-12 外部中断电路

根据题意,要求开放外部中断0和外部中断1,关闭内部中断,则可以使用两条置位指令:EA=1;EX0=1;EX1=1。如果使用字节操作方式,则一条语句即能实现,即IE=0X85。

图3-11 8051单片机中断系统的结构

(2)定时控制寄存器TCON,字节地址为88H

定时控制寄存器TCON是定时器/计数器T0和T1的控制寄存器,也用来锁存T0和T1的溢出中断请求TF0、TF1标志及外部中断请求源标志IE0、IE1。TCON的字节地址88H,既支持字节操作,又支持位操作。位地址的范围是88H~8FH,每一个位单元都可以用位操作指令直接处理。其格式如下:

IT0为外部中断0()触发方式控制位,用于设定中断请求信号的有效方式。如果将IT0设定为1,则外部中断0为边沿(脉冲)触发方式,CPU在每个机器周期的S5P2采样的输入信号(即单片机的P3.2脚)。如果在一个机器周期中采样到高电平,在下一个机器周期中采样到低电平,则硬件自动将IE0置为“1”,向CPU请求中断。如果IT0为0,则外部中断0为电平触发方式。此时系统如果检测到端输入低电平,则置位IE0。采用电平触发时,输入到端的外部中断信号必须保持低电平,直至该中断信号被检测到。同时在中断返回前必须变为高电平,否则会再次产生中断。概括地说,IT0=1时,的中断请求信号是脉冲后沿(负脉冲)有效,即P3.2从1变为0时系统认为有中断请求;IT0=0时,的中断请求信号是低电平有效,即P3.2保持为0时系统认为有中断请求。

IE0为外部中断0的中断请求标志位。如果IT0置1,则当P3.2上的电平由1变为0时,由硬件置位IE0,向CPU申请中断。如果CPU响应该中断,在转向中断服务时,由硬件自动将IE0复位。

IT1为外部中断1(INT1)的触发方式控制位。其意义和IT0相同。

IE1为外部中断1的中断请求标志位。其意义和IE0相同。

TF0为定时器/计数器T0的溢出中断请求标志位。当T0开始计数后,从初值开始加1计数,在计满产生溢出时,由硬件使置位TF0,向CPU请求中断,CPU响应中断时,硬件自动将TF0清零。如果采用软件查询方式,则需要由软件将TF0清零。因此,系统是通过检查TF0的状态来确定T0是否有中断请求。TF0=1表示T0有中断请求,TF0=0时则没有。

TF1为定时器/计数器T1的溢出中断请求标志位,其作用同TF0。

TR0和TR1分别是T0和T1的控制位,与中断无关。此将在定时器/计数器应用内容中介绍。

例如,如图3-12所示,两个外设的中断请求为下降沿触发有效。

根据题意,则可以用两条置位指令IT0=1;IT1=1。如果使用字节操作方式,则一条语句即能实现,即TCON=0x05。

(3)中断优先级控制寄存器IP, 字节地址是B8H

单片机的中断系统有两个中断优先级。对于每一个中断请求源都可编程为高优先级中断或低优先级中断,实现两级中断嵌套。中断优先级是由片内的中断优先级寄存器IP控制的。IP的字节地址是B8H,既支持字节操作,又支持位操作。位地址的范围是B8H~BFH。8位中有5位与中断有关,剩下的3位没有定义。其格式如下:

PS为串行口的中断优先级控制位。PS=1时,串行口被定义为高优先级中断源;PS=0时,串行口被定义为低优先级中断源。

PT1为定时器/计数器T1的中断优先级控制位。PT1=1,T1被定义为高优先级中断源;PT1=0,T1被定义为低优先级中断源。

PX1为外部中断1(INT1)的优先级控制位。PX1=1,外部中断1被定义为高优先级中断源;PX1=0,外部中断1被定义为低优先级中断源。

PT0为定时器/计数器T0的中断优先级控制位。其功能同PT1。

PX0为外部中断0(INT0)的优先级控制位。其功能同PX1。

中断优先级控制寄存器IP的各位都由用户置位或复位,可用位操作指令或字节操作指令更新IP的内容,以改变各中断源的中断优先级,单片机复位后IP全为0,各个中断源均为低优先级中断。

例如:如图3-11所示,两个外设分别接在两个外部中断上,还有1个定时器T0中断源,请设定3个中断源优先级顺序为:

T0>外部中断1 >外部中断0

根据题意,三个中断源,两个优先级顺序,在同一个优先级中,对5个中断源的优先次序按照其默认优先级顺序排列,如表3-2所示。从而得知,本来T0>外部中断1优先级,所以把这两个中断请求设为同一个级别,即高级,外部中断0默认为低级。字节操作语句为IP=0x06。另外也可位操作,PT0=1;PX1=1。

(4)SCON串口控制寄存器,字节地址为98H

SCON为串行通信时用到的串口寄存器,与外部中断无关,将在串行通信内容中介绍。

6.中断过程

单片机的完整的中断过程可包括中断请求、中断响应、中断处理和中断返回4个阶段。下面简单介绍单片机的中断过程。

(1)中断请求

单片机有5个中断源,两个是外部中断源,另外3个是内部固定的中断源。外部中断源是通过P3.2和P3.3引脚送入中断请求,有效信号可以是低电平或下降沿信号,从而置位IE0、IE1。定时器中断请求是定时/计数发生溢出时,向单片机发出中断请求,从而置位IF0、IF1。串行口中断请求是一次串行发送或接收数据结束向单片机发出的中断请求,从而置位TI、RI。

(2)中断响应

① 中断的响应条件。在每个机器周期的S5P2时刻,单片机依次采样每一个中断标志位,而在下一个机器周期对采样到的中断进行查询。如果在前一个机器周期的S5P2有中断标志,则在查询周期内便会查询到并按优先级高低进行中断处理,中断系统将控制程序转入相应的中断服务程序。

CPU响应中断应具备的条件是:

● 首先有中断源发出中断请求。

● 然后CPU中断允许位EA为“1”,即CPU开中断。

● 申请中断的中断源,其相应的中断允许位为“1”,即允许相应的中断源中断。

以上条件满足时,一般CPU会响应中断请求。但若存在以下几种情况,CPU的中断响应会被屏蔽,使本次的中断请求得不到响应:CPU正在处理同级的或更高优先级的中断,或者现行的机器周期不是所执行指令的最后一个机器周期,即正在执行的指令没有执行完以前,CPU不响应任何中断;或者当前正在执行的指令是返回指令(RETI)或是对IE或IP寄存器进行读/写的指令。CPU在执行完这些指令后,至少还要再执行一条其他指令才会响应中断。

CPU响应中断时,会根据中断源的类别,在硬件的控制下,程序转向相应的中断服务程序入口单元,执行中断服务程序。

② 中断的响应过程。51单片机的中断系统中分为两个中断优先级。每一中断请求源均可通过对IP寄存器的编程为高优先级中断或低优先级中断,并可实现多级中断嵌套。一个正在执行的低优先级中断服务程序能被高优先级的中断请求所中断,但不能被另一个同级或低级的中断源所中断。因此,如果CPU正在执行高优先级的中断服务程序,则不能被任何中断源所中断,必须等到当前的中断服务程序执行结束,遇到返回指令(RETI)返回主程序后,至少再执行一条指令才能响应新的中断请求。为了实现上述功能,51单片机的中断系统中有两个不可寻址的优先级状态触发器。一个触发器指出某高优先级的中断正在得到服务,所有后来的中断请求被阻断;另一个触发器指出某低优先级的中断正在得到服务,所有同级的中断请求都被阻断,但不能阻断高优先级的中断请求。

如果8051单片机满足中断响应的条件,并且不存在中断被屏蔽的情况,CPU就响应相应的中断请求。在实际的响应过程中,CPU首先置位被响应中断的优先级状态触发器,以屏蔽(即关闭)同级和低级的中断请求。然后,根据中断源的类别,在硬件的控制下,内部自动执行一条子程序调用指令,将程序转移至相应的中断入口处,开始执行中断服务程序。在转入中断服务程序时,子程序调用指令自动把断点地址(即程序计数器PC的当前值)压入堆栈,但不会自动保存状态寄存器PSW等寄存器中的内容。

当中断的各项条件满足要求时,CPU响应中断,停止现行程序,转向中断服务程序。整个响应过程中CPU应完成的工作有以下几个。

● 关中断。CPU响应中断时便向外设发出中断响应信号,同时自动地关中断,处理一个中断过程中不致又接收另一新的中断,以防止误响应。

● 保护断点。为了保证CPU在执行完中断服务程序后,准确地返回断点,CPU将断点处的PC值推入堆栈保护。待中断服务程序执行完后,由返回指令RETI将其从堆栈中弹回PC,从而实现程序的返回。

● 执行中断服务程序。找出中断服务程序入口地址,转入执行中断服务程序。

因系统保留的各中断入口地址间空间太小,所以,通常在中断入口地址处安排一条相应的跳转指令,跳转至用户设计的中断服务程序入口。

(3)中断处理

CPU响应中断请求后,即转到中断服务程序的入口,执行中断服务程序。从中断服务程序的第一条指令开始到中断返回指令为止,这个过程称为中断处理或中断服务。不同的中断源所需服务的要求及内容各不相同,其处理过程也就有所区别,但在一般情况下,在中断服务程序中一般应完成如下任务。

① 保护现场。由于CPU响应中断是随机的,而CPU中各寄存器的内容和状态标志会因转至中断服务程序而受到破坏,所以要在中断服务程序的开始,把断点处有关的各个寄存器的内容和状态标志,用堆栈操作指令PUSH推入堆栈保护。

② 中断服务。中断源申请中断时应完成的任务。

③ 恢复现场。在中断服务程序完成后,把保护在堆栈中的各寄存器内容和状态标志,用POP指令弹回CPU。

④ 开中断。上面已谈到CPU在响应中断时自动关中断。为了使CPU能响应新的中断请求,在中断服务程序末尾应安排开中断指令。

⑤ 返回主程序。当中断服务程序执行完毕返回主程序时,必须将断点地址弹回PC,因此在中断服务程序的最后用一条RETI指令,使PC返回断点。

(4)中断返回

中断服务程序的最后一条指令是中断返回指令RETI。它的功能是将断点地址从堆栈中弹出,送回程序计数器PC中,使程序能返回到原来被中断的地方继续执行。

单片机的RETI指令除了弹出断点之外,还通知中断系统已完成中断处理,并将优先级状态触发器清除(复位),使系统能响应新的中断请求。

(5)中断请求的撤销

CPU完成中断请求的处理以后,在中断返回之前,应将该中断请求撤销,否则会引起第二次响应中断。在51单片机中,各个中断源撤销中断请求的方法各不相同。

● 定时/计数器的溢出中断:CPU响应其中断请求后,由硬件自动清除相应的中断请求标志位,使中断请求自动撤销,因此不用采取其他措施。

● 外部中断请求:中断请求的撤销与触发方式控制位的设置有关。采用边沿触发的外部中断,CPU在响应中断后,由硬件自动清除相应的标志位,使中断请求自动撤销;采用电平触发的外部中断源,应采用电路和程序相结合的方式,撤销外部中断源的中断请求信号。

● 串行口的中断请求:由于RI和TI都会引起串口的中断,CPU响应后,无法自动区分RI和TI引起的中断,硬件不能清除标志位,需采用软件方法在中断服务程序中清除相应的标志位,以撤销中断请求。

7.中断程序编写

(1)中断初始化

在用到外部中断之前,要先用指令来设置相关寄存器的初始值,设定外部中断的初始条件,即外部中断的初始化,包括:

① 开放CPU中断和有关中断源的中断允许,设置中断允许寄存器IE中相应的位。

② 根据需要确定各中断源的优先级别,设置中断优先级寄存器IP中相应的位。

③ 根据需要确定外部中断的触发方式,设置定时控制寄存器TCON中相应的位。

(2)程序结构

根据中断的定义,整个程序应包括两个程序:主程序、中断服务程序。

① 主程序。主程序是指单片机在响应外部中断之前和之后所做的事情。它的结构:

            void main()
            {
              …
             }

② 中断服务程序。中断服务程序是外部设备要求单片机响应中断所做的事情。当中断发生并被接受后,单片机就跳到相对应的中断服务子程序即中断服务函数执行,以处理中断请求。中断服务子程序有一定的编写格式,以下是C51语言的中断服务子程序的格式。

            void中断服务程序的名称(void) interrupt中断编号[using寄存器组号码]
            {
              中断服务子程序的主体
            }

对于51而言,其中断编号可以是从0到4的数字,表3-3给出了5个中断源的编号。为了方便起见,在包含文件reg51.h中定义了这些常量,如下所示:

表3-3 单片机中断源编号

            #define IE0_VECTOR 0       /*Ox03 External Interrupt 0*/
            #define TF0_VECTOR 1       /*0x0B Timer 0*/
            #define IE1_VECTOR 2       /*Ox03 External Interrupt 1*/
            #define TF1_VECTOR 3       /*0x1B Timer 1*/
            #define SIO_VECTOR 4       /*0x23 Serial port*/

因此用户只要使用以上所定义的常量即可。using寄存器组号码是指使用的第几组工作寄存器,常可省略,默认第0组工作寄存器。

中断函数名同普通函数名一样,只要符合标识符的书写规则就行。那么如何区分中断函数和普通函数呢?主要是通过关键字“interrupt”及中断号来区分,不同的单片机中断源对应不同的中断号。中断函数不能有形参和返回值,也不能被其他函数调用。中断函数可以调用其他函数,使用时要十分小心,尽可能不在中断函数里调用其他函数。中断函数应尽量简短,以保证主函数的执行流畅。

(3)举例

例4:如图3-13所示电路,单片机P1口接有8个发光二极管,P3.2接有1个开关,请设计程序实现:平时,8个灯循环点亮;当开关按下时,8个灯全亮然后全灭,如此循环8次后,然后返回平时状态。

图3-13 中断流水灯电路

此例电路比前面讲过的流水灯电路多了一个按键开关K1,功能比流水灯多了一个按键按下时对应灯的功能。平时,CPU执行流水灯程序;当按键按下时,CPU执行另一程序,执行完后,返回平时状态。这很明显是一个外部中断,外部中断源为按键,此按键正好接在P3.2,可以产生外部中断0请求。

把外部中断请求信号设为下降沿有效,如果单击一下按键,给P3.2送进一个下降沿,向CPU发出一个外部中断0中断请求。

平时,8个灯循环点亮,应该放在主程序;用到中断,需要中断初始化,设置外部中断的初始条件,设置寄存器的值,放在主程序;当点触按键,有中断请求时,8个灯全亮然后全灭,如此循环8次后,返回平时状态,应该放在中断服务程序。所以,得到如下程序格式:

            void main()
             {
              中断初始化;
              8个灯循环点亮;
             }
            void名字()interrupt中断号
             {
              8个灯全亮然后全灭,如此循环8次;
            }

程序清单如下:

        /*****************************************************************************/
        #include <REG51.h>
        unsigned char code sz1[ ]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};
        void delay(unsigned int a)
        {   unsigned char i;
            while(--a!= 0)
              {
              for(i=0;i<125;i++);
            }
  }
  void main()
  {
  unsigned char i,m;
  EA=1;
  EX0=1;
  IT0=1;
  while (1)
  {
        for(m=0;m<8;m++)
         {
            P1=sz1[m];
            delay(1000);
         }
      }
      }
      void lsd() interrupt 0
      {
      unsigned char j;
      for(j=0;j<8;j++)
       {
         P1=0x00;
         delay(1000);
         P1=0xff;
         delay(1000);
        }
      }
      /*****************************************************************************/

程序说明:

● 此处流水灯程序的编写采用的是数组的方法,可以通过改变数组的元素实现花样流水灯。

● 此处中断初始化中应注意大写,把外部中断请求信号设为边沿触发。中断响应后,边沿触发信号会自动撤销,使用比电平触发方便。

● 编写含有中断的程序,一定要清楚程序结构,分清主程序做什么,中断服务程序做什么。另外要弄清楚整个工作流程,这对深刻理解中断非常重要。

8.外部中断的扩展

当有多个外部中断源时,采用中断加查询相结合的方法响应中断。扩展电路原理如图3-14所示。

图3-14 多个外部中断源

中断加查询是指利用中断配合查询的方法响应中断请求,以图3-14为例加以说明。

中断:4个外部中断源(有中断请求,输出高电平)通过或非门电路后产生0与P3.2(P3.3)相连,向CPU发出中断请求。

查询:每一个外部中断源和1个并行I/O口相连,通过逐个查询的方式,来识别哪根线上有中断请求。

在多个外部中断源中若有一个或几个为高电平则通过或非门输出为0,则P3.2(P3.3)为低电平,向CPU发出中断请求;CPU在执行中断服务程序时,先依次查询P1口的中断源输入状态,然后转入到相应的中断服务程序。

3.3 项目实现

3.3.1 设计思路

本项目要求设计手动的计数器,可以通过加入一个按键借助外部中断实现加1计数;要求用两位数码管实时显示计数结果,考虑到实用性,选择数码管动态显示方式来实时显示0~99。

3.3.2 硬件电路设计

根据题意,设计电路图如图3-15所示。按键K1接在P3.2,单片机的P2口接两位动态数码管的段值端,单片机的P3.6、P3.7经过三极管驱动接数码管的位选端,靠按键K1实现手动。

图3-15 手动计数器电路图

3.3.3 软件编程

手动计数器整个工作流程可概括为:数码管平时在显示当前的计数值,当按键按下,向单片机发出中断请求,请求单片机把当前的计数值加1,然后再返回平时状态,继续显示。所以在设计程序时,主程序所做的工作是数码管动态显示当前计数值以及中断初始化,中断服务程序所做的事情是把计数值加1。

程序清单如下:

            /*****************************************************************************/
            #include <REG51.h>
            unsigned char code sz1[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
            sbit seg1=P3^6;
            sbit seg2=P3^7;
            unsigned char m=0;              //定义m为公共变量
            void delay(unsigned int a)
            {
              unsigned char b;
              while(--a!= 0)
              {
                for(i=0;b<125;b++);
                }
            }
            void disp(unsigned char t)
            {
              unsigned char i,j;
              i=t/10;
              j=t%10;
              P2=sz1[i];
              seg1=0;
              delay(20);
              P3=0xff;
              P2=sz1[j];
              seg2=0;
              delay(20);
              P3=0xff;
            }
            void main()
            {
              P3=0xff;
              EA=1;
              EX0=1;
                  IT0=1;
                  while(1)
                  {
                  disp(m);
                  }
                }
                void lsd() interrupt 0
                {
              if(m<99)
              m++;
              else
              m=0;
            }
            /*****************************************************************************/

程序说明:

● 程序中m用来存放当前计数值,定义为全局变量,因为在中断程序、主程序都使用到m。

变量分为局部变量和全局变量。局部变量是指在函数体内定义的变量,局部变量的定义必须放在函数体的最前面。局部变量只能在定义它们的函数内部使用,在该函数外部不能使用。

全局变量是指在所有函数体之外定义的变量。只有从全局变量定义位置之后书写的函数才能使用这些全局变量。如果把全局变量写在程序的最前面,则所有函数就都能使用它们。

全局变量可以在多处被使用和修改,必须在程序的全局范围内考虑其值的变化,控制难度很高,容易出错。全局变量的作用相当巨大,尤其是在处理中断函数时。中断函数不允许有形参和返回值,所以只有通过全局变量才能建立主函数同中断服务函数的联系。

尽量避免定义同名的全局变量和局部变量。

● 为了使程序模块化,把动态显示程序编写为子程序disp(unsigned char t),主程序可以随时调用。t为形参,主程序调用时disp(m),m为实参。

● 要弄懂整个程序的流程,主程序中设置了无限循环while(1)语句,平时CPU一直执行循环体的内容即动态显示当前计数值,其实它是一边显示,一边等外部中断来。当按键按下,外部中断来了,CPU停下主程序,转而去执行中断服务程序lsd(),把当前计数值加1,然后再返回主程序的循环体,继续显示计数值,此时显示的就是加1后的下一个计数值。

3.3.4 仿真调试

在计算机上运行Keil,首先新建一个项目,项目使用的单片机为AT89C51,这个项目暂且命名为jsq;然后新建一个文件,并保存为“jsq.c”文件,并添加到工程项目中。直接在Keil软件界面中编写程序,也可以先把程序清单形成一个TXT文件,然后剪切到Keil的程序编辑界面中。当程序设计完成后,通过Keil编译并创建jsq.HEX目标文件。在Keil的应用过程中,由于编译过程中产生很多文件,因此新建一个项目需在一个目录中建立。

在安装过Proteus软件的PC上运行ISIS文件,即可进入Proteus电路原理仿真界面,利用该软件仿真时操作比较简单,其过程是首先构造电路,然后双击单片机加载HEX文件,最后执行仿真。Proteus界面以及本案例的仿真电路如图3-16所示。仿真过程中,单片机加载程序模拟运行实际状态。电路中单片机采用AT89C51,单片机默认为最小系统,也可以不需要再外接晶体振荡电路和复位电路。

图3-16 手动加1计数器仿真图

刚一进入仿真状态,数码管显示“00”,当第一次按下按键K1,数码管显示“01”;再次按下K1,数码管实时显示按下按键的次数,如图3-16所示。

【项目总结】

1.数码管从内部结构上分为共阴极和共阳极两种,数码管内部没有限流电阻,在使用时需外接限流电阻。要使数码管显示某个数值,需注意两种结构的字形码(即段值)不同。

2.数码管从显示方式上分为静态显示和动态显示。静态显示则显示稳定、电路简单,但占据线太多,适用于一个或两个数码管的显示;动态显示则逐个点亮、轮番扫描、占据线少,但编程复杂,适用于两个以上数码管的显示。

3.单片机外部中断关键是理解中断的整个过程。另外就是掌握程序结构:主程序、中断服务程序。主程序包括中断初始化、CPU平时做的事情;中断服务程序是外设要求CPU响应中断后做的事情。

思考与练习

1.在用共阳极数码管显示电路中,若把共阳极数码管改为共阴极数码管,能否正常显示?为什么?电路和程序应做何修改?

2.中断处理过程包括哪4个步骤?简述中断处理过程。

3.简要说明LED数码管静态显示和动态显示的特点,实际设计时应如何选择?

4.AT89C51单片机外中断采用电平触发方式时,如何防止CPU重复响应外中断?

5.已知有5台外围设备,分别为EX1~EX5,均需要中断。现要求EX1~EX3合用INT0,余下的合用INT1,且用P1.0~P1.4查询,试画出连接电路,并编制程序,当5台外设请求中断(中断信号为低电平)时,分别执行相应的中断服务子程序SEVER1~SEVER5。

6.如何设计0~999的手动计数器?