第十四章 定时器中断
14.1 需求描述
使用定时器中断的方式实现 LED 闪烁,具体要求是使用定时器 0 令 LED1 每 1 秒钟闪
烁一次。
14.2 定时器使用说明
定时器的基本工作原理,是使用一个 n 位的脉冲计数器,对时钟信号的脉冲进行计数,
每个脉冲加 1,当脉冲计数器达到最大值(2n)时,也就是溢出时,触发定时器中断。关
于时钟信号,需要了解如下概念。
根据定时器的工作原理,可以看出,定时的时间会受到以下几个因素的影响。
(1)脉冲计数器的位数
(2)脉冲计数的初始值
(3)时钟信号的频率
在使用定时器时,需要特别注意上述三个因素。定时器的具体用法如下,以定时器 0
为例。
14.2.2 启用定时器中断
STC89C52 系列共有 3 个定时器,每个定时器都有其对应的中断。定时器 0 的中断允
许控制位位于 IE 寄存器,如下。
开启定时器 0 的中断需要做出如下配置。
// 中断总开关
EA = 1;
// 定时器 0 中断开关
ET0 = 1;
14.2.3 选择定时器工作模式
STC89C52 系列的定时器都有多种工作模式,以适用不同的工作场景。因此开发者需
要根据自己的场景选择相应的工作模式。
(1)计数/定时方式
STC89C52 系列的定时器都有定时和计数两种工作方式。定时方式用于产生精确的时
间延迟,计数方式用于统计外部脉冲信号的个数。
两种工作方式的本质是相同的,都是使用脉冲计数器对脉冲进行计数,只不过,定时
方式下的脉冲信号为系统时钟信号,而计数方式下,脉冲信号来自单片机外部引脚。
每个定时器都有一个控制位,用于设置计数/定时方式。定时器 0 的控制位是 TMOD
(Timer Mode,定时器模式)寄存器中的 C/T(Counter/Timer)位。
因此如需使用定时器方式,应将 C/T 控制位设置为 0,注意 TMOD 寄存器不可位寻址。
(2)工作模式
除了可以选择计数/定时方式之外,每个定时器还提供了多种工作模式供开发者选择。
定时器 0 共有 4 个工作模式,如下。
1)模式 0——13 位定时/计数器
该模式下的脉冲计数器共有 13 位,最大计数为 8192。如下图所示,TL0 和 TH0 为两
个 8 位寄存器,用于存储脉冲计数器数值,该模式下 TL0 只用到了低 5 位。
2)模式 1——16 位定时/计数器
该模式的脉冲计数器共有 16 位,最大计数为 65536。如下图所示,TL0 的 8 位和 TH0
的 8 位都用到了。
3)模式 2——8 位自动重装载
前两种模式,一次定时完毕后,如需再次定时,需要开发者重新为脉冲计数器设定初
始值。而该模式可以在脉冲计数器溢出时,自动重新设置初始值,很适合用于执行周期性
任务。
该模式下,只用 TL0 寄存器用于存储脉冲计算器数值,TH0 则用于存储脉冲计数器的
初始值,每次 TL0 溢出之后, 都会自动将 TH0 的值重新装入 TL0。
4)模式 3——双 8 位定时/计数器
该模式下,TL0 和 TH0 分别用作一个 8 位脉冲计数器,如果需要使用两个 8 位定时器
可使用该模式。
这四种工作模式需要两个控制位进行设置,两个控制位位于 TMOD(Timer Mode,定
时器模式)寄存器,如下图所示。
14.2.4 设置脉冲计数器初始值
由于 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=64414;
Th0=64414>>8;
14.2.5 启动定时器
在做完上述配置后,还需最后一步——启动定时器,启动之后定时器才会开始工作。
定时器的启动可由单片机内部的寄存器控制,也可由单片机的外部引脚控制。具体控制逻
辑如下图所示。
当 GATE=0 时,外部引脚(INT0,P3.2)无效,此时只能由内部寄存器 TR0 控制,
当 TR0=1 时,脉冲计数器开始计数,TR0=0 时,停止计数。
当 GATE=1 时,外部引脚(INT0,P3.2)生效,此时只有当内部寄存器 TR0 和外部引
脚 INT0 都为 1 时,脉冲计数器才开始计数,否则停止计数。
定时器 0 的 GATE 控制位位于 TMODE 寄存器,如下图所示
定时器 0 的 TR0 控制位,位于 TCON 寄存器,如下图所示。
14.2.6 定义中断服务程序
定时器 0 的中断号为 1,因此中断服务函数应定义为。
void Timer0_Hander() interrupt 1
{
//编写定时任务逻辑
}
14.3 软件设计
14.3.1 实现思路
(1)启用定时器 0 中断
// 中断总开关
EA = 1;
// 定时器 0 中断开关
ET0 = 1;
(2)选择定时器 0 工作模式
首先需要明确定时/计数的工作方式,其次还需选择脉冲计数器的工作模式。此处选择
计时+模式 1(16 位),具体配置如下.
另外由于 TMOD 寄存器不可位寻址,所以可在设置工作模式的同时,将 GATE 位也
一并设置好,当前案例不需要外部引脚控制定时器,因此将 GATE 设置为 0 即可。
所以 TMOD 寄存器低四位的值应为 0001,而高四位的值应保持原值,所以可对
TMOD 寄存器做出如下配置。
// 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;
}
}
14.3.2 完整代码
1)Util.h
在 Util.h 中增加以下宏定义。
#define FOSC 11059200 // 晶振频率
#define NT 12 // 单片机的工作周期为 12T
2)Dri_Timer0.h
#ifndef __DRI_TIMER0_H__
#define __DRI_TIMER0_H__
#include <STC89C5xRC.H>
#include "Util.h"
void Timer0_Init();
#endif
(3)Dri_Timer0.c
#include "Dri_Timer0.h"
#define T1MS (65536 - FOSC / NT / 1000)
void Timer0_Init()
{
// 中断总开关
EA = 1;
// 定时器 0 中断开关
ET0 = 1;
// 定时器 0 工作方式
TMOD &= 0xF0;
TMOD |= 0x01;
// 脉冲计数器初始值
TL0 = T1MS;
TH0 = T1MS >> 8;
// 启动定时器
TR0 = 1;
}
4)main.c
#include <STC89C5xRC.H>
#include "Util.h"
#include "Dri_Timer0.h"
#define T1MS (65536 - FOSC / NT / 1000) // 1MS 倒计时(12T 模式)
#define LED1 P00
void main()
{
Timer0_Init();
while (1) {}
}
void Timer0_Hander() interrupt 1
{
// 定义静态局部变量
static unsigned int count = 0;
// 重新赋值脉冲计数器
TL0 = T1MS;
TH0 = T1MS >> 8;
// 统计中断次数
if (count++ >= 500) {
LED1 = ~LED1;
count = 0;
}
}
14.4 定时器封装
为使定时器使用起来更加方便和通用,我们可以将定时器代码进一步封装。
14.4.1 实现思路
封装的思想如下图所示
14.4.2 完整代码
1)Dri_Timer0.h
在 Dri 文件夹中新建 Dri_Timer0.h,内容如下:
#ifndef __DRI_TIMER0_H__
#define __DRI_TIMER0_H__
#include <STC89C5xRC.H>
#include "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
2)Dri_Timer0.c
在 Dri 目录中新建 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_callbacks[i]==callback)
{
s_callbacks[i]=callback;
return 1;
}
}
return 0;
}
void Dri_Timer0_Handler() interrupt 1
{
u8 i;
//1.重装初始值
TL0=64414;
TH0=64414>>8;
//2.轮询函数指针数组
for ( i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_callbacks[i]!=NULL)
{
s_callbacks[i]();
}
}
}
3)main.c
#include"Dri_Timer0.h"
#include<STC89C5xRC.H>
#include"Com_Util.h"
void LED_Blink()
{
static u16 count=0;
if (count>=500)
{
P00=~P00;
count=0;
}
}
void main()
{
Dri_Timer0_Init();
Dri_Timer0_RegisterCallback(LED_Blink);
while(1){
}
}