找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 29757|回复: 43
收起左侧

第六章 中断的学习

  [复制链接]
ID:1 发表于 2013-7-11 19:25 | 显示全部楼层 |阅读模式
   中断是单片机系统的重点中的重点,因为有了中断,单片机就具备了快速协调多模块工作的能力,大家对本章节内容要多研究,最终要完全理解并且掌握。
6.1 C语言的数组6.1.1 数组的基本概念
我们第四章学过变量的基本类型,比如charint等等。这种类型描述的数据是比较有限的,当我们要处理非常大量数据的时候,就可以用到数组了,比如我们上节课的那个数码管的真值表,我们就可以用一个数组来表达。
从概念上讲,数组是具有相同数据类型的有序数据的组合,一般来讲,数组定义后满足以下三个条件。
(1)具有相同的数据类型;
(2)具有相同的名字;
(3)在存储器中是被连续存放的。
    比如我们上节课定义的那个数码管真值表,如果我们把关键字code去掉,数组元素将被保存在RAM中,在程序中可读可写,同时我们也可以在中括号里边标明这个数组元素的个数,比如:
unsigned  char LedChar[16] = {
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8e};
在这个数组中的每个值都称之为数组的一个元素,这些元素都具备相同的数据类型就是unsigned char型,他们有一个共同的名字LedChar,不管放到RAM中还是FLASH中,他们都是存放在一块连续的存储空间里的。
有一点要特别注意,这个数组一共有16(中括号里面的数值)个元素,但是数组的元素的表达方式下标是从0开始,因此实际上上边这个数组的首个元素LedChar[0]的值是0xC0,LedChar[15]的值是0x8e,下标从015一共是16个元素。
LedChar这个数组只有一个下标,我们称之为一维数组,还有两个下标或者多个下标的,我们称之为多维数组。比如unsigned char a[2][3];表示这是一个23列的二维数组。在大多数情况下我们使用的是一维数组,对于初学来说,我们先来研究一维数组,多维数组遇到了再了解。
6.1.2 数组的声明
一维数组的声明格式如下:
数据类型   数组名[数组长度
(1)数组的数据类型声明的是该数组的每个元素的类型,即一个数组中的元素具有相同的数据类型。
(2)数组名的声明要符合C语言固定的标识符的声明要求,只能由字母、数字、下划线这三种符号组成,且第一个字符只能是字母或者下划线。
(3)方括号中的数组长度是一个常量或常量表达式,并且必须是正整数。
6.1.3 数组的初始化
数组在进行声明的同时可以进行初始化操作,格式如下:
数据类型   数组名[数组长度] = {初值列表}
还是以上节课我们用的数码管的真值表为例来讲解注意事项。
    unsigned  char LedChar[16] = {
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8e};
(1)初值列表里的数据之间要用逗号隔开。
(2)初值列表里的初值的数量必须小于或者等于数组长度,当小于数组长度时,数组的后边没有赋初值的元素由系统自动赋值0
(3)若给数组的所有元素赋初值,可以省略数组的长度,上节课的例子中我们实际上已经省略了数组的长度。
(4)系统为数组分配连续的存储单元的时候,数组元素的相对次序由下标来决定,就是说LedChar[0]LedChar[1]... ... LedChar[15]是按照顺序排下来的。
6.1.4 数组的使用和赋值
C语言程序中,是不能一次使用整个数组的,只能使用单个数组元素。一个数组元素相当于一个变量,使用数组元素的时候与使用相同数据类型的变量的方法一样。比如这个LedChar这个数组,如果没加code关键字,那么它可读可写,我们可以写成a = LedChar[0]这样来把数组的一个元素的值送个a这个变量,也可以写成LedChar[0] = a这样把a这个变量的值送给数组的一个元素,以下三点要注意:
(1)引用数组的时候,那个方括号里的数字代表的是数组元素的下标,而数组初始化的时候方括号里的数字代表的是这个数组元素的个数。
(2)数组元素的方括号里的下标可以是整型常数,整型变量或者表达式,而数组初始化的时候方括号里的数字必须是常数不能是变量。
(3)数组整体赋值只可以在初始化的时候操作,功能程序只能对单个元素赋值。
6.2 if语句
if语句已经不陌生了,前边程序我们其实已经用过了,这里我们系统的介绍一下,方便后边的深入学习。if语句有两个关键字:ifelse,把这两个关键字翻译一下就是:“如果”和“否则”。if语句一共有三种格式,我们分别来看。
1.if语句的默认形式。
if (条件表达式)
      {语句 1;}
    其执行过程是,if(如果)条件表达式的值为“真”,则执行语句1;如果条件表达式的值为“假”,则不执行语句1。真和假的概念不再赘述,参考第五章。
    这里要提醒一句,C语言一个分号表示一句语句的结束,因此如果if后边只有一条执行语句的时候,可以省略大括号,但是如果有多条执行语句的话,必须加上大括号。
    我们上节课的语句就很好理解了if(16 ==j) { j = 0;},如果j等于16的时候,括号里的值才是“真”,那么就执行j=0这一句,如果j不等于16,那么里边就为“假”,就不执行这一句。
2.if...else语句
    有些情况下,我们除了判断一下if括号里的是否满足条件,执行相应的语句,在不满足条件的时候,我们又要执行另外相应的语句,这个时候就用到了if...else语句,它的基本的语法形式是:
if (条件表达式)
    {语句 1;}
else
    {语句 2;}
比如上节课的后半段程序我们也可以写成:
                P0 = LedChar[j];    //把数组里的对应值送给P0
                if(15 == j)         //当显示到F后,归0重新开始
                {j = 0;}
                else
                {j++;}
    这个程序大家可以改改下载进去试试,程序逻辑大家自己动脑分析一下,我就不解释了。         
3.if....else if语句
    if...esle语句是一个二选一的语句,或者执行if条件下的语句,或者执行else条件下的语句。还有一种多选一的用法就是if...else if语句。他的基本语法格式是:
if (条件表达式1)            {语句 1;}
else if (条件表达式2)        {语句 2; }
else if (条件表达式3)        {语句 3; }
... ...
else                       {语句 n;}
他的执行过程是:依次判断条件表达式的值,当出现某个值为“真”时,则执行相对应的语句,然后跳出整个if的语句块,执行“语句n”后边的程序;如果所有的表达式都为“假”,则执行“语句n”后,再执行“语句n”后边的程序。
if语句在C语言编程的过程中使用频率很高,用法也简单,所以必须要熟练掌握。
6.3 switch语句
if....else语句在处理多分支的时候,分支太多就会显得不方便,且容易出现ifelse配对出现错误的情况,在C语言中提供了另外一种多分支选择的语句——switch语句,它的基本语法格式如下:
switch (表达式)
{
    case 常量表达式1:执行语句1
    case 常量表达式2:执行语句2
    ......
    case 常量表达式n:执行语句n
    default: 执行语句n+1;
}
它的执行过程是:首先计算“表达式”的值,然后从第一个case开始,与“常量表达式x”进行比较,如果与当前常量表达式的值不相等,那么就不执行冒号后边的程序,一旦发现和一个常量表达式的值相等了,那么他会执行之后所有的,注意是所有的“执行语句”,显然这不是我们想要的结果。
C语言中,有一条break语句,作用是跳出当前循环语句,不管是forwhile循环,还是switch循环,都可以用其搭配使用跳出循环switch语句一共有n+1种可能,而我们希望要的是一条多选一的语句,只执行其中一条然后直接退出该循环,不再执行下边的任何语句,这个时候就需要用到break语句,比如我们在switch表达式上加上break语句,如下:
switch (表达式)
{
    case 常量表达式1:执行语句1break
    case 常量表达式2:执行语句2break
    ......
    case 常量表达式n:执行语句nbreak
    default:语句n+1;
}
加了这个break语句后,一旦“常量表达式x”与“表达式”相等了,那就执行“执行语句x”,执行完毕后,由于有了break,直接跳出switch语句,执行switch语句循环后边的程序了,这样就可以避免执行不必要的语句。了解了这个switch语句,我们将会在本章程序中使用巩固。
6.4 数码管的动态显示6.4.1 动态显示的基本原理
我们在上一章学习数码管静态显示的时候说到,74HC138只能在同一时刻导通一个三极管,而我们的数码管是靠了6个三极管来控制,那我们如何来让数码管同时显示呢?这就用到了我们这节课的动态显示。
多个数码管显示数字的时候,我们实际上是轮流点亮数码管(一个时刻内只有一个数码管是亮的),利用人眼的视觉暂留现象(也叫余辉效应),就可以做到看起来是所有数码管都同时亮了,这就是动态扫描显示的含义。
例如:我们有2个数码管,我们要显示“12”这个数字,让高位的位选三极管导通,然后给它赋值“1”,延时一定时间后让低位的位选三极管导通,然后给它赋值“2”。把这个流程以一定的速度循环运行就可以让数码管显示出“12”,由于交替速度非常快,人肉眼识别到的就是“12”这个数字。
那么一个数码管需要点亮多长时间呢?也就是说要多长时间完成一次全部数码管的扫描呢(很明显:整体扫描时间=单个数码管点亮时间*数码管个数)?答案是:10ms以内。当电视机和显示器还处在CRT(电子显像管)时代时,有一句很流行的广告语——“100Hz无闪烁”,没错,只要刷新率大于100Hz,即刷新时间小于10ms,就可以做到无闪烁,这也就是我们的动态扫描的硬性指标。那么你也许会问,有最小值的限制吗?理论上没有,但实际上做到更快的刷新却没有任何进步的意义了,因为已经无闪烁了,再快也还是无闪烁,只是徒然增加CPU的负荷而已(因为1秒内要执行更多次的扫描程序)。所以,通常我们设计程序的时候,都是取一个接近10ms,又比较规整的值就行了。我们板子上有6个数码管,我们下面用程序来验证一下数码管动态显示程序。
#include <reg52.h>               //包含寄存器的库文件                  
sbit  ADDR0 = P1^0;
sbit  ADDR1 = P1^1;
sbit  ADDR2 = P1^2;
sbit  ADDR3 = P1^3;
sbit  ENLED = P1^4;
unsigned char code LedChar[] = {   //用数组来表示数码管真值表
    0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
    0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8e,
};
void main()   
{
    unsigned int counter = 0;
    unsigned char j = 0;
    unsigned long stopwatch = 0;
    unsigned char LedNumber[6] = {0};
    ENLED = 0; ADDR3 = 1;P0 = 0XFF;   //74HC138P0初始化部分
    TMOD = 0x01; //设置定时器0为模式1
    TH0  = 0xFC;
    TL0  = 0x67; //定时值初值,定时1ms
    TR0  = 1;    //打开定时器0
    while(1)
    {
        if(1 == TF0)             //判断定时器0是否溢出
        {
            TF0 = 0;
            TH0 = 0xFC;        //一旦溢出后,重新赋值
            TL0 = 0x67;
            counter++;
            if(1000 == counter)     //判断定时器0溢出是否达到50
            {
                counter = 0;
                stopwatch++;                  //秒表数值一秒加1
                LedNumber[0] = stopwatch%10;
                LedNumber[1] = stopwatch/10%10;
                LedNumber[2] = stopwatch/100%10;     
                LedNumber[3] = stopwatch/1000%10;  //数码管显示值计算
                LedNumber[4] = stopwatch/10000%10;
                LedNumber[5] = stopwatch/100000%10;
            }
          if (0==j)
         { ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; }
          else if (1==j)
         { ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; }
          else if (2==j)
         { ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; }
          else if (3==j)
         { ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; }
          else if (4==j)
         { ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; }
          else if (5==j)
         { ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; }
       }                   //数码管动态刷新部分
    }
}
这程序,大家自己抄到Keil中,然后边抄边理解,最终下载到实验板上实验一下效果。其中下边的if...else语句就是每1ms快速的刷新一个数码管,这样6个数码管整体刷新一遍的时间就是6ms,视觉上就是6个数码管无闪烁的同时亮起来了
另外一个简单知识点这个地方也提一下,其实属于小学三年级知识,但是很多同学刚接触C语言,可能遇到了也会发懵。就是在数码管显示值计算这个地方,相信小学我们没学小数之前,除法运算里边有“被除数”、“除数”、“商”、“余数”这四个概念年。而在我们C语言中,“/”等同于数学里的除法运算,而“%”等同于我们小学学的求余数运算。如果是123456这个数字,我们要正常显示在数码管上,个位显示,就是直接对10取余数,这个“6”就出来了,十位数字就是先除以10,然后再对10取余数,以此类推,就把6个数字全部显示出来了。
对于多选一的动态刷新数码管的方式,我们如果用switch会有更好的效果,大家来看一下我们用switch语句完成的情况。
#include <reg52.h>               //包含寄存器的库文件                  
sbit  ADDR0 = P1^0;
sbit  ADDR1 = P1^1;
sbit  ADDR2 = P1^2;
sbit  ADDR3 = P1^3;
sbit  ENLED = P1^4;
unsigned char code LedChar[] = {   //用数组来表示数码管真值表
    0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
    0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8e,
};
void main()   
{
    unsigned int counter = 0;
    unsigned char j = 0;
    unsigned long stopwatch =0;
    unsigned char LedNumber[6]={0};
    ENLED = 0; ADDR3 = 1;P0 = 0XFF;   //74HC138P0初始化部分
    TMOD = 0x01; //设置定时器0为模式1
    TH0  = 0xFC;
    TL0  = 0x67; //定时值初值,定时1ms
    TR0  = 1; //打开定时器0
    while(1)
    {
        if(1 == TF0)             //判断定时器0是否溢出
        {
            TF0 = 0;
            TH0 = 0xFC;        //一旦溢出后,重新赋值
            TL0 = 0x67;
            counter++;
            if(1000 == counter)     //判断定时器0溢出是否达到1000
            {
                counter = 0;
                stopwatch++;                  //秒表数值一秒加1
                LedNumber[0] = stopwatch%10;
                LedNumber[1] = stopwatch/10%10;
                LedNumber[2] = stopwatch/100%10;     
                LedNumber[3] = stopwatch/1000%10;  //数码管显示值计算
                LedNumber[4] = stopwatch/10000%10;
                LedNumber[5] = stopwatch/100000%10;
            }
            switch(j)
            {
            case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]];break;
            case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]];break;
            case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]];break;
            case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]];break;
            case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]];break;
            case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]];break;
            default: break;
             }
       }                   //数码管动态刷新部分
    }
}
大家是否能感觉到switch语句比if...else语句显得要整齐的多?
6.4.2 数码管消隐处理
不知道细心的同学能否发现,我们的两次数码管动态刷新显示的时候似乎并不是那么完美,第一个小问题,大家仔细看,数码管的不应该显示的段,似乎有微微的发亮,这种现象叫做“鬼影”,这个“鬼影”严重影响了我们的视觉效果,我们该如何解决呢?
同学们今后可能会遇到各种各样的问题,可能有很多我是没有讲过的问题,遇到问题怎么办呢?大家要相信,你作为初学者,遇到的问题肯定不是第一个遇到的,肯定有前辈会遇到同类问题,他们一般会在网上发表各种帖子,各种讨论,所以大家遇到问题,首先解决方法就应该形成一个到网上搜索的条件反射,这个问题大家可以到网上搜:“数码管消隐”或者“数码管鬼影解决”,多找相关关键词搜索,会搜索也是一种能力。
大家在网上搜了一下会发现,解决这类问题的普遍两个方法,其中之一是延时,延时之后我们肉眼就可能看不到这个“鬼影”了。但是延时是一个非常拙劣的手段,且不说延时多久能让我们看不到“鬼影”,延时后,我们的数码管亮度会普遍降低。我们解决问题呢,不能只知其然,不知其所以然,所以我们首先要弄懂为什么会出现“鬼影”。
“鬼影”的出现,主要是因为我们数码管位选和段选产生的瞬态所造成的。举个简单例子,我们在数码管动态刷新的那部分程序中,实际上每一个数码管点亮的持续时间是1ms的时间,1ms后进行下个数码管的切换。在进行数码管切换的时候,比如我们从case 5要切换到case 0的时候,case 5的位选用的是ADDR0=1; ADDR1=0; ADDR2=1;假如此刻case5也就是最高位数码管对应的值是0。我们要切换成的case 0的数码管位选是ADDR0=0; ADDR1=0; ADDR2=0;而对应的数码管的值假如是1
因为我们的C语言程序是一句一句顺序往下执行的,每一条语句都会占用一定的时间,即使这个时间非常非常短暂。但是当我们把“ADDR0=1”改变成“ADDR0=0”的时候,这个瞬间存在了一个中间状态ADDR0=0; ADDR1=0; ADDR2=1;在这个瞬间上,我们就给case 4对应的数码管DS5瞬间赋值了0。当我们全部写完了ADDR0=0; ADDR1=0; ADDR2=0;后,这个时候,我们的P0还没有正式赋值,而P0此刻却保持了前一次的值,也就是在这个瞬间,我们又给case 0对应的数码管DS1赋值了一个0。直到我们把case 0后边的语句全部完成后,我们的刷新才正式完成。而在这个刷新过程中,有2次瞬间我们给了错误的数码管赋值,虽然很弱(因为亮的时间很短),但是我们还是能够发现。
那弄懂了原理后,解决起来就不是困难的事情了,我们只要避开这个瞬态就可以了。不产生瞬态的方法是,我们在进行刷新的赋值语句期间,避免一切数码管的赋值即可。方法有两个,一个方法是刷新之前关闭所有的段,改变好了位选后,再打开段即可;第二个方法是关闭数码管的位,赋值过程都做好后,再重新打开即可。这个不是很难,答案我都公布一下。
关闭段:在switch(j)这句程序之前,加一句P0=0XFF;这样就把数码管所有的段都关闭了,当把“ADDR”的值全部搞定后,再给P0赋对应的值即可。
关闭位:在switch(j)这句程序之前,加上一句ENLED=1;等到把“ADDR=0; ADDR1=0; ADDR2=0; P0=LedChar[LedNumber[0]];这几条刷新程序全部写完后,再加上一句ENLED=0;然后再进行break操作即可。
这个地方稍微有点逻辑思路在里边,大家一定要理解深刻,深刻理解,彻底弄明白,把这个瞬态弄明白,后边很多牵扯到此类情况的问题,我们都可以一并搞定。
上边的数码管程序还有第二个问题,大家仔细看,我们的数码管上的数字每一秒变化一次,变化的时候,不参加变化的数码管可能出现一次抖动,这个抖动没有什么专业的名字,我们就称之为数码管抖动吧。这种数码管抖动是什么原因造成的呢?为何在数据改变的时候才抖动呢?
我们来看我们的程序。我们的程序在定时到1秒的时候,执行了“数码管显示值计算”这个过程,一个32位的除法运算,实际上是比较耗费时间的,至于这一段程序占用了多少时间,大家可以通过第四章讲的Debug进入看看这段程序运行一共占据了多少时间。由于达到1秒的时候,程序多运行了这么一段,导致了某个数码管的点亮时间比其他情况下要长一些,时间是1ms+程序消耗时间,于此同时,其它的数码管就熄灭了5ms+程序消耗时间,如果这个程序消耗时间非常短,那么可以忽略不计,但很明显,现在这段程序已经比较长了,严重影响我们的视觉效果了,所以我们要采取另外一种思路去解决这个问题。
6.5 中断的学习6.5.1 中断的产生背景
比如此刻我正在厨房用煤气烧一壶水,烧开一壶水刚好需要10分钟。我是一个主体,烧水是一个目的,而且我只能时时刻刻在这里烧水,因为一旦水开了,溢出来浇灭煤气的话,有可能引发一场灾难。而这个时候呢,我听到了电视里传来《天龙八部》的主题歌,马上就要开演了,我真想夺门而出,去看我最喜欢的电视剧。然而,听到这个水壶发出的“咕嘟”的声音,我清楚:除非水开了,否则我是无法享受我喜欢的电视剧的。
这里边主体只有我一个,而我要做的有两件事情,一个是看电视,一个是烧水,而电视和烧水是两个独立的客体,他们是同时进行的。其中烧水需要10分钟,但不需要了解烧水的过程的,只需要得到水烧开的这样一个结果就行了,提下水壶和关闭煤气只需要几秒的时间而已。所以我们采取的办法就是:烧水的时候,定上一个闹钟,定时10分钟,然后我就可以安心看电视了。当10分钟时间到了,闹钟响了,此刻水也烧开了,我就过去把煤气灭掉,然后继续回来看电视就可以了。
这个场景和单片机有什么关系呢?
在单片机的程序处理过程中也有很多类似的场景,当单片机正在专心致志的做一件事情的时候(如看电视),总会有一件或者多件紧迫或者不紧迫的事情发生,需要我们去关注,有一些需要我们停下手头的工作去马上完成(比如水开了),只有处理完,才能回头继续完成刚才的工作(看电视)。如果在这个地方用上了单片机的中断机制,不仅仅我拥有了处理意外情况的能力,而且如果我能够充分发挥这个机制的妙用,就可以“同时”完成多个任务了。如果还是一知半解关于中断更详细的介绍可以看这里: http://www.51hei.com/mcuteach/234.html
6.5.2 定时器中断应用方法
在第五章我们学过定时器,而实际上定时器一般用法都是采取中断方式来做的,我是故意在第五章用查询法,就是使用if(TR0 ==0)这样的语句先讲定时器,目的是明确告诉同学们,定时器和中断不是一回事,定时器是单片机模块的一个资源,确确实实存在的一个模块,而中断,是单片机的一种运行机制。尤其是初学者们,很多人会误以为定时器和中断是一个东西,只有定时器才会触发中断,但实际上很多事件都会触发中断的,除了“烧水”,还有“有人按门铃”,“来电话了”等等。
标准51中与中断相关的寄存器,一共有2个,其中1个是中断使能寄存器,另外1个是中断优先级寄存器,这里先介绍中断使能寄存器。随着一些增强型51单片机的问世,可能会有增加的寄存器,大家这些理解了这里所讲的,其他的通过自己研读数据手册全部可以理解明白并且使用起来。
表6-1 IE--中断使能寄存器(地址:A8H)
       可位寻址;复位值:0x00;复位源:任何复位
