第5章 从做成一个声控数码管电子钟来进一步熟悉中断 5.1硬件设计及连接步骤 5.1.1硬件设计 1. 设计思路 我们要制作的电子钟用到的元器件和接法与第4章的基本一样,只是增加了一个声控电路,加此电路的目的是节约能源。 2. 原理图 具体硬件接法见图5.1。 图5.1原理图 声控电路的功能是这样的,当有一个较大的声音对着拾音器发声时,声控电路就输出一个信号给单片机,单片机通过程序让数码管显示当前时间; 当显示一小会儿例如20s后,单片机通过程序让数码管停止显示。下面先给出声控模块图,见图5.2。 图5.2声音传感器模块 3. 元器件清单 声控电路用到的元器件见表5.1。 表5.1所需元器件 序号 元器件名称 型号或容量 数量/个 1 单片机 STC89C52RC DIP40 1 2 晶振 12MHz 1 3 电容 30pF 2 4 0.36in 6位一体16脚带时钟数码管共阴 AH3661 1 5 1kΩ 9脚排阻 A102 1 6 按钮 12×12×4.3,按键,轻触开关 4 7 蜂鸣器 5V无源蜂鸣器 1 8 PNP三极管 9015 1 9 声音传感器模块 1 10 发光二极管 任意演示 1 11 电阻 1kΩ 1 声控数码管 电子钟硬件 连接及运行 视频 5.1.2硬件连接步骤 扫描右侧二维码在手机或平板计算机端一边观看硬件连接视频,一边动手进行硬件连接,硬件连接完成后一定要用万用表检测一下硬件连接得是否可靠,如果不可靠,一定要重新连接直至可靠无误。至此整个硬件电路的安装工作结束。接下来要动手做的就是编写程序了。 5.2程序设计及下载 本系统的程序在定时器的程序基础上做一些增加和改动即可。增加的是声控电路的程序,改动的是把定时功能改成电子钟功能,先给出源程序。 5.2.1源程序 源程序如下: #include<reg52.h> #include<math.h> //因为要计算绝对值,所以要有数学头文件 typedef unsigned char u8; typedef unsigned int u16; u8 d[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; u8 w[]={0x1f,0x2f,0x37,0x3b,0x3d,0x3e}; u8 ms=0,s=0,m=0,h=0,sflash=0,mflash=0,hflash=0,Turn=0,Ifdisplay=1, DisTime,lighting=0; char s1=0,m1=0,h1=0; u16 Tvalue=0; sbit beep=P2^0; //通过P2.0来控制蜂鸣器是否发声 sbit keyadd=P2^2; //通过P2.2来接收加1按钮的输入 sbit keysub=P2^1; //通过P2.1来接收减1按钮的输入 sbit Dot=P2^3; //通过P2.3来控制发光二极管是否亮 sbit keyset=P3^2; //通过P3.2来接收"开始定时"和"暂停定时"按钮的输入 sbit Sound=P2^5; sbit led=P3^3; /********************************************************************** 函数名称:delay(u16 t) 函数功能:产生时间延时 入口参数:无 出口参数:无 备 注: **********************************************************************/ void delay(u16 t) { u8 i; for(;t>0;t--) for(i=19;i>0;i--); } /********************************************************************** 函数名称:beepon() 函数功能:蜂鸣器发声 入口参数:无 出口参数:无 备 注: **********************************************************************/ void beepon() { beep=0; //让蜂鸣器响 delay(500); //延时一小会儿 beep=1; //让蜂鸣器不响 } /********************************************************************** 函数名称:display() 函数功能:显示定时时间 入口参数:无 出口参数:无 备 注: **********************************************************************/ void display(void) { P1=d[h/10]; //通过P1口给数码管输出小时的十位数段码 P0=w[0]; //通过P0口让从左数第一个数码管亮,即输出位选信号 delay(30); P0=0xff; P1=d[h%10]; //通过P1口给数码管输出小时的个位数段码 P0=w[1]; //通过P0口让从左数第二个数码管亮 delay(30); P0=0xff; P1=d[m/10]; //通过P1口给数码管输出分的十位数段码 P0=w[2]; //通过P0口让从左数第四个数码管亮 delay(30); P0=0xff; P1=d[m%10]; //通过P1口给数码管输出分的个位数段码 P0=w[3]; //通过P0口让从左数第五个数码管亮 delay(30); P0=0xff; P1=d[s/10]; //通过P1口给数码管输出秒的十位数段码 P0=w[4]; //通过P0口让从左数第七个数码管亮 delay(30); P0=0xff; P1=d[s%10]; //通过P1口给数码管输出秒的个位数段码 P0=w[5]; //通过P0口让从左数第八个数码管亮 delay(30); P0=0xff; } /********************************************************************** 函数名称:dispalyT() 函数功能:显示预置定时时间 入口参数:无 出口参数:无 备 注: **********************************************************************/ void displayT(void) {Dot=0; switch(Turn) { case 1:{sflash=0;mflash=0;hflash=50;break;} case 2:{sflash=0;mflash=50;hflash=0;break;} case 3:{sflash=50;mflash=0;hflash=0;break;} } if(Turn==1) { P1=d[h1/10]; //通过P1口给数码管输出预置小时的十位数段码 P0=w[0]; //通过P0口让从左数第一个数码管亮,即输出位选信号 delay(hflash); //延时时间是一个变量,当显示小时时,hflash=30 P0=0xff; //当不预置小时时,让hflash=0 P1=d[h1%10]; //通过P1口给数码管输出预置小时的个位数段码 P0=w[1]; //通过P0口让从左数第二个数码管亮 delay(hflash); P0=0xff; } if(Turn==2) { P1=d[m1/10]; P0=w[2]; delay(mflash); P0=0xff; P1=d[m1%10]; P0=w[3]; delay(mflash); P0=0xff; } if(Turn==3) { P1=d[s1/10]; P0=w[4]; delay(sflash); P0=0xff; P1=d[s1%10]; P0=w[5]; delay(sflash); } P0=0xff; } /********************************************************************** 函数名称:keyscan() 函数功能:判断"加1""减1"键是否被按下,如果被按下则进行相应的处理 入口参数:无 出口参数:无 备 注: **********************************************************************/ void keyscan() { if(keyadd==0) //是要对"时""分""秒"加1吗? { delay(120); //延时消抖 if(keyadd==0) //再确认一下"加1"键是否被按下 { switch(Turn) //是预置秒还是预置分或者是时 { case 1:{h1++;if(h1>23) h1=0;break;} //对时加1 case 2:{m1++;if(m1>59) m1=0;break;} //对分加1 case 3:{s1++;if(s1>59) s1=0;break;} //对秒加1 } beepon(); //当按"加1"键时让蜂鸣器响一声 } }while(!keyadd); //按键松开去抖 if(keysub==0) //是要对时、分、秒减1吗? { delay(120); //延时消抖 if(keysub==0) //再确认一下"减1"键是否被按下 { switch(Turn) { case 1:{h1--;if(h1<0) h1=23;break;} //对时减1 case 2:{m1--;if(m1<0) m1=59;break;} //对分减1 case 3:{s1--;if(s1<0) s1=59;break;} //对秒减1 } beepon(); //当按"减1"键时让蜂鸣器响一声 } }while(!keysub); displayT(); //显示设置时间 } /********************************************************************** 函数名称:T0_init() 函数功能:定时器0中断初始化 入口参数:无 出口参数:无 备 注: **********************************************************************/ void T0_init() { TMOD |=0x01; //让TMOD寄存器的M0置1 TMOD &=0xf1; //让TMOD寄存器的8位成为:xxxx 0001(x表示0或者1) ET0=1; //闭合定时器0中断分开关 EA=1; //闭合单片机中断的总开关 } /********************************************************************** 函数名称:T0_int() interrupt 1 函数功能:产生时间并判断定时时间是否到 入口参数:无 出口参数:无 备 注: 定时器0的中断号是1 **********************************************************************/ void T0_int() interrupt 1 { TH0=(65536-50000)/256; TL0=(65536-50000)%256; //给定时器0的TH和TL寄存器预装15536个脉冲 ms++; //让变量ms来记录单片机每次发生的定时器0中断的次数 if(ms%10==0) { Dot=!Dot ; if(lighting==1)led=!led; } if(ms==20) //若发生了20次定时器0中断,则说明经历了1s { ms=0; //清除中断次数的记录 s++; if(s>59) //当秒加到60次时 { s=0; //让秒变量归0 m++; //让分加1 if(m>59) //当分加到60次时 { m=0; //让分变量归0 h++; //让小时加1 if(h>23) h=0; //当小时加到24时,让小时变量归0 } } //if(h==h1&&m==m1&&s==s1) {TR0=0;beep=0;LED=0;} }//当定时时间到的那一刻,停止计时,让蜂鸣器长响,让LED亮 } /********************************************************************** 函数名称:EX0_init() 函数功能:单片机外部中断0中断初始化 入口参数:无 出口参数:无 备 注: **********************************************************************/ void EX0_init() { IT0=1; //设置外部中断0为下降沿触发中断方式 EX0=1; //闭合外部中断0的中断分开关 EA=1; //闭合单片机的中断总开关 } /********************************************************************** 函数名称:EX0_int() interrupt 0 函数功能:如果"设置"键被按下则进行相应的处理 入口参数:无 出口参数:无 备 注: 外部中断0的中断号是0 **********************************************************************/ void EX0_int() interrupt 0 { u8 i; if(keyset==0) //"开始定时"/"设置"键被按下? { for(i=0;i<150;i++); //延时消抖(为什么不用延时函数请读者思考) if(keyset==0) // "开始定时"/"设置"键确实被按下 { EX0=0; //打开外部中断0中断分开关,暂时不允许中断 beepon(); //让蜂鸣器响一声 Turn++; if(Turn==1) { TR0=0; //停止定时器0定时 s1=0; m1=0; h1=0; } if(Turn>3) //"开始定时"/"设置"按钮被按了4下 { Turn=0; //将Turn的值变回到0 s=s1; m=m1; h=h1; //将需要走时的变量h、m、s赋值成对表时的时、分、秒 Tvalue=h1*3600+m1*60+s1; //计算Tvalue的值,看看是否设置 if(Tvalue!=0) TR0=1;//如果设置了对表时间就开始走表 } } }while(!keyset); } /********************************************************************** 函数名称:main() 函数功能:初始化设置,循环往复地检验是否有按键被按下并调用显示函数显示 入口参数:无 出口参数:无 备 注: **********************************************************************/ main() { P1=0xff; //让P1口输出8个1 EX0_init(); //调用外部中断0初始化函数 T0_init(); //调用定时器0初始化函数 Turn=0; //清0标志变量 TR0=1; while(1) { EX0=1; //开启外部中断0 if(Turn) //如果Turn的值不为0,则说明正在设置定时时间 { sflash=30; mflash=30; hflash=30; keyscan(); } if(Sound==1&&Ifdisplay) //如果检测到声音,声控模块的信号端会短暂输出1 { lighting=1; Ifdisplay=0; DisTime=s; } if(lighting==1&&Turn==0) display(); if(abs(s-DisTime)==20) { Ifdisplay=1; lighting=0; led=1; } } } 5.2.2数码管声控电子钟的操作 将以上程序下载到单片机中后,首先按下最左边的“对表”按钮,此时等待对小时,再按一下“对表”按钮,可以对分,再按一下“对表”按钮可以对秒,不管对秒、对分或者对小时,都可以通过按“加1”键和“减1”键来实现,当对好表后,再按一下“对表”按钮,就可以走表了。 声控程序是通过一个特殊位变量Sound与标志变量Ifsound的1和0来控制数码管的显示与不显示的,在定义此变量时让其等于0,及u8 Ifdisplay=0,平时没有声音时声控电路输出高电平,当声控模块接收到一个较大的声音时,声控电路输出低电平,由于声控电路接到了P1口的P1.5,因此此时P1.5得到一个数字0,在main()函数中时时刻刻都检查特殊位变量Sound的值,一旦Sound变为0,就让Ifdisplay变为1,在display()函数中设置一条If语句,即If(Ifdisplay)。 完成了这个数码管电子钟的制作后我们再来深入了解一下单片机的中断。 5.3深入了解单片机的中断 5.3.1中断的有关概念 中断是计算机中非常重要的一个工作机制,有了中断,计算机才能高效率地工作。可以打个比方: 设想一个有六个孩子的母亲,如果她一会儿问这个: “你饿不饿?”一会儿问那个: “你渴不渴?”问完这个又问另一个: “要不要上厕所?”问完又问下一个: “你有没有不舒服?”那这位母亲一天啥都不用干了。假如这位母亲告诉过她的这六个孩子“妈妈在给你们做衣服,你们有什么事就喊妈妈。”那她就在把她的孩子们安顿好后就可以不受打扰地用缝纫机做衣服了。假设当她做衣服做了一半时她的一个孩子说: “妈妈,我渴了,我要喝水。”她就得停下了正在做的衣服给她的这个孩子倒水喝。这对于她正在做的衣服这件事来说就是发生了“中断”。当她给那个孩子喝了水后,她就可以继续被“中断”了的做衣服工作。假设又过了一会儿她的另一个孩子说: “妈妈,我饿了。”她就得又一次停下做衣服去给这个孩子拿吃的,假设正当她打开冰箱取面包时她的另一个孩子说: “妈妈,我要大便。”这位母亲就得停下来取面包,关住冰箱门去领着这个孩子去厕所大便。这种情况就是发生了优先级更高的“中断”,也就是高优先级的中断可以中断低优先级的中断服务。当她伺候她的那个孩子大便完后就可以继续打开冰箱门给那个饿了的孩子取面包,当把面包给了那个饿了的孩子后她又继续被“中断”了的做衣服工作,这种情况叫“中断嵌套”。 增强型MCS51单片机的CPU就像那位母亲,6个中断源类似那位母亲的六个孩子,这6个中断源分别是外部中断0、定时器/计数器0、外部中断1、定时器/计数器1、串行中断和定时器2中断。它们的优先级就是按此顺序排列的,外部中断0的优先级最高,定时器/计数器2中断的优先级最低。但是它们的优先级是可以通过编程改变的。如何改变?下面会介绍。 5.3.2中断响应全过程 要让CPU在中断发生后做特定的事情,必须编写中断服务程序。但是我们需要搞清楚CPU是怎么知道中断服务程序的存放地的。中断服务程序根据中断发生后要处理的事务不同而不同,有的中断服务程序可能编写得很短,也许只有几条程序; 有的中断服务程序可能很长,也许会达到几十条、几百条或者更多。我们都知道,在增强型MCS51单片机的ROM中保留了48B的存储区域专门用来存放各个中断服务程序的入口地址,每个中断分配8B。假如把中断服务程序全部放到这8B里肯定放不下,但是,若把一个中断服务程序的首地址放到一个8B的存储空间就完全放得下,而中断服务程序在下载到ROM中时其所存放的区域首地址会自动被存放到对应的入口地址存放地中。表5.2列出了增强型51单片机6个中断源的中断服务程序入口地址,例如,外部中断0的中断号是0,其中断服务程序的入口地址被存放在0003H到000A的8B中,定时器/计数器0的中断号是1,其中断服务程序的首地址仿真从000BH到0012H共8B中。 表5.2中断源及其对应的中断号和中断服务程序入口地址 中断源 中断标志 中断服务程序入口 中断号 优先级顺序 外部中断0(INT0) IE0 0003H 0 高 定时器/计数器0(T0) TF0 000BH 1 外部中断1(INT1) IE1 0013H 2 定时器/计数器1(T1) TF1 001BH 3 串行口 RI或TI 0023H 4 定时器/计数器2(T2) TF2 002BH 5 低 每个中断源的中断服务程序的入口地址和中断号有一个计算公式,计算公式为 中断服务程序的入口地址=中断号×8+3 图5.3响应中断过程 当表5.2中的某个中断源发生中断时,CPU响应中断过程如图5.3所示。我们都知道,CPU中有一个寄存器叫PC,它里面存放着下一条要执行的指令地址,CPU在响应中断时首先要将PC中的内容即下一条程序的地址压入堆栈保护起来,这个操作叫“保护断点”; 接下来要将此时的一些重要寄存器的内容例如累加器A和B寄存器的内容压入堆栈,这个操作叫“保护现场”。为什么要保护现场呢?因为在发生中断之前在执行主程序的过程中有一些重要的数据被存放在累加器A等寄存器中,如果不保护起来,当CPU转到执行中断服务程序时,也会用到这些重要的寄存器,执行中断服务程序时会把这些重要的寄存器原先的内容覆盖,等到中断服务程序执行完再继续执行被中断了的主程序时就会发生数据错误。 保护完现场后,CPU将中断服务函数名后面的中断号乘以8再加上3,就找到当前的中断服务程序的首地址并将其装到PC中,于是就开始执行中断服务程序。 当中断服务程序执行完后,系统会自动将原先压入堆栈的重要寄存器的值弹回对应的寄存器中,这个操作叫“恢复现场”,然后再将原先压入堆栈的断点地址弹回PC,这个操作叫“恢复断点”,于是CPU就可以接着执行被中断了的主程序了。 5.3.3中断优先级的改变 前面提到增强型MCS51单片机有6个中断源,假如某个时刻有2个或2个以上中断源同时提出中断申请,那CPU该先响应哪个中断后响应哪个中断呢?我们知道,MCS51单片机的6个中断源是有优先级的,默认的中断优先级如表5.2所示,但允许通过编写程序来改变中断的优先级别。如何改变呢?见图5.4。 图5.4增强型MCS51单片机中断系统结构 单片机内部有一个中断优先级控制寄存器,名称叫IP,见图5.5。其最高位和次高位没有意义,从D5位到D0位分别是定时器/计数器2的优先级控制位PT2、串行中断的优先级控制位PS、定时器/计数器1的中断优先级控制位PT1、外部中断1的中断优先级控制位PX1、定时器/计数器0的优先级控制位PT0、外部中断0的优先级控制位PX0。想让那个中断的优先级变高就把其对应的优先级控制位置1,例如想让外部中断1的优先级变为最高,就编写一条PX1=1的指令即可; 如果想让某个中断源的优先级变为最低,在程序中将其对应的优先级控制位清0即可。 图5.5中断优先级控制寄存器IP的6个位 知识点总结 本章的知识点是中断,要搞清楚单片机的CPU响应中断之前要做一些工作,例如保护断点、保护现场,响应中断其实就是执行我们编好的中断服务程序,响应完中断还要做一些工作,例如恢复现场、恢复断点。另外,虽然单片机有其默认中断优先级顺序,但可以通过编程改变此顺序。 扩展电路及创新提示 读者可以在本系统的基础上加上闹钟的功能。