51单片机上的定时器是一种硬件模块,用于计时和生成特定的时间延迟。它的电路和运转都在单片机内完成,它是嵌入在单片机内部的一个功能模块,具有多种工作模式和功能。
在51单片机中,通常有两个定时器/计数器:定时器0(Timer 0)和定时器1(Timer 1)。每个定时器都有一个相应的控制寄存器,用于设置相关参数和配置工作模式。在我们这款STC89C52上还多一个T2的定时器;
1.用于计时系统,可以实现软件计时,或者使程序每隔一定的时间就完成一项任务;
2.替代长时间的Delay,提高CPU运行效率和处理速度;
我们在前面学习了用Delay函数进行延迟,但对于Delay函数来说,我们在完成一件任务时,当它开始延迟时,我们需要停下我们CPU的手上所有的运行,等他延迟之后我们才可以继续操作,因为对于所有子函数来说,我们都需要在主函数(main)中完成,当发生Delay时,就没有办法完成其他操作,所以Delay无法实现多任务线程的工作,例如下面的一边流水灯,一边用按键控制;而对于定时器来说,恰好解决了这一难题。
定时器在单片机内部就像一个小闹钟一样,根据时钟的输出信号(脉冲),每隔一段时间,那么计数单元的数值就会加一,当计数单元达到最大值后,那么计数单元就会向中断系统发出申请,让他开始执行中断服务函数。
对于定时器来说,是有多种模式可以选择的,不只是会让计数单元一直加一加一,也可以是加十加十,这就相当于我们选择了另外一种模式;
对于STC89C52的T0和T1来说,都有四种工作模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用)
模式2:8位自动重装模式
模式3:两个8位计数器
在这里,对于我们来说,模式1是我们这篇文章要讲的,其他的像模式0和模式3,一般情况我们是用不到的,而模式2:8位自动重装模式,主要用于串口通信产生波特率进行调节;
SYSclk:system clock系统时钟,它具有晶振周期,在本开发板上晶振为11.0592MHz,是一个非常大的数字,换算成时间的话就约为9ns左右(T=1/f),可以产生脉冲,将脉冲传入计数系统,然后系统时钟可以有两种模式选择分频,也就是将频率降低;在这里,我们选择的是12T;T0 Pin是外接口引脚,可以由外接口连上一个计时器,每个一个周期脉冲就会加1,那么计时器也就可以变为计数器,通过计算脉冲来达到计数效果;也就是将C/T=0;
TR0,是计时器T0的运行控制位,相当于一个启动器(按钮),只有TR0启动之后,那么技术系统才会开始计数;TL0和TH0分别表示低位和高位,用来计算脉冲传过来的次数,每一次的脉冲过来就加一,这两个加起来总共有16bit位,那么就表示计数系统可以计算0~65535(2^16-1)次,当计数达到最大值时,就会向中断系统发出中断请求;
TF0也就是中断溢出的标志位,简单的来说就是消息提醒,向CPU告知需要中断了,中断就需要进入中断系统;
介绍:
单片机的中断系统是指单片机处理外部事件或者内部事件时的一种机制。在单片机执行程序的过程中,可能会遇到一些特殊情况,比如定时时间到、有外部设备的输入信号等。这时,CPU需要临时停止当前的程序,转去执行相应的中断处理程序,待处理完该事件后再返回原先的程序继续执行。这个定义很好理解,就像睡觉一半被闹钟叫醒去读书一样,闹钟在你睡觉途中打断你的睡觉,让你该去读书了,这个过程就是中断;
组成部分:
中断源:指引起中断的事件或信号源,可以是外部设备产生的输入信号,也可以是定时器的时间到达等。
中断控制器:负责对各个中断源的优先级进行管理和调度,确定哪个中断请求能够被CPU响应。
中断向量表:用于存储每个中断类型对应的中断处理程序的入口地址。
中断处理程序:是指处理具体中断事件的程序代码,根据中断类型执行相应的操作,处理完后返回到原先的程序继续执行.
作用:
实时性:中断系统可以及时响应外部事件,提高系统的实时性能。
节约CPU资源:通过使用中断系统,CPU不再需要不断地轮询外部设备状 态,而是在需要处理事件时才会被中断,节约了CPU的资源。
提高系统效率:使用中断系统可以使单片机在处理外部事件时不需要等待,减少了等待时间,提高了系统的效率。
灵活性:通过中断系统,可以方便地处理不同类型的事件,增加了系统的灵活性和扩展性。
在我们的认知里,一般都会在主函数中执行程序,但如果有中断函数存在的话,当主程序发出中断请求时,程序就会停止主程序,优先处理中断函数中的程序,处理完再返回主程序,这就是中断程序的流程,在下面的代码中将会有进一步加深印象;
在STC89C52中有8个中断资源:外部中断、定时器0中断、外部中断1、定时器1中断、串口中断、外部中断2、外部中断3;
中断的优先级个数有4个;
在这里i,函数后面的nterrupt+序列号就是表示不同的中断资源,当中断请求向CPU发出后中断系统响应后,那么就会执行这个中断程序;
这里的中断系统实际上是简化的了,但对于原理来说,其实是一致的,为了方便对初学者的了解,才使用该原理图;
这个就是定时器连接着中断系统了。当我们计数系统满的时候,就会向TF0标志位告知,让它发出中断请求,接着就是ETO得闭合,允许T0中断,然后就是PT0选择0,选择好定时器的优先级;
在前面几篇文章,我们使用的寄存器相对来说是比较少的,对于定时器来说,它有以下的相关寄存器:
我们可以看到对于计时器和中断系统的寄存器来说,还是相当多的,对于中断寄存器的,我们只挑出相关定时器的寄存器来讲;
寄存器都会有相应的地址,程序会通过指针指向的地址来找到对应的寄存器位置,寄存器一般有8个bit位,这里的可位寻址,表示根据它的bit位来寻找对应的地址;
首先T1和T0对应的bit位功能基本相同,所以这里只对T0展开论述;
TF0:定时器T0溢出中断标志(Timer Flag)。 T0开始计数后,但计数达到最大值后,开始产生溢出,TF0会由硬件置“1”,向CPU发出请求中断,CPU响应后,TF0才由硬件置“0”。
TR0:定时器T0控制位(Timer Run); 当GATE(一个开关)=0,TR0=1 T0开始计数,TR0=0禁止T0计数;
IE0:外部中断0请求源标志(Interrupt Enable);IE0=1外部中断0向CPU请求中断,CPU响应后,外部中断0由硬件清‘0’IE0;
IT0:外部中断0触发方式控制位(Interrupt Trigger);IT0=0时,外部中断0为低电平触发方式,输入低电平时,置位IE0。当IT1=0时,则外部中断0端口由“1”->"0"下降沿跳变,激活中断请求标志位IE0,向主机请求中断处理。
这里不可位寻址就是不能利用TMOD的bit位来寻找对应的地址,必须使用TMOD寄存器,直接对它赋值;
GATE与C/T这里不涉及就不再讲,M1和M0时选择定时器的模式:
这里由于是不可位寻址,我们用到了一种巧妙的方式——按位运算,对于我们要实现定时器0模式1 那么我们可以先对TMOD按位与0xF0,使TMOD定时器0都保持为0,接着用按位或的按位运算,将它或上0x01,这种做的目的是不用管TMOD的初始状态是什么,与上1就是1;
TH与TL后面跟0就表示定时器0的计数,后面跟1就表示定时器1的计数。
在这里,我们跟着原理图的走向,列出相关的寄存器;
==EA:CPU的中断允许控制位(Interrupt All);==这是一个总的中断控制,当EA=0,CPU将会屏蔽所有的中断申请;EA=1才允许申请。
ET0:T0的溢出中断允许位;ET0=1,允许T0中断;
PT0: 一个特殊位申请,当PT0=0,为中断优先级;PT0=1时为高级优先权。
这里我们需要对定时器进行初始化
Timer0.h
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init();
#endif
Timer0.c:
#include
void Timer0Init() //1毫秒@11.0592MHz
{
TMOD &= 0xF0;
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1; //允许T0中断
EA=1; //CPU开放中断
PT0=0; //定时器0中断优先级
}
这里初始化的是可以走1ms的定时器,首先需要对定时器0模式选择为1,然后设置定时的初值,根据每一个计数到下一个计数的时间差,从65535向前推算需要多少个计数,进行对应的高低位初始化;那么1ms后就会向TF0发出中断请求;TF0初始化为0;TR0选择1,让计时器计时,ET0选择1允许T0中断,EA为总中断控制位,选择1为打开,PT0选择0为低级中断优先级;
接着我们写一个按键选择函数:
Delay.h:
#ifndef __DELAY_H__
#define __DELAY_H__
void Delayms(unsigned int x);
#endif
Delay.c
oid Delayms(unsigned int x) //@11.0592MHz
{
unsigned char i, j;
while(x--)
{
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
Key.h
#ifndef __KEY_H__
#define __KEY_H__
unsigned char Key();
#endif
Key.c
include "Delay.h"
#include
/**
* @brief 获取独立按键键码
* @param 无
* @reval 按下按键的键码,范围0~4,无按键按下时返回值为0
*/
unsigned char Key()
{
unsigned char KeyNumber=0;
if(P3_1==0){Delayms(20);while(P3_1==0);Delayms(20);KeyNumber=1;}
if(P3_0==0){Delayms(20);while(P3_0==0);Delayms(20);KeyNumber=2;}
if(P3_2==0){Delayms(20);while(P3_2==0);Delayms(20);KeyNumber=3;}
if(P3_3==0){Delayms(20);while(P3_3==0);Delayms(20);KeyNumber=4;}
return KeyNumber;
}
最后写上主函数:
#include
#include "Timer0.h"
#include "Key.h"
#include
unsigned char KeyNum,LEDMode;
void main()
{
P2=0xFE;
Timer0Init();
while(1)
{
KeyNum=Key();
if(KeyNum==1)
{
LEDMode++;
LEDMode%=2;
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=500)
{
T0Count=0;
if(LEDMode==0)
P2=_crol_(P2,1);
if(LEDMode==1)
P2=_cror_(P2,1);
}
}
在主函数里,先对P2和定时器进行初始化,然后在循环里面走独立按键的程序,每当按一次按键1,LEDCode就会在0和1依次循环选择;
然后是中断函数,1表示是计时器的中断程序,每当计时器计数溢出时,那么我们就开始中断主程序,对中断函数开始执行,先重新对计时器初始化,便于下一次计时,这里ToCount表示多少毫秒走一次LED灯,当LEDMode为0时,向左跑流水灯,LEDMode为1时向右跑流水灯;这里的 crol()是一个LED流水灯函数,第二个参数表示每一次跨越的灯数,1也就是表示跨越一个灯数,就会形成我们的流水灯。