7
6
5
4
3
2
1
0
符号
EA
--
ET2
ES
ET1
EX1
ET0
EX0
表6-2 IE--中断使能寄存器的位描述
符号
描述
7
EA
总中断使能位,相当于总开关
6
--
--
5
ET2
定时器2中断使能
4
ES
串口中断使能
3
ET1
定时器1溢出中断使能
2
EX1
外部中断1使能
1
ET0
定时器0中断使能
0
EX0
外部中断0使能

中断使能寄存器IE控制了6个中断使能,其中第6位暂时不用,第七位是总开关,相当于我们家里或者学生宿舍里的那个电源总闸门。而05位这6个相当于每个分开关。那么也就是说,我们只要用到中断,就要写EA = 1这一句,打开中断总开关,然后用到哪个分中断,再打开相对应的位就可以了。
我们现在就把第五章学的定时器的程序进行改写,使用中断实现出来,把数码管的抖动问题也同时一并处理掉。
#include <reg52.h>               //包含寄存器的库文件                  
sbit  ADDR0 = P1^0;
sbit  ADDR1 = P1^1;
sbit  ADDR2 = P1^2;
sbit  ADDR3 = P1^3;
sbit  ENLED = P1^4;
unsigned char code LedChar[] = {   //用数组来表示数码管真值表
    0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
    0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8e,
};
unsigned char LedNumber[6] = {0}; //定义全局变量
unsigned char j = 0;
unsigned int counter = 0;
void main()
{
    unsigned long stopwatch =0;
    ENLED = 0; ADDR3 = 1; P0 = 0XFF;   //74HC138P0初始化部分
    TMOD = 0x01;  //设置定时器0为模式1
    TH0  = 0xFC;
    TL0  = 0x67;  //定时值初值,定时1ms
    TR0  = 1;     //打开定时器0
    EA = 1;       //打开中中断
    ET0 = 1;      //打开定时器0中断
    while(1)
    {
        if(1000 == counter)     //判断定时器0溢出是否达到1000
        {
            counter = 0;
            stopwatch++;
            LedNumber[0] = stopwatch%10;
            LedNumber[1] = stopwatch/10%10;
            LedNumber[2] = stopwatch/100%10;
            LedNumber[3] = stopwatch/1000%10;
            LedNumber[4] = stopwatch/10000%10;
            LedNumber[5] = stopwatch/100000%10;
        }
    }
}
void InterruptTimer0() interrupt 1               //中断函数的特殊写法,数字’1’为中断入口号
{
    TH0 = 0xFC;   //溢出后进入中断重新赋值
    TL0 = 0x67;
    counter++;   //计数值counter1
    P0 = 0xFF;   //消隐
    switch(j)
    {
        case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; break;
        case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; break;
        case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; break;
        case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; break;
        case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; break;
        case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; break;
        default: break;
    }    //动态刷新
}   
大家可以先把这个程序了解明白,下载到单片机里边实验一下,看看实际效果。是否可以看出来,近乎完美的显示效果经过我们的努力终于做成功了。那下面我们还要来解析一下我们的这个程序。
在我们这个程序中,有两个函数,一个是主函数,一个是中断函数。主函数main()我们就不用说了,重点强调一下中断函数,中断函数的格式是固定的,首先中断函数前边void表示函数返回空,即中断函数不返回任何值,函数名字是InterruptTimer0(),这个函数名字只要符合函数命名规则的前提下我们就可以随便起,我这样起名字是为了方便区分和记忆,而后是interrupt这个关键字不能错,这个是中断特有的关键字,另外后边还有个数字1,这个数字1怎么来的呢?我们先来看一个表格。
6-3 中断查询序列
描述
中断标志
向量地址
中断使能
默认优先级
外部中断0
IE0
0003H
EX0
1(最高)
T0中断
TF0
000BH
ET0
2
外部中断1
IE1
0013H
EX1
3
T1中断
TF1
001BH
ET1
4
UART中断
TI/RI
0023H
ES
5
T2中断
TF2/EXF2
002BH
ET2
6
这个表格同样不需要大家记住,需要的时候过来查就可以了。我们现在看第二行T0中断,它的中断标志是TF0,也就是当TF0变成1的时候,就会触发中断。而在interrupt后边的数字x的计算方法是 x*8+3=向量地址,T0的向量地址是000BH,那么我们可以求得x的值是1。这样这个中断函数名字我们就彻底明白了。
中断函数和普通函数有个不一样的地方,普通函数一般是在程序中调用,而中断函数因为有了中断入口,达到中断条件后,他会自动进入程序执行。比如咱这个程序,平时一直在主程序while(1)的循环中运行,假如程序有100行,当运行到了50行的时候,定时器溢出了,那么CPU就会立刻跑到中断函数中执行中断程序,中断程序运行完毕后再自动返回到刚才的第50行处继续运行下面的程序,这样就保证了动态刷新是固定的1ms时间,不会因为程序运行时间不一致的原因导致数码管的抖动了。
6.5.3 中断的优先级
中断优先级的内容,大家先通过我的介绍大概了解一下即可,后边真正实际应用的时候我们再详细理解。
在讲中断产生背景的时候,我们仅仅讲了看电视和烧水的例子,但是实际生活当中还有更复杂的,比如我们正在看电视,这个时候来电话了,我们要进入接电话的“中断”程序当中去,就在接电话的同时,听到了水开的声音,水开的“中断”也发生了,我们要放下手上的电话,先把煤气关掉,然后再回来听电话,最后听完了电话再看电视,这里就产生了一个优先级的问题。
还有一种情况,我们在看电视的时候,这个时候听到水开的声音,水开的“中断”发生了,我们要进入关煤气的“中断”程序当中,而在关煤气的同时,电话声音响了,而这个时候,我们的处理方式是先把煤气关闭,再去接听电话,最后再看电视。
从这两个过程中,我们可以得到一个结论,就是最最紧急的事情,一旦发生后,我们不管当时处在哪个“程序”当中,我们必须先去解决最最紧急的事情,解决完毕后再去解决其他事情。在我们的单片机程序当中有时候也是这样的,有一般紧急的中断,有特别紧急的中断,这取决于具体的系统设计,这就牵扯到一个中断优先级和中断嵌套的概念,在本章节我们先简单介绍一下相关寄存器,不做例程说明。
中断优先级有两种,一种是抢占优先级,一种是固有优先级,先介绍抢占优先级。
表6-4 IP--中断优先级寄存器的位分配(地址:B8H)
       可位寻址;复位值:0x00;复位源:任何复位
