教学视频 第5章 定时器与数码管基础 通过前面的讲解,读者会发现,自己逐渐进入比较实质性的学习了,需要记住的内容也更多了,个别地方可能会感觉吃力。但是读者不要担心,要有信心。这跟小孩学走路一样,刚开始走得不太稳,没关系,多走几步多练练。看教材的时候要专心,一遍看不懂,思考一下,再回头看第二遍和第三遍,没准一下就明白了。如果三遍还看不明白,那就把不懂的问题放一放,继续往下学,然后再回头看一遍,也可以到相关群或者论坛中多咨询他人,多讨论,有些不明白的问题可能就茅塞顿开了。 5.1逻辑电路与逻辑运算 在数字电路中经常会遇到逻辑电路,而在C语言中则经常用到逻辑运算。二者在原理上是相互关联的,在这里就先简单介绍一下,随着学习的深入,再慢慢加深理解。 首先,在“逻辑”概念范畴内,存在“真”和“假”两个逻辑值,而将其对应到数字电路或C语言中,就变成了“非0值”和“0值”两个值,即逻辑上的“假”就是数字电路或C语言中的“0”值,而逻辑“真”就是其他一切“非0”值。 然后,具体分析几个主要的逻辑运算符。假定有两个字节变量: A和B,二者进行某种逻辑运算后的结果为F。 以下逻辑运算符都是按照变量整体值进行运算的。 (1) &&(逻辑与)。F=A && B,当A、B的值都为真(即非0值,下同)时,其运算结果F为真(具体数值为1,下同); 当A、B值中任意一个为假(即0,下同)时,结果F为假(具体数值为0,下同)。 (2) ||(逻辑或)。F=A || B,当A、B值中任意一个为真时,其运算结果F为真; 当A、B值都为假时,结果F为假。 (3) !(逻辑非),F=!A,当A值为假时,其运算结果F为真; 当A值为真时,结果F为假。 以下逻辑运算符都是按照变量内的每一个位进行运算的,通常也称为位运算符: (4) &(按位与),F=A & B,将A、B两字节中的每一位都进行与运算,再将得到的每一位结果组合为总结果F,例如A=0b11001100,B=0b11110000,则结果F就等于0b11000000。 (5) |(按位或),F=A | B,将A、B两字节中的每一位都进行或运算,再将得到的每一位结果组合为总结果F,例如A=0b11001100,B=0b11110000,则结果F就等于0b11111100。 (6) ~(按位取反),F=~A,将A字节内的每一位进行非运算(就是取反),再将得到的每一位结果组合为总结果F,例如A=0b11001100,则结果F就等于0b00110011; 这个运算符在前面的流水灯实验里已经用过了,现在再回头看看,是不是清楚多了。 (7) ^(按位异或),异或的意思是,如果运算双方的值不同(相异)则结果为真,双方值相同,则结果为假。在C语言里没有按变量整体值进行的异或运算,所以仅以按位异或为例,F=A^B,A=0b11001100,B=0b11110000,则结果F就等于0b00111100。 看资料或芯片手册的时候,会经常遇到一些电路符号,表51列出了数字电路中常用的 表51逻辑电路符号 逻辑电路符号,知道这些符号有利于理解器件的逻辑结构,尤其要重点认识表51中的国外流行图形符号。在这里先简单看一下,如果日后遇到了可以进行查阅。 5.2定时器 定时器是单片机系统的一个重点,但并不是难点,读者一定要完全理解并且熟练掌握定时器的应用。 5.2.1初步认识定时器 1. 时钟周期 时钟周期是时序中最小的时间单位,具体计算的方法为 时钟周期=1时钟源频率 KST51单片机开发板上用的晶振是11.0592MHz,那么对于这个单片机系统来说,时钟周期=1/11059200s。 2. 机器周期 机器周期是单片机完成一个操作的最短时间。机器周期主要针对汇编语言而言,在汇编语言下程序的每一条语句执行所使用的时间都是机器周期的整数倍,而且语句占用的时间是可以计算出来的,而C语言一条语句的时间是不确定的,受到诸多因素的影响。51单片机系列在其标准架构下,一个机器周期是12个时钟周期,也就是12/11059200s。现在有不少增强型的51单片机,其速度都比较快,有的1个机器周期等于4个时钟周期,有的1个机器周期就等于1个时钟周期,也就是说大体上其速度可以达到标准51架构的3倍或12倍。因为本书是讲标准的51单片机,所以后边的内容如果遇到这个概念,全部是指12个时钟周期。 这两个概念了解即可,下面来讲讲重头戏——定时器和计数器。定时器和计数器是单片机内部的同一个模块,通过配置SFR(特殊功能寄存器)可以实现两种不同的功能,大多数情况下使用的是定时器功能,因此下面主要讲定时器功能,计数器功能读者了解一下即可。 顾名思义,定时器就是用来进行定时的。定时器内部有一个寄存器,让它开始计数后,这个寄存器的值每经过一个机器周期就会自动加1,因此,可以把机器周期理解为定时器的计数周期。就像钟表,每经过1s,数字自动加1,而这个定时器就是每过一个机器周期的时间,也就是12/11059200s,数字自动加1。还有一个特别注意的地方,就是钟表是加到60后,秒数就自动变成0了,这种情况在单片机或计算机里称为溢出。那定时器加到多少才会溢出呢?后面会讲到定时器有多种工作模式,分别使用不同的位宽(指使用多少个二进制位),假如是16位的定时器,也就是两字节,计数的最大值就是65535,那么加到65535后,再加1就算溢出,如果有其他位数,道理是一样的,对于51单片机来说,溢出后,这个值会直接变成0。从某个初始值开始,经过确定的时间后溢出,这个过程就是定时的含义。 5.2.2定时器相关的寄存器 标准的51单片机内部有T0和T1两个定时器,T就是Timer的缩写,现在很多51系列单片机还会增加额外的定时器,在这里先讲定时器0(T0)和定时器1(T1)。前边提到过,对于单片机的每一个功能模块,都是由它的SFR,也就是特殊功能寄存器来控制。与定时器有关的特殊功能寄存器,有以下几个,读者不需要记忆这些寄存器的名称和作用,只要大概知道就行,用的时候,随时可以查手册,找到每个寄存器的名称和每个寄存器所起到的作用。 表52是定时值存储寄存器,是存储定时器的计数值的。TH0/TL0用于T0,TH1/TL1用于T1。 表52定时值存储寄存器 名称描述SFR地址复位值 TH0定时器0高字节0x8C0x00 TL0定时器0低字节0x8A0x00 TH1定时器1高字节0x8D0x00 TL1定时器1低字节0x8B0x00 表53是定时器控制寄存器TCON的位分配,表54则是对每一位的具体含义的描述。 表53TCON的位分配(地址0x88、可位寻址) 位76543210 符号TF1TR1TF0TR0IE1IT1IE0IT0 复位值00000000 表54 TCON的位描述 位符号描述 7TF1定时器1溢出标志。一旦定时器1发生溢出时硬件置1。清0有两种方式: 软件清0或者进入定时器中断时硬件清0 6TR1定时器1运行控制位。软件置位/清0来进行启动/停止定时器 5TF0定时器0溢出标志。一旦定时器0发生溢出时硬件置1。清0有两种方式: 软件清0或者进入定时器中断时硬件清0 4TR0定时器0运行控制位。软件置位/清0来进行启动/停止定时器 3IE1 2IT1 1IE0 0IT0外部中断部分,与定时器无关,暂且不看 读者注意在表54的描述中,只要写到硬件置1或者清0,就是指一旦符合条件,单片机将自动完成动作,只要写软件置1或者清0,就是指必须用程序去完成这个动作,后续遇到此类描述就不再另做说明了。 对于TCON这个SFR,其中有TF1、TR1、TF0、TR0这4位需要理解清楚,它们分别对应于T1和T0,以定时器1为例讲解,那么定时器0同理。先看TR1,当程序中写TR1=1以后,定时器值就会每经过一个机器周期自动加1,当程序中写TR1=0以后,定时器就会停止加1,其值会保持不变化。TF1是一个标志位,它的作用是告诉大家定时器溢出了。比如定时器设置成16位模式,那么每经过一个机器周期,TL1加1一次,当TL1加到255后,再加1,TL1变成0,TH1会加1一次,如此一直加到TH1和TL1都是255(TH1和TL1组成的16位整型数为65535)以后,再加1一次,就会溢出了,TH1和TL1同时都变为0,只要一溢出,TF1马上自动变成1,告诉大家定时器溢出了,仅仅是提供给读者一个信号,让读者知道定时器溢出了,它不会对定时器是否继续运行产生任何影响。 本节开头就提到了定时器有多种工作模式,工作模式的选择就由定时器模式寄存器TMOD来控制,TMOD的位分配和描述如表55和表56所示,TMOD的位功能如表57所示。 表55TMOD的位分配(地址0x89、不可位寻址) 位76543210 符号GATE (T1)C/T (T1)M1 (T1)M0 (T1)GATE (T0)C/T (T0)M1 (T0)M0 (T0) 复位值00000000 表56TMOD的位描述 符号描述 T1/T0在表55中,标T1的表示控制定时器1的位,标T0的表示控制定时器0的位 GATE该位被置1时为门控位。仅当INTx引脚为高并且TRx控制位被置1时使能定时器x,定时器开始计时,当该位被清0时,只要TRx位被置1,定时器x就使能开始计时,不受到单片机引脚INTx外部信号的干扰,常用来测量外部信号脉冲宽度。这是定时器一个额外功能,暂不介绍 C/T定时器或计数器选择位。该位被清0时用作定时器功能(内部系统时钟),被置1用作计数器功能 表57TMOD的M1/M0工作模式 M1M0工作模式描述 000兼容8048单片机的13位定时器,THn的8位和TLn的5位组成一个13位定时器 011THn和TLn组成一个16位的定时器 1028位自动重装模式,定时器溢出后THn重装到TLn中 113禁用定时器1,定时器0变成两个8位定时器 可能读者已经注意到了,表53的TCON标注了“可位寻址”,而表55的TMOD标注的是“不可位寻址”。意思就是说: 比如TCON有一个位TR1,可以在程序中直接进行TR1=1这样的操作,但对TMOD里的位,比如(T1)M1,进行M1=1这样的操作就是错误的。要操作就必须一次操作整个字节,也就是必须一次性对TMOD所有位操作,不能对其中某一位单独进行操作。那么能不能只修改其中的一位而不影响其他位的值呢?当然可以,在后续内容中读者就会学到方法,现在就先不关心它了。 表57列出的就是定时器的4种工作模式,其中模式0是为了兼容老的8048系列单片机而设计的,现在的51单片机几乎不会用到这种模式,而模式3根据应用经验,它的功能用模式2完全可以取代,所以基本上也是不会用到的,那么就重点来学习模式1和模式2。 模式1是THn和TLn组成了一个16位的定时器,计数范围是0~65535,溢出后,只要不对THn和TLn重新赋值,则从0开始计数。模式2是8位自动重装载模式,只有TLn做加1计数,计数范围为0~255,THn的值不会变化,而会保持原来的值; TLn溢出后,TFn就直接置1了,并且THn原先的值直接赋给TLn,然后TLn从新赋值的这个数字开始计数。这个功能可以用来产生串口的通信波特率,讲串口的时候要用到。本节我们重点来学习模式1。为了加深大家理解定时器的原理,先来看一下模式1的电路示意图,如图51所示。 图51定时器/计数器模式1的电路示意图 一起来分析一下这个示意图,日后如果再遇到类似的图,就可以自己研究了。OSC表示时钟频率,因为1个机器周期等于12个时钟周期,所以d就等于12。下边GATE右侧的门是一个非门,再右侧是一个或门,再往右是一个与门,读者可以对照一下5.1节的内容。 从图51可以看出,下边部分电路是控制了上边部分电路。先来看是如何控制的(以定时器0为例)。 (1) TR0和下边或门的结果要进行与运算,TR0如果是0,与运算完了肯定是0,所以如果要让定时器工作,那么TR0就必须置1。 (2) 这里的与门结果要想得到1,那么前面的或门出来的结果必须也得是1才行。在GATE位为1的情况下,经过一个非门变成0,或门结果要想是1,那INT0即P3.2引脚必须是1的情况下,这个时候定时器才会工作,而INT0引脚是0的情况下,定时器不工作,这就是GATE位的作用。 (3) 当GATE位为0的时候,经过一个非门会变成1,那么不管INT0引脚是什么电平,经过或门后肯定都是1,定时器就会工作。 (4) 要想让定时器工作,就是自动加1,从图上看有两种方式。第一种方式是开关打到上边的箭头,就是C/T=0时,一个机器周期TL就会加1一次; 第二种方式是开关打到下边的箭头,即C/T =1时,T0引脚(即P3.4引脚)有一个脉冲,TL就加1一次,这就是计数器功能。 5.2.3定时器的应用 了解了定时器相关的寄存器,下面就来做一个定时器的程序,巩固前面学到的内容。下面的程序先使用定时器0,在使用定时器0的时候,需要以下几个步骤: 第1步: 设置特殊功能寄存器TMOD,配置好工作模式。 第2步: 设置计数寄存器TH0和TL0的初值。 第3步: 设置TCON,通过TR0置1来让定时器开始计数。 第4步: 判断TCON寄存器的TF0位,监测定时器溢出情况。 写程序之前,要先学会如何用定时器定时。晶振是11.0592MHz,时钟周期就是1/11059200,机器周期是12/11059200,假如要定时20ms(0.02s),要经过x个机器周期得到0.02s,下面来算一下x×12/11059200=0.02,得到x=18432。16位定时器的溢出值是65536(因65535再加1才是溢出),于是就可以这样操作,先给TH0和TL0一个初始值,让它们经过18432个机器周期后刚好达到65536,也就是溢出,溢出后可以通过检测TF0的值得知,刚好是0.02s。那么初值y=65536-18432=47104,转换成十六进制就是0xB800,也就是TH0=0xB8,TL0=0x00。 这样0.02s的定时就做出来了,细心的读者会发现,如果初值直接给一个0x0000,一直到65536溢出,定时器定时值最大也就是71ms左右,那么想定时更长时间怎么办呢?用小学学过的逻辑,倍数关系就可以解决此问题。 下面就用程序来实现这个功能,代码如下: #include <reg52.h> sbit LED = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; void main() { unsigned char cnt = 0;//定义一个计数变量,记录T0溢出次数 ENLED = 0;//使能U3,选择独立LED ADDR3 = 1; ADDR2 = 1; ADDR1 = 1; ADDR0 = 0; TMOD = 0x01; //设置T0为模式1 TH0= 0xB8; //为T0赋初值0xB800 TL0= 0x00; TR0= 1;//启动T0 while (1) { if (TF0 == 1) //判断T0是否溢出 { TF0 = 0; //T0溢出后,清0中断标志 TH0 = 0xB8; //并重新赋初值 TL0 = 0x00; cnt++; //计数值自加1 if (cnt >= 50) //判断T0溢出是否达到50次 { cnt = 0;//达到50次后计数值清0 LED = ~LED;//LED取反: 0-->1、1-->0 } } } } 程序中都写了注释,结合前几章学的内容,读者自己分析一下,不难理解。本程序实现的结果是开发板上最右边的小灯点亮一秒,熄灭一秒,也就是以0.5Hz的频率进行闪烁。 5.3数码管 LED小灯是一种简单的LED,只能通过亮和灭表达简单的信息。而本节要学习一种能表达更复杂信息的器件——LED数码管。 5.3.1数码管概述 数码管的原理图如图52所示。 这是比较常见的数码管原理图,开发板上一共有6个数码管。前边有了LED小灯的学习,数码管学习就会轻松得多了。从图52可以看出来,数码管共有a、b、c、d、e、f、g、dp这8个段,而实际上,这8个段每一段都是一个LED小灯,所以一个数码管就是由8个LED小灯组成的。数码管内部结构示意图如图53所示。 图52数码管的原理图 图53数码管内部结构示意图 数码管分为共阳和共阴两种,共阴数码管就是8只LED小灯的阴极是连接在一起的,阴极是公共端,由阳极控制单个小灯的亮灭。同理,共阳数码管就是阳极接在一起,读者可以认真研究图53。细心的读者会发现,图52的数码管上边有2个com,这就是数码管的公共端。为什么有2个呢,一方面是2个可以起到对称的效果,刚好是10个引脚,另一方面,公共端通过的电流较大,读者初中就学过,并联电路电流之和等于总电流,用2个com可以把公共电流平均到2个引脚上去,降低单条线路承受的电流。 从开发板的电路图上能看出来,所用的数码管都是共阳数码管,一共有6个,如图54所示。 6个数码管的com都是接到了正极上,当然,和LED小灯电路一样,也是由74HC138控制三极管的导通来控制整个数码管的使能。先来看最右边的DS1这个数码管,从原理图上可以看出,控制DS1的三极管是Q17,控制Q17的引脚是LEDS0,对应到74HC138上就是U3的Y0输出,如图55所示。 图54KST51数码管电路 图5574HC138控制图 现在的目的是让LEDS0引脚输出低电平,相信读者现在可以根据前边学过的知识独立地把ADDR0、ADDR1、ADDR2、ADDR3、ENLED这4个所需输入的值写出来了,现在读者不要偷懒,根据74HC138的手册去写一下,不需要你记住这些结论,但是只要遇到就写,锻炼过几次后,遇到同类芯片自己就知道如何去解决问题了。 数码管通常是用来显示数字的,开发板上有6个数码管,习惯上称为6位,那控制位选择的就是74HC138了。而数码管内部的8个LED小灯称为数码管的段,那么数码管的段选择(该段的亮灭)是通过P0口控制,经过74HC245驱动。 5.3.2数码管的真值表 现把数码管的8个段直接当成8个LED小灯来控制,那就是a、b、c、d、e、f、g、dp一共8个LED小灯。通过图52可以看出,如果点亮b和c这两个LED小灯,也就是数码管的b段和c段,其他的所有的段都熄灭,就可以让数码管显示出一个数字1,那么这个时候实际上P0的值就是0b11111001,十六进制就是0xF9。那么写一个程序,看一看数码管显示的效果,代码如下: #include <reg52.h> sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; void main() { ENLED = 0;//使能U3,选择数码管DS1 ADDR3 = 1; ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; P0 = 0xF9;//点亮数码管段b和c while (1); } 读者把这个程序编译一下,并下载到单片机中,就可以看到程序运行的结果是在最右侧的数码管上显示了一个数字1。 用同样的方法,可以把其他的数字字符都在数码管上显示出来,而根据数码管显示的数字字符对应给P0的赋值,形成数码管的真值表。电路图的数码管真值表如表58所示,注意,这个真值表中显示的数字都不带小数点。 表58数码管真值表 数字字符01234567 数值0xC00xF90xA40xB00x990x920x820xF8 数字字符89ABCDEF 数值0x800x900x880x830xC60xA10x860x8E 读者可以把上边那个用数码管显示数字1程序中的P0的赋值随便修改成表58真值表中的数值,看看显示的数字的效果。 5.3.3数码管的静态显示 在第3章学习了74HC138,了解到74HC138在同一时刻只能让一个输出口为低电平,也就是说在一个时刻内,只能使能一个数码管,并根据给出的P0值改变这个数码管的显示字符,可以将此理解为数码管的静态显示。 数码管静态显示是相对于动态显示而言的,静态显示对于一两个数码管还行,对于多个数码管,静态显示实现的意义就没有了。本节先用一个数码管的静态显示实现一个简单的秒表,为后面的动态显示打下基础。 先来介绍51单片机的关键字code。前边定义变量的时候,一般用到unsigned char或者unsigned int这两个关键字,用这两个关键字定义的变量都是放在单片机的RAM中,在程序中可以随意改变这些变量的值。但是还有一种数据,在程序中要使用,却不会改变它的值,定义这种数据时可以加一个code关键字修饰一下,这个数据就会存储到程序空间Flash中,这样可以大大节省单片机RAM的使用量,毕竟单片机RAM空间比较小,而程序空间则大得多。那么现在要使用的数码管真值表,只会使用它们的值,而不需要改变它们,就可以用code关键字把它放入Flash中了,具体程序代码如下。 #include <reg52.h> sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; //用数组存储数码管的真值表,数组将在第6章详细介绍 unsigned char code LedChar[] = { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; void main() { unsigned char cnt = 0;//记录T0中断次数 unsigned char sec = 0;//记录经过的秒数 ENLED = 0;//使能U3,选择数码管DS1 ADDR3 = 1; ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; TMOD = 0x01;//设置T0为模式1 TH0= 0xB8;//为T0赋初值0xB800 TL0= 0x00; TR0= 1; //启动T0 while (1) { if (TF0 == 1)//判断T0是否溢出 { TF0 = 0;//T0溢出后,清0中断标志 TH0 = 0xB8;//并重新赋初值 TL0 = 0x00; cnt++; //计数值自加1 if (cnt >= 50) //判断T0溢出是否达到50次 { cnt = 0;//达到50次后计数值清0 P0 = LedChar[sec];//当前秒数对应的真值表中的值送到P0口 sec++;//秒数记录自加1 if (sec >= 16)//当秒数超过0x0F(15)后,重新从0开始 { sec = 0; } } } } } 5.4习题 1. 熟练掌握单片机定时器的原理和应用方法。 2. 通过研究定时器模式1的示意图,自己打开STC89C52RC数据手册的定时器部分,独立研究模式0、模式2和模式3的示意图,锻炼研究示意图的能力。 3. 使用定时器实现左右移动的流水灯程序。 4. 了解数码管的原理,掌握数码管真值表的计算方法。 5. 编程实现数码管静态显示秒表的倒计时。