打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
51单片机教程-第六集:中断的学习(二)

  


  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分钟时间到了,闹钟响了,此刻水也烧开了,我就过去把煤气灭掉,然后继续回来看电视就可以了。

  这个场景和单片机有什么关系呢?

  在单片机的程序处理过程中也有很多类似的场景,当单片机正在专心致志的做一件事情的时候(如看电视),总会有一件或者多件紧迫或者不紧迫的事情发生,需要我们去关注,有一些需要我们停下手头的工作去马上完成(比如水开了),只有处理完,才能回头继续完成刚才的工作(看电视)。如果在这个地方用上了单片机的中断机制,不仅仅我拥有了处理意外情况的能力,而且如果我能够充分发挥这个机制的妙用,就可以“同时”完成多个任务了。

  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位暂时不用,第七位是总开关,相当于我们家里或者学生宿舍里的那个电源总闸门。而0到5位这6个相当于每个分开关。那么也就是说,我们只要用到中断,就要写EA = 1这一句,打开中断总开关,然后用到哪个分中断,再打开相对应的位就可以了。

  我们现在就把第五章学的定时器的程序进行改写,使用中断实现出来,把数码管的抖动问题也同时一并处理掉。

#include                //包含寄存器的库文件                   

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;   //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溢出是否达到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++;   //计数值counter加1

    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中的优先级高的中断,处理完后再来相应优先级低的中断。


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
51单片机的中断函数
嵌入式系统优先级详解
基于proteus的51单片机开发实例(12)-数码管显示按键次数
单片机入门培训专题(十) – 数码管相关知识 – 著名的PCB哥
日志
STM32单片机学习笔记(超详细整理143个问题,学习必看)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服