7
6
5
4
3
2
1
0
符号
--
--
PT2
PS
PT1
PX1
PT0
PX0
6-5 IP--中断优先级寄存器的位描述(地址:B8H)
符号
描述
7
--
保留
6
--
保留
5
PT2
定时器2中断优先级控制位
4
PS
串口中断优先级控制位
3
PT1
定时器1中断优先级控制位
2
PX1
外部中断1中断优先级控制位
1
PT0
定时器0中断优先级控制位
0
PX0
外部中断0中断优先级控制位
这个寄存器的每一位,表示对应的中断功能的优先级,每一位的复位值都是0,当我们把某一位设置为1的时候,这一位的优先级就比其他位的优先级高。比如我们设置了PT0位为1后,当程序运行在主循环里边,或者任何其他中断程序内部的时候,一旦定时器0发生中断,作为更高级的优先级,程序马上就会跑到定时器0的中断程序中运行。同理,当程序此刻运行在定时器0中断中时,其他低级的中断发生后,程序还是会继续运行定时器0中断程序,直到把定时器0中的中断程序运行完成后,再会去相应其他中断程序。
我们在专业的术语中,当进入低级中断以后,发生高级中断,我们先进入高级中断运行,处理完了高级中断后,返回处理低级中断,低级中断处理完了再返回主函数,这种叫做中断嵌套。在抢占优先级配置过程中,优先级高的中断是可以抢占优先级低的中断,形成中断嵌套的,当然,优先级低的是不能抢占优先级高的中断的。
第二种是固有优先级,大家可能在看表6-3中断查询序列里就看到了有一个中断优先级列表,在这个列表中,中断优先级是从高到低排列的。但是固有优先级和抢占优先级不同,首先固有优先级不会形成中断嵌套,也就是只要当前程序进入中断执行程序了,其他任何中断来了,都会先执行完了当前的中断再回头响应的。
那这个固有优先级的作用是什么呢?还有一种情况,就是当中断同时发生,或者是我们在开中断前,已经有几个中断标志位置位了,也就是说我们可以理解为同时检测到几个中断产生了,那么我们会先相应表6-3中的优先级高的中断,处理完后再来相应优先级低的中断。
6.6 作业
1、掌握C语言的数组的概念、定义和应用。
2、掌握if语句和switch语句的用法及区别,编程的时候能够正确选择使用哪个语句。
3、彻底理解中断的原理和应用方法,关闭教程自己独立把本章节程序编写完毕并且下载到实验板上实践。
4、大家尝试修改程序,让我们的数码管只显示有效位,也就是高位的0不显示。
5、大家改动程序,写一个数码管从999999倒计时程序,并且改用定时器1的中断来完成,通过写这个程序,熟练掌握定时器和中断的应用。

