简明理解从晶振原理到51单片机精确延时
鄙人初学51,很多地方不是太懂,只是根据自己理解所写,大佬勿喷。图片来源网络,如有侵权,请联系删除。
时钟信号的作用及产生原理
在使用51单片机时,以前只会用while()等循环语句进行粗略的延时,但在很多时候这种方法不太nice。最近刚学习了51单片机的精确延时,来做一个笔记。好了,闲言少叙。
要想实现精确延时,首先我们要有一个准确的振荡信号,即时钟源。我们知道一个单片机要想正常运行程序就需要①电源电路,②复位电路,③时钟电路。对于电源电路,供电嘛,没什么可说的。复位电路则可用于重置单片机,就像无线路由器后面的重置键一样。如果说我们的单片机收到外部干扰等一系列可怕的情况导致程序跑飞(错乱了),我们就可以通过复位电路重置单片机,使其重新从头运行程序。那么时钟电路又是干什么用的?
其实时钟电路才是单片机的核心,它为单片机规定了时间基准。有的初学者可能会有疑问,为什么要规定时间基准?打个比方,我们知道单片机只能识别二进制,即0和1。0就代表关,1就代表开。这是一种最简单的逻辑。在电路中我们用高电压代表1,用低电压代表0。因此我们就可以用电压的高低来在电路中表示简单的逻辑。在这个前提下,如果我们要给计算机发送一串数据,如10101010,那很好实现,我们只需要检测电压的高低变化就可以。然而如果我们发送的数据是11110000,鬼知道你到底发了几个0和1?所以我们需要一个时钟步调去给它打一个拍子,隔一段相同的时间就去观察一下现在是高还是低。这就是为什么要引入时钟电路的原因。
那具体怎么实现呢?先来科普一个小知识:(压电效应——某些电介质在沿一定方向上受到外力的作用而变形时,其内部会产生极化现象,同时在它的两个相对表面上出现正负相反的电荷。当外力去掉后,它又会恢复到.原来的状态。)简单来说,对于某些物质,你在它的两个面上施加压力,在这两个面上就会产生电压(就像打火机里面的打火装置一样)。反之,如果你在这两个面上施加一定的电压,它就会产生机械形变(有的玩具里面的喇叭,一个小片,叫做压电陶瓷)。
压电效应
压电陶瓷
我们在了解了压电效应以后,就不难想到,如果对于某些物质,给他一个电压使其发生弹性机械形变,然后它在其自身的弹性作用下就会恢复原来的状态,在它的形状回到原来状态的形变过程中,就会产生一个电压,等他回到原来的状态,则又接触到了我们最初施加给它的电压,它有会发生相同的弹性机械形变。如此周而复始。当然,这个过程是非常快的,而其在恢复自身原来状态过程中产生的电压信号,就形成了我们最初检测到的振荡信号。我们一般会利用石英来作为产生时钟信号的元件,这种元件我们叫做晶体振荡器,简称晶振。
晶振
时钟信号怎么用?
我们在了解了时钟信号的产生原理后,可能会有疑问,时钟信号怎么去利用?下面我们来讨论这一问题。首先我们要明确的是晶振的频率是特别高的,常用的有8M,11.0592M,12M等频率。我们要知道,对于一个单片机来说如果工作太快,就可能造成运行不稳定,如果一个时钟的频率太高,那么我们的单片机要与之同步,就会使单片机一直检测时钟信号而疲于奔命,所以我们为了追求单片机运行的稳定性以及准确性,我们引出了一个概念——分频。
所谓分频即受外部晶振周期信号激励所产生的震荡,其频率恰为激励信号频率的纯分数,叫做分频,如果有一个晶振的频率是12MHz,其经过12分频后,那么其产生的时钟频率就是1MHz。即n分频就是晶振频率除以n后产生的时钟频率。像常用的国产STC89C52单片机,其利用的就是12分频。产生分频的电路成为分频器。其原理鄙人也没研究过。
经过以上步骤,我们已经可以产生一个可供单片机直接使用的时钟信号。但是我们还有几个很重要的概念需要去理解:时钟周期,机器周期,指令周期。
-
时钟周期
时钟周期就是我们所说的晶振震荡信号的周期,其为时钟频率的倒数。时钟周期是计算机中最基本的、最小的时间单位。 -
机器周期
机器周期是指经过分频后的时钟周期,在一个机器周期内,单片机可以完成一个最基本的动作。 -
指令周期
指令周期,我们知道,在C语言编程时,我们需要先写好程序,再由计算机上的C语言编译器将我们的代码编译为汇编指令,在进一步处理。然而每一条C语言程序可能会编译出汇编指令的条数各不相同。(一条汇编指令就是一条基本操作,即占用一个机器周期)因此一条C语言代码编译出若干个汇编指令,即一条C语言指令运行的时间位机器周期的整数倍。
触发器与寄存器
了解了以上概念后,我们就可以对单片机进行精确延时了。首先,我们要知道什么是寄存器。不过首先我们先来看看什么叫触发器,我们应该都玩过5块钱一个的小激光笔,上面往往有2个开关,就像下面这幅图一样:
对于这种激光笔上的两个开关,一个是激光头的开关,我们需要一直按着这个开关,激光器才会一直发光,然而对于另一个开关,买过这种激光笔的小伙伴儿肯定知道,这种玩具激光笔上除了激光头,还有两个LED灯,另一个开关(应该是下面那一个)就是控制LED灯的。我们会惊讶地发现,如果你想打开LED灯,不需要一直按着开关,只需要按一下就松开,那么LED灯就会常亮。
这就比较好玩儿了,对于这种 “一触即发” 的装置,我们称之为触发器。对于触发器的电路原理这里限于篇幅不再赘述。可以在《编码的奥秘》一书中的“反馈与触发器”一节中了解详细原理。那么所谓的触发器能干什么呢?我们会发现开关一按,它就能一直保持一个状态。所以我们是不是可以考虑利用他的这一特性,来为我们保存一些数据?
一个触发器只能存储两种状态,不是开就是关。
那么如果我们将8个触发器串起来,每一个都有2种情况,那么这个组合就可以存储2的8次方即256种情况即可存数字(0~255)。如下图。
这下就不算很难了吧?这种我们可以把二进制值暂存进去的东西就叫做寄存器,图为一个8位寄存器。同理,如果把16个触发器串起来,我们就可以用来存储65536种情况,即数字(0~65535)。到这里是不是很多东西就明了了?
定时器
终于到了重点定时器了,好了,我现在告诉你,定时器就是一个寄存器,只不过是用作计数的,换了个名字而已。我们现在以STC89C52单片机为例,这款单片机上有定时器0和定时器1两个定时器。这两个定时器都是16位寄存器,但是分为高八位和低八位,例如定时器0 ,可分为高八位TH0和第八位TL0(HIGH和LOW而已,我英语这么LOW都看得懂)
我们以定时器TH0为例,如上图,将16位定时器0分为高八位和低八位。将其命名为TH0和TL0,由图易知:TH0和TL0所能表示的数据范围均为0xff。但两者虽然表面上被隔开,但是其仍为一个16位寄存器,所谓的隔开只是使我们可以分开观测高八位和低八位上分别存放的数据。它们就像十位和个位的关系。如果个位满了,就向十位进一。它们运行起来也是如此,如果低八位满了,就像高八位进一。
我们由上图可得,低八位可以存储(2^ 0+2^ 1+2^ 2+2^ 3+2^ 4+2^ 5+2^ 6+2^ 7)即(0~255,共256中情况)同理高八位也可以存储256种情况,若高八位与低八位结合起来,可表示256*256=65536种情况(即0-65535)
我们现在让定时器T0开始工作,它每过一个机器周期就会自动加1(先加在低八位上,若低八位溢出,则向高八位进位)如果我们使用的晶振是11.0592MHz,而STC89C52采用的是12分频,即一个机器周期是12/11.0592M秒。 现在假设我们想要延时1秒。我们先计算一下需要多少个机器周期n=1秒/(12/11.0592M)=921600,而这已经超过了整个16位定时器T0的允许范围。
这可如何是好?我们可以先定个小目标,挣他一个亿(开个玩笑)。我们可以先定一个小的时间,比如1毫秒。我们算一下1毫秒需要多少机器周期n=1*10^-3/(12/11.0592M)=939,我们取一个整数即需要939个机器周期。我们16位定时器可以检测0-65535
范围的机器周期,这里我们需要184个机器周期来实现我们需要的20毫秒延时。所以我们让定时器经过184个机器周期后检测溢出就可以了。因此我们对T0赋值,在这里我们对它的高八位和低八位分别赋值为TH0=0xff,TL0=0x47。如何?再经过184个机器周期是不是整个定时器T0就溢出了。到时我们只要监测T0的溢出就可以,不过这才延时了1ms,那如何延时1s呢?定义变量走循环不就得了?完美!!!
下面以一份代码来封装1毫秒函数
#include<reg52.h>
sbit LED = P1^0;
void delay(unsigned int ms)
{
unsigned int cycle = 0;
TMOD = 0x01; //设置定时器工作模式为高低八位合并为一个16位寄存器
TH0 = 0xfc;
TL0 = 0x65;
TR0 = 1;
while(1)
{
if(TF0==1) //检测定时器0是否溢出
{
TF0 = 0; //若溢出,则重置检测位TF0
TH0 = 0xfc;
TL0 = 0x65;
cycle++;
if(cycle==ms)
break;
}
}
}
void main()
{
LED = 0;
while(1)
{
delay(1000);
LED = ~LED;
}
}
注:
①上述程序未考虑每一条代码的运行时间,勉强可以使用
②上述TMOD为另一个8位寄存器,定时器T1和T0都有4种工作模式通过TMOD(定时器模式选择寄存器)来控制定时器的工作模式。
③TF0的作用如下图:鄙人没学过HTML,因此排版可能令人感到不适,谨此致歉。