2024年5月28日发(作者:明丰)
第五节 数码管的使用
5.1 数码管简介
同学们!相信你的流水灯也做的不错了吧,现在能玩出几种花样了?但是工程师们设计
这么一个单片机,并不是只为了让它做流水灯的,那样也太浪费点了吧 ... ^_^ 。
数码管的一种是半导体发光器件,7段LED数码管是利用7个LED(发光二极管)外
加一个小数点的LED组合而成的显示设备,可以显示0~9等10个数字和小数点,使用非常
广泛,数码管可以分为一位和多位它的外观如图5-1所示。
图5-1
5.2 数码管的显示原理
数码管可以分为共阳极与共阴极两种,共阳极就是把所有LED的阳极连接到共同接点
com,使用时com接正5伏电源,而每个LED的阴极分别为a、b、c、d、e、f、g及dp(小
数点);共阴极则是把所有LED的阴极连接到共同接点com,使用时com要将其接地。而
每个LED的阳极分别为a、b、c、d、e、f、g及dp(小数点),8个LED的分布方式如图
5-2所示。图中的8个LED分别与上面那个图中的A~DP各段相对应,通过控制各个LED
的亮灭来显示数字。
那么,实际的数码管的引脚是怎样排列的呢?对于单个数码管来说,从它的正面看进去,
左下角那个脚为1脚,以逆时针方向依次为1~10脚,左上角那个脚便是10脚了,上面两个
图中的数字分别与这10个管脚一一对应。注意,3脚和8脚是连通的,这两个都是公共脚。
它对应的引脚分布为图5-3所示。
图5-2 图5-3
数码管的8段,对应一个字节的8位,a对应最低位,dp(小数点)对应最高位。所以
如果想让数码管显示数字0,那么共阴数码管的字符编码为00111111,即0x3f;共阳数码
管的字符编码为11000000,即0xc0。可以看出两个编码的各位正好相反。如图5-4所示。
图5-4
那么,一位数码管要显示字符0~F,则对应的编码如表2所示。
一个八段数码管称为一位,多个数码管并列在一起可构成多位数码管,它们的段选线(即
a,b,c,d,e,f,g,dp)连在一起,而各自的公共端称为位选线。显示时,都从段选线送入字符编码,
而选中哪个位选线,那个数码管便会被点亮。
5.3 一位数码管的显示
请实现:让一位数码管依次显示字符0~F,每个字符显示1秒,如此反复。
一般情况下,为了计算或取码的方便,我们把a-dp依次接到单片机某个口上的Px.0--Px.7
上。x表示0,1,2,3其中的一个。这样我们只要给某个口,赋一个值,则相应的LED段
就被点亮,但是在硬件连接上要注意了:单片机可能不能直接驱动LED,所以我们可以通
过控制三级管的导通或截止,或者使用共阳极数码管(以灌电流的方式)、或者使用锁存器
来驱动。来控制LED的亮与灭!
5.3.1硬件的选择与仿真电路的设计
1.打开Proteus,选择“File/NewDesign”菜单选项,新建一个“设计项目”。并将项目保
存为“SEG7_1”。
2.选择“P”按钮或菜单“Library/Pick Divice/Symbol… P”菜单,从“元件库”中选取
元件。依次添加其他元件。其名称和位置见下表。
元件名称
AT89C52
Category Sub-Category Results
AT89C52 Microprocessor ICs 8051 Family
7SEG-MPX1-CA(注1) Optoelectronics
RX8(注2)
RESPACK-8(注3)
Resistors
Resistors
7-Segment Displays 7SEG-MPX1-CA
Resistor Packs
Resistor Packs
RX8
RESPACK-8
注1:7SEG表示7段数码管(Proteus还提供了14段和16段数码管)
MPX1表示1位(Proteus还提供了2位、4位、6位和8位数码管)
CA表示共阳极(CC表示共阴极)
注2:RX8表示电阻排,它实际相当于8个电阻并排摆放在一个容器内。在这里是作为限流
电阻来使用的。
注3:RESPACK-8表示电阻排,它实际相当于8个电阻并排摆放在一个容器内,但是这8个
电阻的一段是连接在一起作为公共端的。在这里是作为P0口的上拉电阻来使用的。
依次从备选元件库中摆放器件,连线,画出仿真电路图,如图5-5所示。
图5-5
注意:在Proteus中,实际上,51单片机是不需要晶振、复位电路和电源就可以仿真的,因
此,为了方便我们教学,以后,我们将不再画上述51的外围电路。
5.3.2程序的设计
1.新建一个keil项目,并命名为“SEG7_1”并添加一个名为“main.c”的源代码文件,然
后键入如下代码。如代码5.1所示。
//代码5.1
#include
#define SegPort P0 //定义数码管连接的端口
#define uchar unsigned char //宏定义 将unsigned char 替换为较为简单的uchar写法
#define uint unsigned int //宏定义 将unsigned int 替换为较为简单的uint写法
//用一个数组来定义字符0~f共阳极数码管编码
uchar code seg7ca[]={
0xc0,0xf9,0xa4,0xb0,//0~3
0x99,0x92,0x82,0xf8,//4~7
0x80,0x90,0x88,0x83,//8~b
0xc6,0xa1,0x86,0x8e //c~f
};
//延时函数ms毫秒
void DelayMs(uint ms)
{
uint i,j;
for(i=0;i { for(j=0;j<124;j++); } } void main() { while(1)//无限大循环 { //这段代码将字符0~F轮流显示一遍,每个字符显示1秒 uchar i=0; //从数组第一个元素开始显示 for(i=0;i<16;i++) { SegPort=seg7ca[i];//按次序显示字符0~F DelayMs(1000); //延时1秒 } } 知识点:数组seg7ca[]的定义 格式:数据类型 (数组位置) 数组名称[]={ 数组元素1,数组元素2,……}; 知识点:code关键字 “code”是C51所定义的关键字,他的含义是定义将代码放在ROM中,由于51的RAM很 小,因此,为了节约有限的RAM,我们通常会把一些不会变化的数据(比如数码管的编码、 参数表等等)放在ROM中,这样,我们就可以多空出一些RAM供程序来使用。 那么怎样把这些常量放在ROM中呢,即给定义语句前添加一个“code”关键字,告诉编译 器,这些常量要放在ROM中,如果没有添加“code”,那么这些编码就会放在RAM中。同 学们可以通过删除“code”关键字,重新build程序,然后查看程序占用RAM的大小,来 验证“code” 关键字的作用。 知识点:数组元素的访问 数组元素是通过他的序号来访问的。例如数组 seg7ca[]={ 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8 e}一共有16个元素,其中0xc0的序号为0(请记住,数组中的第一个元素序号为0),0xf9 的序号为1,……,0x8e的序号为15,以此类推。那么当我们想要使用0xc0时,我们可以 使用“seg7ca[0]”这种方式。因此,代码“SegPort=seg7ca[i]”;//按次序显示字符0~F 程序代码说明 uchar i=0; //从数组第一个元素开始显示 for(i=0;i<16;i++) { SegPort=seg7ca[i];//按次序显示字符0~F DelayMs(1000); //延时1秒 } 这段代码将字符0~F轮流显示一遍,每个字符显示1秒 当for循环开始时,i=0,因此,SegPort=seg7ca[0]; 由于SegPort是P0,seg7ca[0]=0xc0,因此,P0=0xc0,即P0口送出0xc0,由于0xc0是共 阳极数码管字符“0”的编码,因此,数码管显示字符“0”。 当for第二次循环时,i=1,因此,SegPort=seg7ca[1]; 即,P0=0xf9,即P0口送出0xf9,由于0xf9是共阳极数码管字符“1”的编码,因此,数 码管显示字符“1”。 以此类推,随着for循环,P0口送出数组seg7ca所定义的16个元素,从而在数码管上显示 对应的十六个字符。 5.4 多位数码管的显示 想必大家已经可以把0-F显示出来了吧!但是如果要你显示两位数,三位数呢?让我们 实现如下功能:让两位数码管显示数字“15”。或许,有的朋友会这么想:在P0口上接一 个数码管,再在P1口上接个数码管!但是,如果要显示4位、5位的数字呢?那岂不是一 块AT8951都接不过来!难到就不能接4位或5位以上的吗?肯定不是的! 说到这里,我们来讲讲数码管的显示方式,可分为两种:动态扫描和静态显示。上面我 们所说的即为静态显示。但是如果我们采用动态扫描显示,那么就可以解决上面的问题,动 态扫描是指每隔一段时间循环点亮每个数码管,每次只有一个数码管被点亮。细心的朋友会 问这样的问题:是让数码管一个一个亮,那还是不能控制数码管一起亮或灭嘛!怎么解决? 其实,人的眼睛有视觉暂留效应,黑夜里,拿着一支烟,在你面前快速的晃动,你会发 现什么样的现象?是不是原本不连续的点变成了一条看上去连续的曲线或者直线!再回过头 来,仔细想想我们的数码管!原理是一样的,只要我们快速的循环显示每个数码管,人的眼 睛看起来就好像是它们同时被点亮了,关键是速度。 比如点亮6位数码管,硬件连接可以这样解决:a--dp还是接至P0.0--P0.7上,还有6 个COM脚再接至另外口的P2.0--P2.5。P0口作段选(控制数字字符),P2口作位选(选通 哪个数码管被点亮)这样我们控制P0和P2口就可以控制6个数码管了。 5.4.1硬件的选择与仿真电路的设计 1.打开Proteus,选择“File/NewDesign”菜单选项,新建一个“设计项目”。并将项目保 存为“SEG7_2”。 2.选择“P”按钮或菜单“Library/Pick Divice/Symbol… P”菜单,从“元件库”中选取 元件。依次添加其他元件。其名称和位置见下表。 元件名称 AT89C52 Category Sub-Category Results AT89C52 7SEG-MPX2-CC RX8 RESPACK-8 74HC573 Microprocessor ICs 8051 Family 7-Segment Displays Resistor Packs Resistor Packs Flip-Flops & latches 7SEG-MPX2-CC(注1) Optoelectronics RX8 RESPACK-8 74HC573(注2) Resistors Resistors TTL 74HC series 注1:7SEG表示7段数码管、MPX2表示2位、CC表示共阴极(CC表示共极) 注2:74HC573是一个8位锁存器,在这里主要是提供锁存和驱动功能。 依次从备选元件库中摆放器件,连线,画出仿真电路图,如图5-6所示。 图5-6 1.连线的标号连接法:由于某些连线不太方便直接连接,或者由于美观原因,我们可以采用 标号连接。例如,我们想将51的P1.2引脚和74HC573的“LE”引脚相连,方法如下: (1)先将51的P1.2引脚和74HC573的“LE”引脚各用鼠标延长一段。 (2)点击Proteus侧边工具栏的“LBL(Wire Label Mode)”按钮,如图5-7所示。 Wire Label Mode 按钮 图5-7 图5-8 (3)将鼠标移动到51的P1.2引脚点一下,出现如下“Edit Wire Label”窗口,在“Edit Wire Label”窗口的“string”编辑框中填入该引脚的名称,比如“P12”,然后点击“OK”。如果 5-8所示。 (4)再将将鼠标移动到74HC573的“LE”引脚点一下,再在“Edit Wire Label”窗口的“string” 编辑框中填入该相同的名称,比如“P12”, 然后点击“OK”。 (5)这样,引脚标号相同的引脚就会被物理的连接在一起了。 2. 74HC573的使用 74HC573为8位3态非反转透明锁存器。数据从D0~D7引脚输入,从Q0~Q7引脚输出。 他的真值表如图5-9所示。 图5-9 OE为输出使能端,低电平有效,当OE为高电平时,D和Q不通,即所谓的输出高阻态。 LE为锁存端,当LE为高电平时,D和Q是直通的,当LE为低电平时,不管D端输入是 什么,Q端保持上一次的输出,即开启锁存功能。 3.电路说明 因为我们使用了两位数码管,因此,我们需要轮流点亮数码管的各位和十位。由于数码管是 共阴极的,因此,我们需要在P0口送出数码管共阴极的编码,同时,给要点亮数码管位选 线一个低电平,由于51的驱动能力不足以直接驱动数码管,因此,我们通过74HC573这个 锁存器来驱动,它的输出电流足够大,能够很好的驱动数码管发光。 5.4.2程序的设计 1.新建一个keil项目,并命名为“SEG7_2”并添加一个名为“main.c”的源代码文件,然 后键入如下代码。如代码5.2所示。 代码5.2 #include #define SegPort P0 //定义数码管连接的端口 #define uchar unsigned char //宏定义 将unsigned char 替换为较为简单的uchar写法 #define uint unsigned int //宏定义 将unsigned int 替换为较为简单的uint写法 //用一个数组来定义字符0~f共阴极数码管编码 uchar code seg7cc[]= { 0x3f,0x06,0x5b,0x4f, //0~3 0x66,0x6d,0x7d,0x07, //4~7 0x7f,0x6f,0x77,0x7c, //8~b 0x39,0x5e,0x79,0x71 //c~f }; sbit GeWei=P1^1; //定义数码管个位的位选线 sbit ShiWei=P1^0; //定义数码管十位的位选线 sbit le573=P1^2; //定义573LE引脚的位选线 //延时函数ms毫秒 void DelayMs(uint ms) { uint i,j; for(i=0;i { for(j=0;j<124;j++); } } void main() { GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 while(1)//无限大循环 { le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[5]; //将‘5’的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[1]; //将‘1’的编码送出 ShiWei=0;//再显示十位,将个位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); } } GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 程序代码说明 1.初始化 GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 通过这两行代码将数码管的个位和十位位选线都置为高电平,对于共阴极数码管来讲, 那么个位和十位都不能显示了,即先将两位都关闭。 2.我们现在想让个位数码管显示字符“5”,我们需要做的是: (1)LE引脚拉高,使74HC573进入直通模式。 (2)P0口送出字符“5”共阴极编码“0x6d” (3)GeWei (即P1.1)引脚送出低电平,选中数码管个位,此时数码管个位显示字符“5”。 (4) LE引脚拉低,使74HC573进入锁存模式,此时,74HC573的Q端始终保持输出“0x6d”, 则数码管个位始终保持显示字符“5” 把上述伪代码翻译为C51代码就是: le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[5]; //将‘5’的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 3.消隐操作 为了防止数码管闪烁,我们需要消隐操作,即将两位数码管都关闭,代码如下: DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 4.我们再用相同的方法让十位数码管显示字符“1”。 5.通过循环操作,使两位数码管快速轮流显示字符“1”和“5”,由于视觉暂留效应,我们 看到的就是字符“1”和“5”同时显示出来了。 5.5 多位数码管显示秒数据 让我们实现如下功能:让两位数码管依次显示数字“00~59”,每个数字显示1秒,显示 完59后,回到00,如此反复。 5.5.1实现原理分析 1.我们使用一个变量second表示当前的秒数,让second从0开始,每隔一秒钟加1,当second 增加到59时,让它回到0,重新开始。这样,second就在0~59之间反复变化。 2.从second中拆分出它的个位和十位呢?我们可以使用以下方法。 提取个位:tmp=second%10;//例如second=45,second%10=5 提取十位:tmp=second/10;//例如second=45,second/10=4 3. 我们让数码管的个位显示second的个位,数码管的十位显示second的十位。 5.5.2 硬件电路的设计 不变。 5.5.3程序的设计 1.新建一个keil项目,并命名为“SEG7_3”并添加一个名为“main.c”的源代码文件,然 后键入如下代码。如代码5.3所示。 //代码5.3 #include #include "commmon.h" //包含#define uchar unsigned char等定义 #include "function.h" //包含DelayMs函数 #define SegPort P0 //定义数码管连接的端口 sbit GeWei=P1^1; //定义数码管个位的位选线 sbit ShiWei=P1^0; //定义数码管十位的位选线 sbit le573=P1^2; //定义573LE引脚的位选线 //用一个数组来定义字符0~f共阴极数码管编码 unsigned char code seg7cc[]= { 0x3f,0x06,0x5b,0x4f, //0~3 0x66,0x6d,0x7d,0x07, //4~7 0x7f,0x6f,0x77,0x7c, //8~b 0x39,0x5e,0x79,0x71 //c~f }; void main() { uchar tmp,second; //second为当前的秒数 GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 second=0; //second将从0开始增加 while(1)//无限大循环 { tmp = second%10;//提取个位数 le573=1; //将锁存器设置为直通模式 } } SegPort=seg7cc[tmp]; //将‘second’个位的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(500); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 tmp = second/10;//提取十位数 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[tmp]; //将‘second’十位的编码送出 ShiWei=0;//再显示十位,将个位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(500); //延时500毫秒,连同前面延时的500毫秒,刚好是1秒 GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 if(second<59) //second 增加到59后,将重新从0开始增加 second++; else second=0; 程序代码说明 1.让second就在0~59之间反复变化。代码如下: if(second<59) //second增加到59后,将重新从0开始增加 second++; else second=0; 2.从second中拆分出它的个位和十位。 提取个位:tmp=second%10;//例如second=45,second%10=5 提取十位:tmp=second/10;//例如second=45,second/10=4 3. 我们让数码管的个位显示second的个位,数码管的十位显示second的十位。 SegPort=seg7cc[tmp]; //将‘second’个位的编码送出 SegPort=seg7cc[tmp]; //将‘second’十位的编码送出 4. second每隔一秒钟加1 我们使用两次DelayMs(500),则总的延时时间就是1秒 实际代码执行的效果 我们看到,实际代码执行的效果其实不太理想,秒数的显示不是同时显示出来的,问题出在 哪里呢? 其实,问题出在second的增加和数码管扫描的速度要求不一致。 second增加的速度要求很慢,1秒钟增加一次,而数码管扫描要求速度很快,几毫秒就要动 态显示一遍。而我们在一个程序序列中是无法同时满足这个快速和慢速两个要求的。 5.6 多位数码管显示秒数据改进版(定时器的使用) 5.6.1实现原理分析 1.如果我们能使用一个方法使second值1秒钟增加一次,同时能让数码管很快的动态显示, 那么就能解决我们的问题了。使用什么方法呢,答案就是使用定时器。定时器可以实现在一 定的时间间隔重复执行某个任务。例如,我们想每隔1秒执行一次second值加1的操作, 我们就应该使用定时器。51单片机内部有两个定时器(52有三个),即T0和T1。 2.我们在主程序循环中做快速的数码管动态扫描,同时在定时器中断中做second每秒加1 的操作就可以了。 知识点:定时器中断 什么是中断呢?讲个比较通俗的例子:比如你正在家中看电视,突然电话响了,你的第一反 应是什么?是不是先跑过去接电话!接完电话后,继续看电视。这就是个中断的例子,电话 打断了你看电视这件事(所以叫做中断源),你跑过去就是响应中断,接电话就是中断的处 理!接完电话后,接续看电视,即恢复中断!但是这个好象和单片机没什么联系呀?类比一 下:比如单片机正在执行某个任务(比如数码管的显示),突然要有任务,要求单片机响应 (比如1秒时间到了,second要加1),单片机就会应答响应,去执行该任务(即中断处理), 原来的任务(数码管的显示)就会暂停。等执行完中断任务后,继续执行原来的任务(继续 数码管的显示)。由于中断处理很快,因此你实际是感觉不到数码管的显示曾经暂停过。 5.6.2 硬件电路的设计 不变。 5.6.3程序的设计 1.新建一个keil项目,并命名为“SEG7_4”并添加一个名为“main.c”的源代码文件,然 后键入如下代码。如代码5.4所示。 //代码5.4 #include #include "commmon.h" //包含#define uchar unsigned char等定义 #include "function.h" //包含DelayMs函数 #define SegPort P0 //定义数码管连接的端口 sbit GeWei=P1^1; //定义数码管个位的位选线 sbit ShiWei=P1^0; //定义数码管十位的位选线 sbit le573=P1^2; //定义573LE引脚的位选线 //用一个数组来定义字符0~f共阴极数码管编码 unsigned char code seg7cc[]= { 0x3f,0x06,0x5b,0x4f, //0~3 0x66,0x6d,0x7d,0x07, //4~7 0x7f,0x6f,0x77,0x7c, //8~b 0x39,0x5e,0x79,0x71 //c~f }; uchar tmp,second; //定义一个全局变量,second为当前的秒数 uchar count; //定义一个全局变量,count为定时器中断执行的次数 void main() { //************定时器T0的初始化************** TMOD=0x01; //设置定时器T0工作在方式1 TH0=(65536-50000)/256; TL0=(65536-50000)%256;//设置定时器中断时间间隔为50毫秒 EA=1; //开总中断 ET0=1; //开定时器T0中断 TR0=1; //定时器开始运行 //******************************************** GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 second=0; //second将从0开始增加 count=0; //count将从0开始增加 while(1)//无限大循环 { tmp = second%10;//提取个位数 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[tmp]; //将‘second’个位的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 tmp = second/10;//提取十位数 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[tmp]; //将‘second’十位的编码送出 ShiWei=0;//再显示十位,将个位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 } } //定时器T0的中断处理函数,本例中,该中断函数每隔50毫秒执行一次,则执行20次后, 累积的时间为1秒 void T0_Timer() interrupt 1 { TH0=(65536-50000)/256; TL0=(65536-50000)%256; //因为方式1中,TH和TL值会清零,因此需要重新设置定 时器中断时间间隔为50毫秒 count++; //中断执行的次数加1 if(count==20) //注意,中断20次后,累计的时间为50*20=1000毫秒,即一秒, 此时,second该加1了 { count=0; //先将count重新置零 //**********每秒钟second加1************************ if(second<59) //second 增加到59后,将重新从0开始增加 second++; else second=0; //************************************************* } } 知识点:中断处理函数的格式 C51的中断处理函数的格式如下: void 函数名() interrupt 中断号 { } 例如 void T0_Timer() interrupt 1 { } 其中T0_Timer是函数名(可以自己取名) interrupt 是C51的关键字,表示该函数是中断处理函数 中断号表示该中断的种类。52系列 的单片机共有6个中断源,分别为:外中断INT0(中 断号0) 、定时器T0中断(中断号1)、外中断INT1(中断号2)、定时器T1中断(中断 号3)、串口中断(中断号4)、定时器T2中断(中断号5)。 又如 void T1_Timer() interrupt 3 { } 表示该函数是处理定时器T1的中断处理函数,也即当T1发生中断时,会进入该函数来处 理中断。 知识点:定时器的初始化 (1)定时器的工作方式 51的定时器有4种工作方式,本例中使用方式1,即16位定时器模式,要让定时器工作在 工作方式1,需要设置寄存器TMOD(高四位管T1,低四位管T0)的值,设置方式如下: TMOD=0x01; //设置定时器T0工作在方式1 TMOD=0x10; //设置定时器T1工作在方式1 TMOD=0x11; //设置定时器T0、T1都工作在方式1 注:TMOD的详细设置,请参看单片机课本。 (2)工作方式1模式下,我们通常通过如下代码设置其中断时间间隔 THx=(65536-N)/256; TLx=(65536-N)%256; //x=0表示设置定时器0,x=1表示设置定时器1 中断处理的代码 代码中的“N”值就是定时器的中断时间间隔。一般分为两种情况: 当51采用12MHz的晶振时,计时单位为1us,则N=t/1us,其中t为要设置的实际中断 间隔。例如我们想设置T0中断时间间隔为50ms,则N=50ms/1us=50000。 当51采用11.0592MHz的晶振时,计时单位为1.085us,则N=t/1.085us,其中t为要设 置的实际中断间隔。例如我们想设置T0中断时间间隔为50ms,则N=50ms/1.085us≈ 46083。 由于我们采用的是12MHz的晶振,因此,本例中,T0的中断时间间隔为50毫秒,即每隔 50毫秒,进入中断处理程序一次。 (3)设置中断允许 只用当单片机允许响应中断时,中断发生时,才会进入中断处理程序,利用这个特性,我们 可以打开和切断中断的响应。中断的响应允许分为两个级别:特定中断开关和总中断开关。 例如本例中 EA=1; //开总中断,当EA=0即关闭总中断时,所有类型的中断都不会被响应 ET0=1; //开定时器T0中断 其中,EA为总中断开关,EA=1为打开总中断,EA=0,表示关闭总中断,此时,系统发生 的所有类型的中断都不会被响应,因此,我们可以利用EA=0来切断所有的中断响应。ET0=1 为打开定时器T0中断开关,当ET0=0时,即使发生了定时器T0中断,程序也不会进入中 断处理程序,而是好像不知道有这个中断一样。利用ET0=0可以单独切断定时器T0的中断 响应。同理,ET1为定时器T1中断开关。 (4)启动定时器 TR0=1; //定时器开始运行 TR0为定时器T0的启动和停止开关,TR0=1表示定时器T0开始计时,好比把秒表开关按 下一样。TR0=0则是停止T0计时。同理,TR1为定时器T1的启动和停止开关。 程序代码说明 (1)重设定时器定时初始值 由于在工作方式1中,每次发生定时器中断后,定时器的计时初始值都会被清零,因此,为 了保证每次中断发生的时间间隔都是50毫秒,我们需要在中断处理函数中给定时器重新设 置计时初始值。即: TH0=(65536-50000)/256; TL0=(65536-50000)%256; //因为方式1中,TH和TL值会清零,因此需要重新设置定 时器中断时间间隔为50毫秒 (2)怎样设定计时间隔为1秒 由于单片机计数器字长的限制(16位),因此,我们通常能够设定的最长的定时间隔为几十 毫秒,那么怎样实现计时间隔为1秒的要求呢?我们通常采用多次中断来累积时间的方法。 原理如下: 一次计时间隔只有50ms,那么我累积计时20次后,再对秒数据second进行一次处理,则 可以变相实现计时间隔为1秒的要求了,代码如下: void T0_Timer() interrupt 1 { count++; //中断执行的次数加1 if(count==20) //中断20次后,累计的时间为50*20=1000毫秒,此时,second该加1了 { count=0; //先将count重新置零 …… } } 知识点:局部变量和全局变量 可能有同学已经注意到了,本例中,我们定义变量的位置在main函数的外边。 uchar tmp,second; //定义一个全局变量,second为当前的秒数 uchar count; //定义一个全局变量,count为定时器中断执行的次数 void main() { } 这种变量称为全局变量,它的作用范围为整个文件。如果我们把函数定义为如下形式: void main() { uchar tmp,second; //定义一个局部变量,second为当前的秒数 uchar count; //定义一个局部变量,count为定时器中断执行的次数 …… } void T0_Timer() interrupt 1 { //这里将不能使用second、count变量 } 由此,引出一个非常重要的话题,即变量的作用范围。 知识点:变量的作用范围 实际上,一个变量是有作用范围的,它的作用范围就是离他最近的一对花括号的范围,一旦 出了这对花括号的范围,那么,实际上这个变量就消失了。例如 for(i=0;i<15;i++) { for(j=0;j<15;j++) { uchar tmp; }//变量tmp的作用范围为这对花括号之间 } 因此,如果你要想定义一个所有函数都能调用的变量,那么你需要把它定义称为一个全局变 量,即它被定义在main函数外边,不被任何一对花括号所包含。 tmp=0;//此句有错,实际上此时tmp变量已经消失了。
2024年5月28日发(作者:明丰)
第五节 数码管的使用
5.1 数码管简介
同学们!相信你的流水灯也做的不错了吧,现在能玩出几种花样了?但是工程师们设计
这么一个单片机,并不是只为了让它做流水灯的,那样也太浪费点了吧 ... ^_^ 。
数码管的一种是半导体发光器件,7段LED数码管是利用7个LED(发光二极管)外
加一个小数点的LED组合而成的显示设备,可以显示0~9等10个数字和小数点,使用非常
广泛,数码管可以分为一位和多位它的外观如图5-1所示。
图5-1
5.2 数码管的显示原理
数码管可以分为共阳极与共阴极两种,共阳极就是把所有LED的阳极连接到共同接点
com,使用时com接正5伏电源,而每个LED的阴极分别为a、b、c、d、e、f、g及dp(小
数点);共阴极则是把所有LED的阴极连接到共同接点com,使用时com要将其接地。而
每个LED的阳极分别为a、b、c、d、e、f、g及dp(小数点),8个LED的分布方式如图
5-2所示。图中的8个LED分别与上面那个图中的A~DP各段相对应,通过控制各个LED
的亮灭来显示数字。
那么,实际的数码管的引脚是怎样排列的呢?对于单个数码管来说,从它的正面看进去,
左下角那个脚为1脚,以逆时针方向依次为1~10脚,左上角那个脚便是10脚了,上面两个
图中的数字分别与这10个管脚一一对应。注意,3脚和8脚是连通的,这两个都是公共脚。
它对应的引脚分布为图5-3所示。
图5-2 图5-3
数码管的8段,对应一个字节的8位,a对应最低位,dp(小数点)对应最高位。所以
如果想让数码管显示数字0,那么共阴数码管的字符编码为00111111,即0x3f;共阳数码
管的字符编码为11000000,即0xc0。可以看出两个编码的各位正好相反。如图5-4所示。
图5-4
那么,一位数码管要显示字符0~F,则对应的编码如表2所示。
一个八段数码管称为一位,多个数码管并列在一起可构成多位数码管,它们的段选线(即
a,b,c,d,e,f,g,dp)连在一起,而各自的公共端称为位选线。显示时,都从段选线送入字符编码,
而选中哪个位选线,那个数码管便会被点亮。
5.3 一位数码管的显示
请实现:让一位数码管依次显示字符0~F,每个字符显示1秒,如此反复。
一般情况下,为了计算或取码的方便,我们把a-dp依次接到单片机某个口上的Px.0--Px.7
上。x表示0,1,2,3其中的一个。这样我们只要给某个口,赋一个值,则相应的LED段
就被点亮,但是在硬件连接上要注意了:单片机可能不能直接驱动LED,所以我们可以通
过控制三级管的导通或截止,或者使用共阳极数码管(以灌电流的方式)、或者使用锁存器
来驱动。来控制LED的亮与灭!
5.3.1硬件的选择与仿真电路的设计
1.打开Proteus,选择“File/NewDesign”菜单选项,新建一个“设计项目”。并将项目保
存为“SEG7_1”。
2.选择“P”按钮或菜单“Library/Pick Divice/Symbol… P”菜单,从“元件库”中选取
元件。依次添加其他元件。其名称和位置见下表。
元件名称
AT89C52
Category Sub-Category Results
AT89C52 Microprocessor ICs 8051 Family
7SEG-MPX1-CA(注1) Optoelectronics
RX8(注2)
RESPACK-8(注3)
Resistors
Resistors
7-Segment Displays 7SEG-MPX1-CA
Resistor Packs
Resistor Packs
RX8
RESPACK-8
注1:7SEG表示7段数码管(Proteus还提供了14段和16段数码管)
MPX1表示1位(Proteus还提供了2位、4位、6位和8位数码管)
CA表示共阳极(CC表示共阴极)
注2:RX8表示电阻排,它实际相当于8个电阻并排摆放在一个容器内。在这里是作为限流
电阻来使用的。
注3:RESPACK-8表示电阻排,它实际相当于8个电阻并排摆放在一个容器内,但是这8个
电阻的一段是连接在一起作为公共端的。在这里是作为P0口的上拉电阻来使用的。
依次从备选元件库中摆放器件,连线,画出仿真电路图,如图5-5所示。
图5-5
注意:在Proteus中,实际上,51单片机是不需要晶振、复位电路和电源就可以仿真的,因
此,为了方便我们教学,以后,我们将不再画上述51的外围电路。
5.3.2程序的设计
1.新建一个keil项目,并命名为“SEG7_1”并添加一个名为“main.c”的源代码文件,然
后键入如下代码。如代码5.1所示。
//代码5.1
#include
#define SegPort P0 //定义数码管连接的端口
#define uchar unsigned char //宏定义 将unsigned char 替换为较为简单的uchar写法
#define uint unsigned int //宏定义 将unsigned int 替换为较为简单的uint写法
//用一个数组来定义字符0~f共阳极数码管编码
uchar code seg7ca[]={
0xc0,0xf9,0xa4,0xb0,//0~3
0x99,0x92,0x82,0xf8,//4~7
0x80,0x90,0x88,0x83,//8~b
0xc6,0xa1,0x86,0x8e //c~f
};
//延时函数ms毫秒
void DelayMs(uint ms)
{
uint i,j;
for(i=0;i { for(j=0;j<124;j++); } } void main() { while(1)//无限大循环 { //这段代码将字符0~F轮流显示一遍,每个字符显示1秒 uchar i=0; //从数组第一个元素开始显示 for(i=0;i<16;i++) { SegPort=seg7ca[i];//按次序显示字符0~F DelayMs(1000); //延时1秒 } } 知识点:数组seg7ca[]的定义 格式:数据类型 (数组位置) 数组名称[]={ 数组元素1,数组元素2,……}; 知识点:code关键字 “code”是C51所定义的关键字,他的含义是定义将代码放在ROM中,由于51的RAM很 小,因此,为了节约有限的RAM,我们通常会把一些不会变化的数据(比如数码管的编码、 参数表等等)放在ROM中,这样,我们就可以多空出一些RAM供程序来使用。 那么怎样把这些常量放在ROM中呢,即给定义语句前添加一个“code”关键字,告诉编译 器,这些常量要放在ROM中,如果没有添加“code”,那么这些编码就会放在RAM中。同 学们可以通过删除“code”关键字,重新build程序,然后查看程序占用RAM的大小,来 验证“code” 关键字的作用。 知识点:数组元素的访问 数组元素是通过他的序号来访问的。例如数组 seg7ca[]={ 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8 e}一共有16个元素,其中0xc0的序号为0(请记住,数组中的第一个元素序号为0),0xf9 的序号为1,……,0x8e的序号为15,以此类推。那么当我们想要使用0xc0时,我们可以 使用“seg7ca[0]”这种方式。因此,代码“SegPort=seg7ca[i]”;//按次序显示字符0~F 程序代码说明 uchar i=0; //从数组第一个元素开始显示 for(i=0;i<16;i++) { SegPort=seg7ca[i];//按次序显示字符0~F DelayMs(1000); //延时1秒 } 这段代码将字符0~F轮流显示一遍,每个字符显示1秒 当for循环开始时,i=0,因此,SegPort=seg7ca[0]; 由于SegPort是P0,seg7ca[0]=0xc0,因此,P0=0xc0,即P0口送出0xc0,由于0xc0是共 阳极数码管字符“0”的编码,因此,数码管显示字符“0”。 当for第二次循环时,i=1,因此,SegPort=seg7ca[1]; 即,P0=0xf9,即P0口送出0xf9,由于0xf9是共阳极数码管字符“1”的编码,因此,数 码管显示字符“1”。 以此类推,随着for循环,P0口送出数组seg7ca所定义的16个元素,从而在数码管上显示 对应的十六个字符。 5.4 多位数码管的显示 想必大家已经可以把0-F显示出来了吧!但是如果要你显示两位数,三位数呢?让我们 实现如下功能:让两位数码管显示数字“15”。或许,有的朋友会这么想:在P0口上接一 个数码管,再在P1口上接个数码管!但是,如果要显示4位、5位的数字呢?那岂不是一 块AT8951都接不过来!难到就不能接4位或5位以上的吗?肯定不是的! 说到这里,我们来讲讲数码管的显示方式,可分为两种:动态扫描和静态显示。上面我 们所说的即为静态显示。但是如果我们采用动态扫描显示,那么就可以解决上面的问题,动 态扫描是指每隔一段时间循环点亮每个数码管,每次只有一个数码管被点亮。细心的朋友会 问这样的问题:是让数码管一个一个亮,那还是不能控制数码管一起亮或灭嘛!怎么解决? 其实,人的眼睛有视觉暂留效应,黑夜里,拿着一支烟,在你面前快速的晃动,你会发 现什么样的现象?是不是原本不连续的点变成了一条看上去连续的曲线或者直线!再回过头 来,仔细想想我们的数码管!原理是一样的,只要我们快速的循环显示每个数码管,人的眼 睛看起来就好像是它们同时被点亮了,关键是速度。 比如点亮6位数码管,硬件连接可以这样解决:a--dp还是接至P0.0--P0.7上,还有6 个COM脚再接至另外口的P2.0--P2.5。P0口作段选(控制数字字符),P2口作位选(选通 哪个数码管被点亮)这样我们控制P0和P2口就可以控制6个数码管了。 5.4.1硬件的选择与仿真电路的设计 1.打开Proteus,选择“File/NewDesign”菜单选项,新建一个“设计项目”。并将项目保 存为“SEG7_2”。 2.选择“P”按钮或菜单“Library/Pick Divice/Symbol… P”菜单,从“元件库”中选取 元件。依次添加其他元件。其名称和位置见下表。 元件名称 AT89C52 Category Sub-Category Results AT89C52 7SEG-MPX2-CC RX8 RESPACK-8 74HC573 Microprocessor ICs 8051 Family 7-Segment Displays Resistor Packs Resistor Packs Flip-Flops & latches 7SEG-MPX2-CC(注1) Optoelectronics RX8 RESPACK-8 74HC573(注2) Resistors Resistors TTL 74HC series 注1:7SEG表示7段数码管、MPX2表示2位、CC表示共阴极(CC表示共极) 注2:74HC573是一个8位锁存器,在这里主要是提供锁存和驱动功能。 依次从备选元件库中摆放器件,连线,画出仿真电路图,如图5-6所示。 图5-6 1.连线的标号连接法:由于某些连线不太方便直接连接,或者由于美观原因,我们可以采用 标号连接。例如,我们想将51的P1.2引脚和74HC573的“LE”引脚相连,方法如下: (1)先将51的P1.2引脚和74HC573的“LE”引脚各用鼠标延长一段。 (2)点击Proteus侧边工具栏的“LBL(Wire Label Mode)”按钮,如图5-7所示。 Wire Label Mode 按钮 图5-7 图5-8 (3)将鼠标移动到51的P1.2引脚点一下,出现如下“Edit Wire Label”窗口,在“Edit Wire Label”窗口的“string”编辑框中填入该引脚的名称,比如“P12”,然后点击“OK”。如果 5-8所示。 (4)再将将鼠标移动到74HC573的“LE”引脚点一下,再在“Edit Wire Label”窗口的“string” 编辑框中填入该相同的名称,比如“P12”, 然后点击“OK”。 (5)这样,引脚标号相同的引脚就会被物理的连接在一起了。 2. 74HC573的使用 74HC573为8位3态非反转透明锁存器。数据从D0~D7引脚输入,从Q0~Q7引脚输出。 他的真值表如图5-9所示。 图5-9 OE为输出使能端,低电平有效,当OE为高电平时,D和Q不通,即所谓的输出高阻态。 LE为锁存端,当LE为高电平时,D和Q是直通的,当LE为低电平时,不管D端输入是 什么,Q端保持上一次的输出,即开启锁存功能。 3.电路说明 因为我们使用了两位数码管,因此,我们需要轮流点亮数码管的各位和十位。由于数码管是 共阴极的,因此,我们需要在P0口送出数码管共阴极的编码,同时,给要点亮数码管位选 线一个低电平,由于51的驱动能力不足以直接驱动数码管,因此,我们通过74HC573这个 锁存器来驱动,它的输出电流足够大,能够很好的驱动数码管发光。 5.4.2程序的设计 1.新建一个keil项目,并命名为“SEG7_2”并添加一个名为“main.c”的源代码文件,然 后键入如下代码。如代码5.2所示。 代码5.2 #include #define SegPort P0 //定义数码管连接的端口 #define uchar unsigned char //宏定义 将unsigned char 替换为较为简单的uchar写法 #define uint unsigned int //宏定义 将unsigned int 替换为较为简单的uint写法 //用一个数组来定义字符0~f共阴极数码管编码 uchar code seg7cc[]= { 0x3f,0x06,0x5b,0x4f, //0~3 0x66,0x6d,0x7d,0x07, //4~7 0x7f,0x6f,0x77,0x7c, //8~b 0x39,0x5e,0x79,0x71 //c~f }; sbit GeWei=P1^1; //定义数码管个位的位选线 sbit ShiWei=P1^0; //定义数码管十位的位选线 sbit le573=P1^2; //定义573LE引脚的位选线 //延时函数ms毫秒 void DelayMs(uint ms) { uint i,j; for(i=0;i { for(j=0;j<124;j++); } } void main() { GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 while(1)//无限大循环 { le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[5]; //将‘5’的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[1]; //将‘1’的编码送出 ShiWei=0;//再显示十位,将个位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); } } GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 程序代码说明 1.初始化 GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 通过这两行代码将数码管的个位和十位位选线都置为高电平,对于共阴极数码管来讲, 那么个位和十位都不能显示了,即先将两位都关闭。 2.我们现在想让个位数码管显示字符“5”,我们需要做的是: (1)LE引脚拉高,使74HC573进入直通模式。 (2)P0口送出字符“5”共阴极编码“0x6d” (3)GeWei (即P1.1)引脚送出低电平,选中数码管个位,此时数码管个位显示字符“5”。 (4) LE引脚拉低,使74HC573进入锁存模式,此时,74HC573的Q端始终保持输出“0x6d”, 则数码管个位始终保持显示字符“5” 把上述伪代码翻译为C51代码就是: le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[5]; //将‘5’的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 3.消隐操作 为了防止数码管闪烁,我们需要消隐操作,即将两位数码管都关闭,代码如下: DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 4.我们再用相同的方法让十位数码管显示字符“1”。 5.通过循环操作,使两位数码管快速轮流显示字符“1”和“5”,由于视觉暂留效应,我们 看到的就是字符“1”和“5”同时显示出来了。 5.5 多位数码管显示秒数据 让我们实现如下功能:让两位数码管依次显示数字“00~59”,每个数字显示1秒,显示 完59后,回到00,如此反复。 5.5.1实现原理分析 1.我们使用一个变量second表示当前的秒数,让second从0开始,每隔一秒钟加1,当second 增加到59时,让它回到0,重新开始。这样,second就在0~59之间反复变化。 2.从second中拆分出它的个位和十位呢?我们可以使用以下方法。 提取个位:tmp=second%10;//例如second=45,second%10=5 提取十位:tmp=second/10;//例如second=45,second/10=4 3. 我们让数码管的个位显示second的个位,数码管的十位显示second的十位。 5.5.2 硬件电路的设计 不变。 5.5.3程序的设计 1.新建一个keil项目,并命名为“SEG7_3”并添加一个名为“main.c”的源代码文件,然 后键入如下代码。如代码5.3所示。 //代码5.3 #include #include "commmon.h" //包含#define uchar unsigned char等定义 #include "function.h" //包含DelayMs函数 #define SegPort P0 //定义数码管连接的端口 sbit GeWei=P1^1; //定义数码管个位的位选线 sbit ShiWei=P1^0; //定义数码管十位的位选线 sbit le573=P1^2; //定义573LE引脚的位选线 //用一个数组来定义字符0~f共阴极数码管编码 unsigned char code seg7cc[]= { 0x3f,0x06,0x5b,0x4f, //0~3 0x66,0x6d,0x7d,0x07, //4~7 0x7f,0x6f,0x77,0x7c, //8~b 0x39,0x5e,0x79,0x71 //c~f }; void main() { uchar tmp,second; //second为当前的秒数 GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 second=0; //second将从0开始增加 while(1)//无限大循环 { tmp = second%10;//提取个位数 le573=1; //将锁存器设置为直通模式 } } SegPort=seg7cc[tmp]; //将‘second’个位的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(500); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 tmp = second/10;//提取十位数 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[tmp]; //将‘second’十位的编码送出 ShiWei=0;//再显示十位,将个位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(500); //延时500毫秒,连同前面延时的500毫秒,刚好是1秒 GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 if(second<59) //second 增加到59后,将重新从0开始增加 second++; else second=0; 程序代码说明 1.让second就在0~59之间反复变化。代码如下: if(second<59) //second增加到59后,将重新从0开始增加 second++; else second=0; 2.从second中拆分出它的个位和十位。 提取个位:tmp=second%10;//例如second=45,second%10=5 提取十位:tmp=second/10;//例如second=45,second/10=4 3. 我们让数码管的个位显示second的个位,数码管的十位显示second的十位。 SegPort=seg7cc[tmp]; //将‘second’个位的编码送出 SegPort=seg7cc[tmp]; //将‘second’十位的编码送出 4. second每隔一秒钟加1 我们使用两次DelayMs(500),则总的延时时间就是1秒 实际代码执行的效果 我们看到,实际代码执行的效果其实不太理想,秒数的显示不是同时显示出来的,问题出在 哪里呢? 其实,问题出在second的增加和数码管扫描的速度要求不一致。 second增加的速度要求很慢,1秒钟增加一次,而数码管扫描要求速度很快,几毫秒就要动 态显示一遍。而我们在一个程序序列中是无法同时满足这个快速和慢速两个要求的。 5.6 多位数码管显示秒数据改进版(定时器的使用) 5.6.1实现原理分析 1.如果我们能使用一个方法使second值1秒钟增加一次,同时能让数码管很快的动态显示, 那么就能解决我们的问题了。使用什么方法呢,答案就是使用定时器。定时器可以实现在一 定的时间间隔重复执行某个任务。例如,我们想每隔1秒执行一次second值加1的操作, 我们就应该使用定时器。51单片机内部有两个定时器(52有三个),即T0和T1。 2.我们在主程序循环中做快速的数码管动态扫描,同时在定时器中断中做second每秒加1 的操作就可以了。 知识点:定时器中断 什么是中断呢?讲个比较通俗的例子:比如你正在家中看电视,突然电话响了,你的第一反 应是什么?是不是先跑过去接电话!接完电话后,继续看电视。这就是个中断的例子,电话 打断了你看电视这件事(所以叫做中断源),你跑过去就是响应中断,接电话就是中断的处 理!接完电话后,接续看电视,即恢复中断!但是这个好象和单片机没什么联系呀?类比一 下:比如单片机正在执行某个任务(比如数码管的显示),突然要有任务,要求单片机响应 (比如1秒时间到了,second要加1),单片机就会应答响应,去执行该任务(即中断处理), 原来的任务(数码管的显示)就会暂停。等执行完中断任务后,继续执行原来的任务(继续 数码管的显示)。由于中断处理很快,因此你实际是感觉不到数码管的显示曾经暂停过。 5.6.2 硬件电路的设计 不变。 5.6.3程序的设计 1.新建一个keil项目,并命名为“SEG7_4”并添加一个名为“main.c”的源代码文件,然 后键入如下代码。如代码5.4所示。 //代码5.4 #include #include "commmon.h" //包含#define uchar unsigned char等定义 #include "function.h" //包含DelayMs函数 #define SegPort P0 //定义数码管连接的端口 sbit GeWei=P1^1; //定义数码管个位的位选线 sbit ShiWei=P1^0; //定义数码管十位的位选线 sbit le573=P1^2; //定义573LE引脚的位选线 //用一个数组来定义字符0~f共阴极数码管编码 unsigned char code seg7cc[]= { 0x3f,0x06,0x5b,0x4f, //0~3 0x66,0x6d,0x7d,0x07, //4~7 0x7f,0x6f,0x77,0x7c, //8~b 0x39,0x5e,0x79,0x71 //c~f }; uchar tmp,second; //定义一个全局变量,second为当前的秒数 uchar count; //定义一个全局变量,count为定时器中断执行的次数 void main() { //************定时器T0的初始化************** TMOD=0x01; //设置定时器T0工作在方式1 TH0=(65536-50000)/256; TL0=(65536-50000)%256;//设置定时器中断时间间隔为50毫秒 EA=1; //开总中断 ET0=1; //开定时器T0中断 TR0=1; //定时器开始运行 //******************************************** GeWei=1; //先将数码管个位显示关闭 ShiWei=1; //先将数码管十位显示关闭 second=0; //second将从0开始增加 count=0; //count将从0开始增加 while(1)//无限大循环 { tmp = second%10;//提取个位数 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[tmp]; //将‘second’个位的编码送出 GeWei=0; //先显示个位,将十位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 tmp = second/10;//提取十位数 le573=1; //将锁存器设置为直通模式 SegPort=seg7cc[tmp]; //将‘second’十位的编码送出 ShiWei=0;//再显示十位,将个位关闭 le573=0; //将锁存器设置为锁存模式 DelayMs(1); GeWei=1; ShiWei=1; //消隐操作,防止数码管闪烁 } } //定时器T0的中断处理函数,本例中,该中断函数每隔50毫秒执行一次,则执行20次后, 累积的时间为1秒 void T0_Timer() interrupt 1 { TH0=(65536-50000)/256; TL0=(65536-50000)%256; //因为方式1中,TH和TL值会清零,因此需要重新设置定 时器中断时间间隔为50毫秒 count++; //中断执行的次数加1 if(count==20) //注意,中断20次后,累计的时间为50*20=1000毫秒,即一秒, 此时,second该加1了 { count=0; //先将count重新置零 //**********每秒钟second加1************************ if(second<59) //second 增加到59后,将重新从0开始增加 second++; else second=0; //************************************************* } } 知识点:中断处理函数的格式 C51的中断处理函数的格式如下: void 函数名() interrupt 中断号 { } 例如 void T0_Timer() interrupt 1 { } 其中T0_Timer是函数名(可以自己取名) interrupt 是C51的关键字,表示该函数是中断处理函数 中断号表示该中断的种类。52系列 的单片机共有6个中断源,分别为:外中断INT0(中 断号0) 、定时器T0中断(中断号1)、外中断INT1(中断号2)、定时器T1中断(中断 号3)、串口中断(中断号4)、定时器T2中断(中断号5)。 又如 void T1_Timer() interrupt 3 { } 表示该函数是处理定时器T1的中断处理函数,也即当T1发生中断时,会进入该函数来处 理中断。 知识点:定时器的初始化 (1)定时器的工作方式 51的定时器有4种工作方式,本例中使用方式1,即16位定时器模式,要让定时器工作在 工作方式1,需要设置寄存器TMOD(高四位管T1,低四位管T0)的值,设置方式如下: TMOD=0x01; //设置定时器T0工作在方式1 TMOD=0x10; //设置定时器T1工作在方式1 TMOD=0x11; //设置定时器T0、T1都工作在方式1 注:TMOD的详细设置,请参看单片机课本。 (2)工作方式1模式下,我们通常通过如下代码设置其中断时间间隔 THx=(65536-N)/256; TLx=(65536-N)%256; //x=0表示设置定时器0,x=1表示设置定时器1 中断处理的代码 代码中的“N”值就是定时器的中断时间间隔。一般分为两种情况: 当51采用12MHz的晶振时,计时单位为1us,则N=t/1us,其中t为要设置的实际中断 间隔。例如我们想设置T0中断时间间隔为50ms,则N=50ms/1us=50000。 当51采用11.0592MHz的晶振时,计时单位为1.085us,则N=t/1.085us,其中t为要设 置的实际中断间隔。例如我们想设置T0中断时间间隔为50ms,则N=50ms/1.085us≈ 46083。 由于我们采用的是12MHz的晶振,因此,本例中,T0的中断时间间隔为50毫秒,即每隔 50毫秒,进入中断处理程序一次。 (3)设置中断允许 只用当单片机允许响应中断时,中断发生时,才会进入中断处理程序,利用这个特性,我们 可以打开和切断中断的响应。中断的响应允许分为两个级别:特定中断开关和总中断开关。 例如本例中 EA=1; //开总中断,当EA=0即关闭总中断时,所有类型的中断都不会被响应 ET0=1; //开定时器T0中断 其中,EA为总中断开关,EA=1为打开总中断,EA=0,表示关闭总中断,此时,系统发生 的所有类型的中断都不会被响应,因此,我们可以利用EA=0来切断所有的中断响应。ET0=1 为打开定时器T0中断开关,当ET0=0时,即使发生了定时器T0中断,程序也不会进入中 断处理程序,而是好像不知道有这个中断一样。利用ET0=0可以单独切断定时器T0的中断 响应。同理,ET1为定时器T1中断开关。 (4)启动定时器 TR0=1; //定时器开始运行 TR0为定时器T0的启动和停止开关,TR0=1表示定时器T0开始计时,好比把秒表开关按 下一样。TR0=0则是停止T0计时。同理,TR1为定时器T1的启动和停止开关。 程序代码说明 (1)重设定时器定时初始值 由于在工作方式1中,每次发生定时器中断后,定时器的计时初始值都会被清零,因此,为 了保证每次中断发生的时间间隔都是50毫秒,我们需要在中断处理函数中给定时器重新设 置计时初始值。即: TH0=(65536-50000)/256; TL0=(65536-50000)%256; //因为方式1中,TH和TL值会清零,因此需要重新设置定 时器中断时间间隔为50毫秒 (2)怎样设定计时间隔为1秒 由于单片机计数器字长的限制(16位),因此,我们通常能够设定的最长的定时间隔为几十 毫秒,那么怎样实现计时间隔为1秒的要求呢?我们通常采用多次中断来累积时间的方法。 原理如下: 一次计时间隔只有50ms,那么我累积计时20次后,再对秒数据second进行一次处理,则 可以变相实现计时间隔为1秒的要求了,代码如下: void T0_Timer() interrupt 1 { count++; //中断执行的次数加1 if(count==20) //中断20次后,累计的时间为50*20=1000毫秒,此时,second该加1了 { count=0; //先将count重新置零 …… } } 知识点:局部变量和全局变量 可能有同学已经注意到了,本例中,我们定义变量的位置在main函数的外边。 uchar tmp,second; //定义一个全局变量,second为当前的秒数 uchar count; //定义一个全局变量,count为定时器中断执行的次数 void main() { } 这种变量称为全局变量,它的作用范围为整个文件。如果我们把函数定义为如下形式: void main() { uchar tmp,second; //定义一个局部变量,second为当前的秒数 uchar count; //定义一个局部变量,count为定时器中断执行的次数 …… } void T0_Timer() interrupt 1 { //这里将不能使用second、count变量 } 由此,引出一个非常重要的话题,即变量的作用范围。 知识点:变量的作用范围 实际上,一个变量是有作用范围的,它的作用范围就是离他最近的一对花括号的范围,一旦 出了这对花括号的范围,那么,实际上这个变量就消失了。例如 for(i=0;i<15;i++) { for(j=0;j<15;j++) { uchar tmp; }//变量tmp的作用范围为这对花括号之间 } 因此,如果你要想定义一个所有函数都能调用的变量,那么你需要把它定义称为一个全局变 量,即它被定义在main函数外边,不被任何一对花括号所包含。 tmp=0;//此句有错,实际上此时tmp变量已经消失了。