上一课:第五章 定时器和数码管
下一课:第七章 点阵LED的学习

评分

参与人数 2黑币 +10 收起 理由
chenjuncom11 + 5 赞一个!
hei51ck + 5 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

ID:37147 发表于 2013-9-7 12:04 | 显示全部楼层
很有用!我要反复的看看
回复

使用道具 举报

ID:61042 发表于 2014-4-23 22:59 | 显示全部楼层

高手请帮忙看看这个程序哪里有什么问题?

本帖最后由 lmjnkj 于 2014-4-23 23:02 编辑

include<reg52.h>
#include<intrins.h>
sbit D14=P1^4;
sbit D24=P2^4;
sbit D25=P2^5;
sbit D26=P2^6;
sbit D27=P2^7;
unsigned char da_l,da_h,i;
volatile unsigned char e;
void delay();
void main()
{
    TMOD=0X01;
    EA=1;
    ET0=1;
    TH0=0xb1;
    TL0=0xe0;
    TR0=1;
    P0=0Xff;
    D27=1;
    D27=0;
    D14=1;
    P0=0X00;
    da_l=0xff;//164输出高电平有效
     //下面循环程序是将164从Q7到Q0全总部输出为高电平
    for(i=0;i<8;i++)
    {
       D24=0;
       D25=da_l&0x80;
       D24=1;
       da_l<<=1;
    }
    P0=0XFF;
    D26=1;
    da_h=0xfe; //Y为低电平有效
   
    while(1);
   
}
void time0(void) interrupt 1
{
   
   
    TH0=0xb1;
    TL0=0xe0;
    e++;
    if(e==50)
    {
          e=0;
          P0=da_h;
         
      
          da_h=_crol_(da_h,1);//循环移位程序
    }   
    P0=0XFF;
}
回复

