对于刚接触单片机的同学来说可能会对定时器/计数器的应用很蒙圈,特别是初值的计算和各种定时方式的选择。下面希望能给你带来一个清晰的思路。
定时器:一般用于软件计时,给定时器设置一个时间,时间到了系统停止当前的工作跳转到事先定义好的定时器中断函数里,函数里可以做一些周期性的事情。
计数器:一般用于检测外来脉冲信号,给计数器设置一个次数,次数到了系统停止当前的工作跳转到事先定义好的计数器中断函数里,函数里做相应的事情。
先说一下相关的寄存器,也可以直接跳过,看后面的实例分析。
配置定时器或者计数器就是对相应的寄存器进行赋值,下面是相关的寄存器描述:
对照着上面这一字节的每一位,进一步解析:
位(符号) |
功能 |
TMOD.7 (GATE) |
置1时,只有在脚为高、TR1=1时才可打开定时器/计数器1 置0时,TR1=1即可打开定时器/计数器1 |
TMOD.3 (GATE) |
置1时,只有在脚为高、TR1=1时才可打开定时器/计数器0 置0时,TR1=1即可打开定时器/计数器0。 |
TMOD.6 ( /) |
置1时,用作计数器1(从T1/P3.5脚输入) 置0时,用作定时器1 |
TMOD.2 (/ ) |
置1时,用作计数器0(从T0/P3.4脚输入) 置0时,用作定时器0 |
TMOD.5/TMOD.4 (M1、M0) 定时器/计数器1 选择工作方式 |
方式0:M1=0,M0=0 ,13位定时器/计数器 |
方式1:M1=0,M0=1 ,16位定时器/计数器 |
|
方式2:M1=1,M0=0 ,8位自动重载定时器 |
|
方式3:M1=1,M0=1 ,定时器/计数器1 此时无效 |
|
TMOD.1/TMOD.0 (M1、M0) 定时器/计数器0 选择工作方式 |
方式0:M1=0,M0=0 ,13位定时器/计数器 |
方式1:M1=0,M0=1 ,16位定时器/计数器 |
|
方式2:M1=1,M0=0 ,8位自动重载定时器 |
|
方式3:M1=0,M0=0 ,双8位定时器/计数器 |
/*1*/ TMOD|=0x00; //选择定时器0,工作方式为0,
/*2*/ TMOD|=0x30; //选择定时器1,工作方式为1
/*3*/ TMOD|=0x40; //选择计数器1,工作方式为0
用或运算是为了在给相应位赋值时不会影响无关位。可以试着解读TMOD=0xDA
主要看T开头的,I开头是外部中断,先不管。
位(符号) |
功能 |
TCON.7 (TF1) |
定时器/计数器1溢出标志位。当 T1 被允许计数后T1从初值开始加1计数,最高位产生溢出时,置“1 ”TF1 ,并向 CPU请求中断,当CPU响应时,由硬件清“0 ”TF1 ,TF1也可以由程序查询或清“0 ”。 |
TCON.5 (TR1) |
定时器 T1 的运行控制位。该位由软件置位和清零。当 GATE(TMOD.7)=0,TR1=1 时就允许T1开始计数,TR1=0 时禁止 T1 计数。当 GATE(TMOD.7)=1,TR1=1 且 INT1 输入高电平时,才允许 T1 计数。 |
TCON.4 (TF0) |
定时器/计数器 0 溢出标志位。当T0被允许计数后T0 从初值开始加 1 计数,最高位产生溢出时,置“1”TF0,并向CPU请求中断,当 CPU 响应时,由硬件清“0”TF0,TF0也可以由程序查询或清“0”。
|
TCON.3 (TR0) |
定时器 T0 的运行控制位。该位由软件置位和清零。当 GATE(TMOD.3)=0,TR0=1 时就允许T0开始计数,TR1=0 时禁止 T0 计数。当 GATE(TMOD.3)=1,TR0=1 且 INT0 输入高电平时,才允许 T0 计数 |
除了TCON、TMOD还有TL0、TH0和TL1、TH1,它们分别是定时器0的Timer寄存器和定时器1的Timer寄存器。这个参数没有单位,不是毫秒或是其他,所以设置定时器的时间要通过一定的计算得来,也就是后面要说的重点部分。
编写单片机定时器程序的步骤:
下面以定时器0为例,阐述不同的方式的编程过程。
#include
#define uchar unsigned char
#define uint unsigned int
sbit led1=P1^0;
uchar num;
void TIM0init(void)
{
TMOD=0x00; //设置定时器0为工作方式0
TH0=(8192-5000)/32; //装入初值,怎么计算,下面分析
TL0=(8192-5000)%32;
EA=1; //开总中断
ET0=1; //开定时器中断
TR0=1; //启动定时器0
}
/*
interrupt 0 指明是外部中断0;
interrupt 1 指明是定时器中断0;
interrupt 2 指明是外部中断1;
interrupt 3 指明是定时器中断1;
interrupt 4 指明是串行口中断;
函数名字可以随便起,但定时器0的中断号是固定为1的
*/
void T0_time() interrupt 1
{
TH0=(8192-5000)/32; //重装初值,如果不重装,中断只触发一次
TL0=(8192-5000)%32;
num++;
}
void main()
{
TIM0init();
while(1)
{
if(num==200) //如果到了200,说明一秒时间到
{
num=0;
led1=~led1; //让发光管状态取反
}
}
}
假设单片机用的晶振是12MHz,上面的中断函数每过5ms会被调用一次,也就是发光管每一秒状态取反一次。那么怎么计算初值以确定TL0和TH0的值呢?
定时器方式0是指13位定时器,=8192;也就是说,当设置好初值后,系统会在这个初值的隔一个机器周期就会自增1,当累加到8192的时候溢出,然后触发中断。所以(8192-初值)*机器周期=定时器产生一次中断的时间。
如果我们要设定的定时器产生一次中断的时间为5ms,那么:
机器周期=12*(1/12MHz)=1μs
初值=(8192-5ms/1μs)=3192
13位定时器中,TH0整个 8 位全用,TL0只用低 5 位参与分频。
TH0 |
bit7 |
bit6 |
bit5 |
bit4 |
bit3 |
bit2 |
bit1 |
bit0 |
TL0 |
|
|
|
bit4 |
bit3 |
bit2 |
bit1 |
bit0 |
因:3192=“110001111000”
所以TH0=“1100011”,TL0=“11000”
即TH0=(8192-5000)/32,TL0=(8192-5000)%32
如果用的是11.0592MHz的晶振,机器周期就不是整数了,12*(1/11059200)≈1.0851μs.
关于机器周期:
方式0跟方式1差不多的,不同的是方式1中TH0、TL0所有位全用。两个字节,=65536.
在定时器的方式0和方式1中,当计数溢出后,计数器变为0,因此在循环定时或循环计数时必须用软件反复设置计数初值,这必然会影响到定时的精度,同时也给程序设计带来很多麻烦。
方式2被称为8位初值自动重装的8位定时器/计数器,TL(0/1)从初值开始计数,当溢出时,在溢出标志TF(0/1)置1的同时,自动将TH(0/1)中的常数重新装入TL(0/1)中,使TL(0/1)从初值开始重新计数,这样避免了认为软件重新装初值所带来的时间误差,从而提高了定时的精度。
#include
#define uchar unsigned char
#define uint unsigned int
sbit led1=P1^0;
uint num;
void TIM0init(void)
{
TMOD=0x02; //设置定时器0为工作方式2
TH0=6; //装入初值
TL0=6;
EA=1; //开总中断
ET0=1; //开定时器中断
TR0=1; //启动定时器0
}
void T0_time() interrupt 1
{
//相比上面的方式0,这里不需要认为加入重装初值的代码
num++;
}
void main()
{
TIM0init();
while(1)
{
if(num==4000) //如果到了4000,说明1秒时间到
{
num=0;
led1=~led1; //让发光管状态取反
}
}
}
这个也是基于12MHz的振荡频率,TL0跟TL1必然是相同的,计算初值的方法跟上面一样。方式2为8位定时器/计数器,最多能装载=256个,相对方式0的13位和方式1的16位的少。方式2经历256个机器周期该计数器就会溢出。
还有一个值得注意的是num变量的类型变了,因为4000已经超出了uchar的方位,所以改为uint。
当选择方式3时,定时器T0就会被分成两个独立的计数器或者定时器。此时,TL0为8位计数器,计数溢出好置位TF0,并向CPU申请中断,之后需要软件重装初值; TH0也被固定为8位计数器,不过TL0已经占用了TF0和TR0,因此TH0将占T1的中断请求标志TF1和定时器启动控制为TR1。
为了防止中断冲突,定时器T0在方式3时,T1不能产生中断,但可以正常工作在方式0、1、2下。通常这种情况下T1将用作串行口的波特率发生器。
下面的例子是利用定时器方式3,TL0计数器对应的8位定时器实现第一个发光管以1s亮灭闪烁,用TH0计数器对应的8位定时器实现第二个发光管以0.5s亮灭闪烁。
#include
#define uchar unsigned char
#define uint unsigned int
sbit led1=P1^0;
sbit led2=P1^1;
uint num1,num2;
void TIMEinit(void)
{
TMOD=0x03; //设置定时器0为工作方式3
TH0=6; //装初值
TL0=6;
EA=1; //开总中断
ET0=1; //开定时器0中断
ET1=1; //开定时器1中断
TR0=1; //启动定时器0
TR1=1; //启动定时器0的高8位计数器
}
void TL0_time() interrupt 1
{
TL0=6; //重装初值
num1++;
}
void TH0_time() interrupt 3 //占用T1定时器的中断号
{
TH0=6; //重装初值
num2++;
}
void main()
{
TIMEinit();
while(1)
{
if(num1>=4000) //12*(1/12MHz)*(256-6)*4000=1s
{
num1=0;
led1=~led1;
}
if(num2>=2000) //12*(1/12MHz)*(256-6)*2000=0.5s
{
num2=0;
led2=~led2 ;
}
}
}
这里的num1>=4000而不是num1==4000,是为了稳妥起见,万一定时器计数超过了4000,而主循环还没来得及判断,则会错过4000.那led1就不能实现取反了。
仅供参考,错误之处以及不足之处还望多多指教。