定时器中断方式
基于STC89C52RC芯片
使用说明
定时器的基本工作原理,是使用一个n位的脉冲计数器,对时钟信号的脉冲进行计数,每个脉冲加1,当脉冲计数器达到最大值(2n)时,也就是溢出时,触发定时器中断。
定时的时间会受到以下几个因素的影响。
- 脉冲计数器的位数
- 脉冲计数的初始值
- 时钟信号的频率
启用定时器中断
STC89C52系列共有3个定时器,每个定时器都有其对应的中断。定时器0的中断允许控制位位于IE寄存器
开启定时器0的中断需要做出如下配置。
// 中断总开关
EA = 1;
// 定时器0中断开关
ET0 = 1;
定时器工作方式
STC89C52系列的定时器有两种工作方式
- 定时:用于产生精确的时间延迟
- 计数:用于统计外部脉冲信号的个数
两种工作方式的本质是相同的,都是使用脉冲计数器对脉冲进行计数
- 定时方式下,脉冲信号为系统时钟信号
- 计数方式下,脉冲信号来自单片机外部引脚
每个定时器都有一个控制位,用于设置计数/定时方式。定时器0的控制位是TMOD(Timer Mode,定时器模式)寄存器中的C/T(Counter/Timer)位。
该位的作用可参考下图(定时器结构图)
因此,如需使用定时器方式,应将C/T控制位设置为0,注意 TMOD寄存器不可位寻址。
定时器工作模式
- 模式0——13位定时/计数器
该模式下的脉冲计数器共有13位,最大计数为8192。如下图所示,TL0和TH0为两个8位寄存器,用于存储脉冲计数器数值,该模式下TL0只用到了低5位。
- 模式1——16位定时/计数器
该模式的脉冲计数器共有16位,最大计数为65536。如下图所示,TL0的8位和TH0的8位都用到了。
- 模式2——8位自动重装载
前两种模式,一次定时完毕后,如需再次定时,需要开发者重新为脉冲计数器设定初始值。而该模式可以在脉冲计数器溢出时,自动重新设置初始值,很适合用于执行周期性任务。
该模式下,只用TL0寄存器用于存储脉冲计算器数值,TH0则用于存储脉冲计数器的初始值,每次TL0溢出之后, 都会自动将TH0的值重新装入TL0。
- 模式3——双8位定时/计数器
该模式下,TL0和TH0分别用作一个8位脉冲计数器,如果需要使用两个8位定时器可使用该模式。
这四种工作模式需要两个控制位进行设置,两个控制位位于TMOD(Timer Mode,定时器模式)寄存器
设置脉冲计数器初始值
由于51单片机定时器是在脉冲计数器溢出时触发中断,因此定时的长短需要通过脉冲计数器的初始值控制。因此在使用定时器时,需要先根据期望的定时长短计算出脉冲计数器的初始值。
下面以定工作模式1(16位)为例,介绍初始值的计算过程。
1)明确每个计数脉冲的时间
根据定时器的结构图可以看出,传递给脉冲计数器的信号是系统时钟信号经过分频后得到,且分频可选两种,分别是12分频和6分频,默认是12分频。
当前系统的时钟频率为11.0592MHz,也就是11059200Hz,所以计数脉冲的频率是11059200/12 Hz,因此一个计数脉冲的时间是12/11059200 s,大约是1.08us。
2)计算所需脉冲个数
明确每个计数脉冲的时长之后,在根据期望定时时长便能计算出所需脉冲个数。假如现在需要定时1ms,那么1ms需要的脉冲个数应为0.001/(12/11059200)。
3)计算脉冲计数器初始值
假如现在需要定时1ms,那么1ms需要的脉冲个数应为0.001/(12/11059200),因此定时器的初始值应为65536-0.001/(12/11059200),大约等于64614。
计算完毕后,需要将该值赋予TL0(低8位)和TH0(高8位),如下。
TL0 = 64614;
TH0 = 64614 >> 8;
启动定时器
两种方式
-单片机内部的寄存器控制
单片机的外部引脚控制
- 当GATE=0时,外部引脚(INT0,P3.2)无效,此时只能由内部寄存器TR0控制,当TR0=1时,脉冲计数器开始计数,TR0=0时,停止计数。
- 当GATE=1时,外部引脚(INT0,P3.2)生效,此时只有当内部寄存器TR0和外部引脚INT0都为1时,脉冲计数器才开始计数,否则停止计数。
定时器0的GATE控制位位于TMODE寄存器
定时器0的TR0控制位,位于TCON寄存器
实现思路
1)启用定时器0中断
// 中断总开关
EA = 1;
// 定时器0中断开关
ET0 = 1;
2)选择定时器0工作模式
首先需要明确定时/计数的工作方式,其次还需选择脉冲计数器的工作模式。此处选择计时+模式1(16位),具体配置如下。
另外由于TMOD寄存器不可位寻址,所以可在设置工作模式的同时,将 GATE位也一并设置好,当前案例不需要外部引脚控制定时器,因此将GATE设置为0即可。
TMOD寄存器低四位的值应为0001,而高四位的值应保持原值
// GATE=0;C/T=0;M1=0,M0=1
TMOD &= 0xF0;
TMOD |= 0x01;
3)设置脉冲计数器的初始值
当前需求是令LED1每秒钟闪烁一次,具体来说就是每隔0.5s改变一下LED1的状态,显然这是一个周期性任务。对于该任务,我们可以先考虑为定时器定时0.5s,然后在定时器中断触发后,再次定时0.5s,这样就能实现周期性任务了。
但是需要注意,0.5s所需的脉冲个数为0.5/(12/11059200)= 460800个,显然已经超出了16位置脉冲计数器的最大值(65536),也就是说定时器不支持0.5s的长时间定时。
针对这种情况,就需要令脉冲计数器溢出多次来达到期望的定时时长,具体来讲就是设定一个较短的定时,比如1ms,中断之后,再次定时1ms,直到达到期望的定时时长之后,再去执行具体的任务。
综上所述,对于当前需求,我们就可以将定时时长设置为1ms,每次中断之后,就再定时1ms,除此之外,我们还需要对中断的次数进行统计,每中断500次,就改变一下LED1的状态,这样就能够实现0.5s的周期性任务了。
所以脉冲计数器的初始值应该设置为65536-0.001/(12/11059200)= 64614,具体如下。
TL0 = 64614;
TH0 = 64614 >> 8;
4)启动定时器
由于GATE已经设置为0,因此只需将TR0设置为1,即可启动定时器。
// 启动定时器
TR0 = 1;
5)定义中断服务程序
按照前文的描述,中断服务程序需要完成如下任务。
(1)重新装载脉冲计数器
(2)统计脉冲次数,每500次改变一次LED1的状态
具体代码如下。
void Timer0_Hander() interrupt 1
{
//定义静态局部变量
static unsigned int count = 0;
//重新状态脉冲计数器
TL0 = 64614;
TH0 = 64614 >> 8;
//统计中断次数
if (count++ >= 500) {
LED1 = ~LED1;
count = 0;
}
}
实现代码
Com_Util.h
#ifndef _UTIL_H_
#define _UTIL_H_
#include <INTRINS.H>
#define FOSC 11059200 // 晶振频率
#define NT 12 // 单片机的工作周期为12T
// 8bit无符号数
typedef unsigned char u8;
// 16bit无符号数
typedef unsigned int u16;
// 32bit无符号数
typedef unsigned long u32;
/**
* @brief 延时一定时长
*
* @param count 延时时长,单位1ms
*/
void Com_Util_Delay1ms(u16 count);
#endif
Com_Util.c
#include "Com_Util.h"
void Com_Util_Delay1ms(u16 count) //@11.0592MHz
{
u8 i, j;
while (count > 0) {
count--;
_nop_();
i = 2;
j = 199;
do {
while (--j);
} while (--i);
}
}
Dri_Timer0.h
#ifndef __DRI_TIMER0_H__
#define __DRI_TIMER0_H__
#include <STC89C5xRC.H>
#include "Com_Util.h"
typedef void (*Timer0_Callback)(void);
#define MAX_CALLBACK_COUNT 4
/**
* @brief 定时器初始化
*
*/
void Dri_Timer0_Init();
/**
* @brief 提供注册入口,用这个函数注册完成的函数,会以1000Hz的频率被调用
*
* @return 成功返回1,失败返回0
*
*/
bit Dri_Timer0_RegisterCallback(Timer0_Callback);
/**
* @brief 反注册回调函数,反注册的函数不会再被周期调用
*
* @return bit 反注册的结果,成功位1,失败为0
*/
bit Dri_Timer0_DeregisterCallback(Timer0_Callback);
#endif
Dri_Timer0.c
#include "Dri_Timer0.h"
#include <STDIO.H>
#define T1MS (65536 - FOSC / NT / 1000)
static Timer0_Callback s_timer0_callbacks[MAX_CALLBACK_COUNT];
void Dri_Timer0_Init()
{
u8 i;
// 总中断开关
EA = 1;
// 定时器中断开关
ET0 = 1;
// 设置定时器0的工作模式:16位定时器
TMOD &= 0xF0;
TMOD |= 0x01;
// 设置定时器的初始值
TL0 = T1MS;
TH0 = T1MS >> 8;
// 定时器0的开关
TR0 = 1;
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
s_timer0_callbacks[i] = NULL;
}
}
bit Dri_Timer0_RegisterCallback(Timer0_Callback callback)
{
// 判断这个函数有没有被注册过
u8 i;
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i] == callback)
{
// 如果该函数被注册过,直接返回
return 1;
}
}
// 注册该函数
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i] == NULL)
{
s_timer0_callbacks[i] = callback;
return 1;
}
}
return 0;
}
bit Dri_Timer0_DeregisterCallback(Timer0_Callback callback)
{
u8 i;
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i] == callback)
{
s_timer0_callbacks[i] = NULL;
return 1;
}
}
return 0;
}
/**
* @brief 1ms调用一次这个函数
*
*/
void Dri_Timer0_Func() interrupt 1
{
u8 i;
// 定义下次进入时钟中断的时间
TL0 = T1MS;
TH0 = T1MS >> 8;
// 调用所有的回调函数
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i])
{
s_timer0_callbacks[i]();
}
}
}
main.c
#include <STC89C5xRC.H>
#include "Dri_Timer0.h"
#define LED P20
static u16 s_counter = 0;
void ToggleLED() {
s_counter++;
// 每500ms切换LED亮灭
if (s_counter >= 500) {
s_counter = 0;
LED = ~LED;
}
}
void main() {
Dri_Timer0_Init();
// 在定时器回中注册回调函数ToogleLED
// 这样ToogleLED这个函数会每ms被调用一次
Dri_Timer0_RegisterCallback(ToggleLED);
while (1);
}