使用道具 举报

ID:78273 发表于 2015-5-28 00:12 | 显示全部楼层
中断是单片机系统的重点中的重点,
回复

使用道具 举报

ID:86177 发表于 2015-7-20 22:15 | 显示全部楼层
实在是太谢谢你了
回复

使用道具 举报

ID:88499 发表于 2015-8-27 06:09 | 显示全部楼层
不错!
回复

使用道具 举报

ID:112090 发表于 2016-4-3 20:10 | 显示全部楼层
你好   我想请教一下  如何高位灭0啊  就是这个教程的作业4
回复

使用道具 举报

ID:124050 发表于 2016-6-1 16:26 | 显示全部楼层
超级好人    学习了
回复

使用道具 举报

ID:126696 发表于 2016-6-15 11:37 | 显示全部楼层
大好人 先保存 暑假再好好学习
回复

使用道具 举报

ID:147082 发表于 2016-11-18 11:41 | 显示全部楼层
那个if(1000==counter)判断是否溢出50次不是很明白,有没有大神指点一下
回复

使用道具 举报

ID:160490 发表于 2017-1-6 19:43 | 显示全部楼层
太有用了!
回复

使用道具 举报

ID:151661 发表于 2017-3-25 19:13 来自手机 | 显示全部楼层
难度逐渐大了。
回复

使用道具 举报

ID:193871 发表于 2017-4-26 16:11 | 显示全部楼层
好多中断啊,学习了
回复

使用道具 举报

ID:192838 发表于 2017-4-27 20:14 | 显示全部楼层
Bnuzdaxian 发表于 2016-11-18 11:41
**** 作者被禁止或删除 内容自动屏蔽 ****

应该是溢出1000次
回复

使用道具 举报

ID:219424 发表于 2017-7-17 12:47 | 显示全部楼层
怎么看不到原文呢,前面几张讲得很好,先保存了
回复

使用道具 举报

ID:214997 发表于 2017-7-19 20:28 | 显示全部楼层
效果是什么样的?我是一闪一闪的呢?是这样吗?
回复

使用道具 举报

ID:224296 发表于 2017-8-3 10:24 | 显示全部楼层
以前就看到这个帖子了,最近在学这个。正好来看看,中断好像比32的简单一点
回复

使用道具 举报

ID:220342 发表于 2017-8-8 09:29 | 显示全部楼层
Bnuzdaxian 发表于 2016-11-18 11:41
**** 作者被禁止或删除 内容自动屏蔽 ****

他写错了,应该是判断是否溢出1000次,1000次相当于1秒。
回复

使用道具 举报

ID:227864 发表于 2017-8-16 11:57 | 显示全部楼层
讲得通俗易懂,很不错
回复

使用道具 举报

ID:199513 发表于 2017-9-8 18:27 | 显示全部楼层
unsigned char LedNumber[6]={0};  各位高手能够否讲解一下,这一句中的   {0}  代表的是啥? 谢谢!
回复

使用道具 举报

ID:155764 发表于 2017-10-24 13:07 | 显示全部楼层
赞一个!
回复

使用道具 举报

ID:233325 发表于 2017-11-1 12:40 来自手机 | 显示全部楼层
看得很仔细、就是忘得也挺快
回复

使用道具 举报

ID:237247 发表于 2017-11-5 21:21 | 显示全部楼层
关闭位时,ENLED=0;74HC138就不起作用了,也起不到选取六个数码管了?不理解。帮解释一下
回复

使用道具 举报

ID:248102 发表于 2017-11-10 15:54 | 显示全部楼层
沙灬漠 发表于 2017-9-8 18:27
unsigned char LedNumber[6]={0};  各位高手能够否讲解一下,这一句中的   {0}  代表的是啥? 谢谢!

意思是这个数组里的每一个值都初始化为0
回复

使用道具 举报

ID:248102 发表于 2017-11-10 16:33 | 显示全部楼层
请教一个问题:
在利用中断解决“数码管抖动”的问题时,假如在执行到 if(1000 == counter) 里面中间位置时,LedNumber:0,1,2已经更新,3,4,5没有更新,这时定时器发生中断,在中断服务程序中去设置P0的值会不会出错?
回复

使用道具 举报

ID:250781 发表于 2017-11-24 13:56 来自手机 | 显示全部楼层
学到不少知识啊,太给力了
回复

使用道具 举报

ID:253047 发表于 2017-11-24 14:32 | 显示全部楼层
太给力了  学习很多
回复

使用道具 举报

ID:247525 发表于 2017-12-16 17:49 | 显示全部楼层
switch(j)             {             case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]];break;             case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]];break;             case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]];break;             case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]];break;             case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]];break;             case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]];break;             default: break;              }#在这里快速回复#case后面没有常量判断表达式那么往下是执行哪一条语句
回复

使用道具 举报

ID:280978 发表于 2018-1-30 22:44 | 显示全部楼层
#include <reg52.h>               //包含寄存器的库文件                  

unsigned char code LedChar[] = {   //用数组来表示数码管真值表
    0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80};
unsigned int counter = 0;
unsigned char j = 5;
unsigned char LedNumber[6] = {0};
void main()   
{
   
    unsigned long stopwatch = 999999;
   
    P2=0xe0;P0=0x80;P2=0x00;   //74HC138和P0初始化部分
    TMOD = 0x01; //设置定时器0为模式1
    TH0  = 0xFC;
    TL0  = 0x67; //定时值初值,定时1ms
    TR0  = 1;    //打开定时器0
        EA = 1;       //打开中中断
    ET0 = 1;      //打开定时器0中断
    while(1)
    {
        
            if(1000 == counter)     //判断定时器0溢出是否达到50次
            {
                counter = 0;
                stopwatch--;                  //秒表数值一秒加1
                LedNumber[0] = stopwatch%10;
                LedNumber[1] = stopwatch/10%10;
                LedNumber[2] = stopwatch/100%10;     
                LedNumber[3] = stopwatch/1000%10;  //数码管显示值计算
                LedNumber[4] = stopwatch/10000%10;
                LedNumber[5] = stopwatch/100000%10;
            }
         
                          //数码管动态刷新部分
    }
}
void InterruptTimer0() interrupt 1               //中断函数的特殊写法,数字’1’为中断入口号
{
    TH0 = 0xFC;   //溢出后进入中断重新赋值
    TL0 = 0x67;
    counter++;   //计数值counter加1
    P0=0xff;   //消隐
        switch(j)
            {
            case 0: P2=0xe0; j=5; P0=LedChar[LedNumber[5]];P2=0x00;P2=0xc0;P0=0x01;P2=0x00;break;
            case 1: P2=0xe0; j--; P0=LedChar[LedNumber[4]];P2=0x00;P2=0xc0;P0=0x02;P2=0x00;break;
            case 2: P2=0xe0; j--; P0=LedChar[LedNumber[3]];P2=0x00;P2=0xc0;P0=0x04;P2=0x00;break;
            case 3: P2=0xe0; j--; P0=LedChar[LedNumber[2]];P2=0x00;P2=0xc0;P0=0x08;P2=0x00;break;
            case 4: P2=0xe0; j--; P0=LedChar[LedNumber[1]];P2=0x00;P2=0xc0;P0=0x10;P2=0x00;break;
            case 5: P2=0xe0; j--; P0=LedChar[LedNumber[0]];P2=0x00;P2=0xc0;P0=0x20;P2=0x00;break;
            default: break;
             }


}
求教作业题就是那个999999,做不出来。。。
回复

使用道具 举报

ID:285691 发表于 2018-2-28 10:14 | 显示全部楼层
Bnuzdaxian 发表于 2016-11-18 11:41
**** 作者被禁止或删除 内容自动屏蔽 ****

这个是笔误

回复

使用道具 举报

ID:315638 发表于 2018-4-25 10:05 | 显示全部楼层
请问有没有开发板的原理图?可不可以分享一下!!!!
回复

使用道具 举报

ID:320487 发表于 2018-5-3 16:15 | 显示全部楼层
太难了  这一章根本看不懂
回复

使用道具 举报

ID:320487 发表于 2018-5-5 07:05 | 显示全部楼层
越看越复杂呀
这个应该是高中的程度啦  
回复

使用道具 举报

ID:93625 发表于 2018-6-29 10:24 | 显示全部楼层
继续学习!
回复

使用道具 举报

ID:378091 发表于 2018-7-23 17:45 | 显示全部楼层
我用定时器1时,很明显没到1秒数字就跳了,这是为什么啊?
回复

使用道具 举报

ID:341430 发表于 2018-7-25 15:28 | 显示全部楼层
        case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; break;
        case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; break;
        case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; break;
        case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; break;
        case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; break;
        case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; break;
请问下这段的意思是什么
回复

使用道具 举报

ID:341901 发表于 2018-9-2 10:58 来自手机 | 显示全部楼层
沙灬漠 发表于 2017-9-8 18:27
**** 作者被禁止或删除 内容自动屏蔽 ****

按道理说里面应该有六组数,实际就写了第一组为0,缺少的五组数据自动默认为0,这里的首位0你可以更改为任意数都一样的,代表的是下标0的数
回复

使用道具 举报

ID:313459 发表于 2018-9-26 20:08 来自手机 | 显示全部楼层
很有用,谢谢
回复

使用道具 举报

ID:490950 发表于 2019-4-25 12:56 来自手机 | 显示全部楼层
问一下,怎么高位灭0
回复

使用道具 举报

ID:490950 发表于 2019-5-11 23:42 来自手机 | 显示全部楼层
数码管怎么高位灭0啊求教
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表