定时器中断是51单片机最常用的外设之一,它能让单片机"一心二用",在不占用CPU时间的前提下精准计时。本文将结合代码实例、寄存器图解和定时原理,带你彻底搞懂定时器0和定时器1的中断编程。
一、为什么要用定时器中断?
很多初学者刚开始都是用 Delay 函数实现延时,比如 Delay_xms(1000)。这种方式的缺点是:延时期间CPU啥也干不了,就像一个员工在"死等",浪费了处理能力。
而定时器是一个独立于CPU的硬件外设,它像一个小闹钟:
-
设置好时间长度(初值)
-
启动后,硬件自动计时
-
时间一到,触发中断,通知CPU去处理
-
CPU处理完中断,回去继续干原来的活
这样,CPU就不需要一直盯着时间,效率大大提高。
二、51单片机定时器资源概览
| 型号 | 定时器数量 | 说明 |
|---|---|---|
| STC89C51 | 2个(T0, T1) | 常用 |
| STC89C52 | 3个(T0, T1, T2) | 多一个T2,功能更丰富 |
每个定时器/计数器都可以通过寄存器配置为 定时模式 (对内部时钟计数)或 计数模式(对外部引脚脉冲计数)。
本文主要讲解 T0 和 T1 的定时中断应用。
三、核心寄存器详解(配图说明)
定时器编程需要操作以下几个特殊功能寄存器(SFR):
1. TMOD ------ 定时器工作模式寄存器(地址89H,不可位寻址)

| 位 | 符号 | 功能说明 |
|---|---|---|
| TMOD.7 / TMOD.3 | GATE | 门控位:0 → 仅由TRx启动;1 → 需INTx引脚为高且TRx=1才启动 |
| TMOD.6 / TMOD.2 | C/T | 功能选择:0 → 定时器(对内部时钟计数);1 → 计数器(对T0/T1引脚脉冲计数) |
| TMOD.5,4 / TMOD.1,0 | M1,M0 | 模式选择(见下表) |
模式选择表(M1 M0)
| M1 | M0 | 模式 | 说明 | 常见使用 |
|---|---|---|---|---|
| 0 | 0 | 0 | 13位定时器(THx+TLx低5位) | 较少用 |
| 0 | 1 | 1 | 16位定时器(THx+TLx全8位) | 最常用 |
| 1 | 0 | 2 | 8位自动重装初值 | 常用 |
| 1 | 1 | 3 | T0分成两个8位;T1停止计数 | T0专用 |
本文示例均采用模式1(16位定时器,需手动重装初值)
2. TCON ------ 定时器控制寄存器(地址88H,可位寻址)

| 位 | 符号 | 功能 |
|---|---|---|
| TCON.7 | TF1 | T1溢出标志:计满溢出时由硬件置1,进入中断后自动清零(也可软件查询清零) |
| TCON.6 | TR1 | T1运行控制位:软件置1启动,清零停止 |
| TCON.5 | TF0 | T0溢出标志,类似TF1 |
| TCON.4 | TR0 | T0运行控制位,类似TR1 |
另外低4位用于外部中断,这里不展开。
3. IE ------ 中断允许寄存器(地址A8H,可位寻址)

| 位 | 符号 | 功能 |
|---|---|---|
| IE.7 | EA | 总中断允许:1=开,0=关 |
| IE.4 | ET1 | 定时器T1中断允许 |
| IE.3 | EX1 | 外部中断1允许 |
| IE.2 | ET0 | 定时器T0中断允许 |
| IE.1 | EX0 | 外部中断0允许 |
4. THx / TLx ------ 计数初值寄存器
-
T0:TH0(高8位)、TL0(低8位)
-
T1:TH1、TL1
-
模式1时,THx和TLx拼接成一个16位计数器,从初值加1到65535,溢出触发中断。
四、定时器计数原理(重点!)
要理解初值计算,必须先搞清楚时钟周期、机器周期、指令周期的关系。
1. 几个重要概念
-
时钟周期 = 1 / 晶振频率。例如11.0592MHz晶振,时钟周期 ≈ 0.0904μs
-
机器周期 = 12个时钟周期(标准51模式,也叫12T模式)。这是CPU执行一个基本操作的时间单位。
-
定时器计数 :每经过一个机器周期 ,定时器的计数值自动加1。
(如果是6T模式,则每6个时钟周期加1,速度加倍)
2. 定时时间计算公式(模式1,12T)
定时时间(s)=65536−初值晶振频率(Hz)×12定时时间(s)=晶振频率(Hz)65536−初值×12
或者写成微秒形式:
初值=65536−定时时间(μs)×晶振频率(MHz)12初值=65536−12定时时间(μs)×晶振频率(MHz)
实例:晶振11.0592MHz,想得到10ms = 10000μs 定时
初值=65536−10000×11.059212初值=65536−1210000×11.0592
先算 10000×11.059212=11059212=92161210000×11.0592=12110592=9216
再算 65536−9216=5632065536−9216=56320
转换为十六进制:56320 = 0xDC00
所以 TH0 = 0xDC, TL0 = 0x00 ✅(与代码一致)
3. 为什么代码中是 TH0 = 0xDC; TL0 = 0x00?
因为0xDC00 高8位是0xDC,低8位是0x00。注意这里使用的是16位初值直接拆分。
4. 补充:定时器计数范围
-
模式1(16位):计数范围 0 ~ 65535
-
模式2(8位自动重装):计数范围 0 ~ 255,溢出后自动从THx取数重装,适合产生精确的短延时。
五、完整编程步骤(以定时器0为例)
参照提供的代码 main.c(第一个文件),步骤如下:
cpp
// 1. 设置TMOD ------ 选择定时器0、模式1、定时功能
TMOD &= 0xF0; // 低4位清零
TMOD |= 0x01; // M1=0, M0=1 => 模式1;C/T=0 => 定时器;GATE=0
// 2. 装入初值(10ms)
TH0 = 0xDC;
TL0 = 0x00;
// 3. 清空溢出标志位(可选,上电默认0)
TF0 = 0;
// 4. 打开定时器0中断允许
ET0 = 1;
// 5. 打开总中断
EA = 1;
// 6. 启动定时器(TR0置1)
TR0 = 1;
然后编写中断服务函数:
cpp
void Timer0_Routine(void) interrupt 1 // 中断号1对应定时器0
{
// 重装初值(模式1不会自动重装)
TH0 = 0xDC;
TL0 = 0x00;
// 用户代码:例如累计100次得到1秒,翻转LED
tim_count++;
if(tim_count >= 100) {
LED1 = ~LED1;
tim_count = 0;
}
}
注意 :中断服务函数必须使用
interrupt关键字,并指定正确的中断号:
外部中断0 → 0
定时器0 → 1
外部中断1 → 2
定时器1 → 3
串口中断 → 4
六、代码实战分析
代码1 ------ 定时器0,1秒翻转LED1和LED2
cpp
uint tim_count = 0; // 全局变量,累计中断次数
void main()
{
TMOD &= 0xF0; TMOD |= 0x01;
TL0 = 0x00; TH0 = 0xDC;
TF0 = 0; TR0 = 1;
ET0 = 1; EA = 1;
while(1)
{
Delay_xms(1000); // 主循环里随便干点别的,这里延时1秒
}
}
void Timer0_Routine(void) interrupt 1
{
TL0 = 0x00; TH0 = 0xDC;
tim_count++;
tim_count = tim_count % 100; // 0~99循环
if(0 == tim_count) // 每100次(1秒)翻转
{
LED1 = ~LED1;
LED2 = ~LED2;
}
}
效果 :两个LED同步1秒闪烁。
注意 :主循环中的 Delay_xms(1000) 是软件延时,会阻塞CPU,但因为中断仍然会按时触发,所以LED闪烁依然准确。但更好的写法是在主循环中做其他事情,完全依赖定时器产生延时。
代码2 ------ 定时器1,LED4每500ms翻转
cpp
void main()
{
TMOD &= 0x0F; TMOD |= 0x10; // 高4位设T1模式1,低4位保留
TH1 = 0xFC; TL1 = 0x66; // 初值0xFC66,定时多久?见后文计算
TF1 = 0; TR1 = 1;
ET1 = 1; EA = 1;
while(1) Delay_xms(1000);
}
void Timer1_Routine(void) interrupt 3
{
TH1 = 0xFC; TL1 = 0x66;
tim_count++;
tim_count = tim_count % 500;
if(0 == tim_count)
LED4 = ~LED4;
}
初值计算验证 :
0xFC66 = 64614
定时时间=65536−6461411.0592×12=92211.0592×12≈83.33×12≈1000μs=1ms定时时间=11.059265536−64614×12=11.0592922×12≈83.33×12≈1000μs=1ms
所以该定时器每1ms触发一次中断,tim_count 计数500次后翻转LED4 → 500ms翻转一次。
七、实验现象与效果
| 定时器 | 中断周期 | 累计次数 | 翻转周期 | 对应LED |
|---|---|---|---|---|
| T0 | 10ms | 100 | 1s | LED1, LED2 |
| T1 | 1ms | 500 | 500ms | LED4 |
实际烧录到开发板,可以看到LED1和LED2同步1秒闪一次,LED4以两倍频率(0.5秒)闪动。
八、进阶补充知识
1. 6T模式与12T模式
默认情况下,51单片机是12T模式,即一个机器周期=12个时钟周期。但一些增强型51(如STC系列)可以设置成6T模式,定时器计数速度加倍。如果烧录时选了6T,同样初值定时时间会减半,需要重新计算。
2. 使用模式2自动重装
模式2下,THx存放重装值,TLx从初值加到255溢出,然后自动装入THx,非常适合产生精确的PWM或串口波特率。示例:
cpp
TMOD |= 0x02; // T0模式2
TH0 = 0x38; // 自动重装值
TL0 = 0x38;
ET0 = 1; EA = 1; TR0 = 1;
3. 定时器用作计数器(外部脉冲计数)
将C/T位设为1,T0引脚(P3.4)或T1引脚(P3.5)上的负跳变会使计数器加1。例如:
cpp
TMOD |= 0x04; // T0计数器模式,C/T=1
TH0 = 0; TL0 = 0;
TR0 = 1;
while(1) {
if(TF0) {
// 计数溢出处理
TF0 = 0;
}
}
4. 门控位GATE测量脉宽
当GATE=1时,定时器启动由INTx引脚控制。可以测量INT0引脚上高电平的宽度:
cpp
TMOD |= 0x08; // GATE=1
TH0 = 0; TL0 = 0;
while(INT0 == 0); // 等待高电平到来
TR0 = 1; // 启动,但实际计数受INT0控制
while(INT0 == 1); // 等待低电平
TR0 = 0;
// 此时TH0,TL0中的数值 = 高电平宽度对应的机器周期数
九、常见问题与排错
| 现象 | 可能原因 |
|---|---|
| 定时器不工作 | 忘记 TR0=1 或 EA=1 或 ET0=1 |
| 时间不准 | 晶振频率与初值计算不匹配 |
| 中断只触发一次 | 模式1忘记重装初值 |
| 主函数死循环中不响应中断 | 中断优先级问题,但通常不会;检查总中断 |
| 编译报错"undefined symbol" | 忘记包含 reg52.h 或关键字写错 |
十、总结
51单片机的定时器中断是嵌入式入门的核心知识点。掌握以下几点即可熟练应用:
-
TMOD 选模式(最常用模式1和模式2)
-
THx/TLx 装初值,用公式 初值=65536−时间(μs)×晶振(MHz)12初值=65536−12时间(μs)×晶振(MHz)
-
TCON 的TRx控制启停,TFx查询/自动清零
-
IE 开总中断EA和对应的ETx
-
中断服务函数用
interrupt n指定正确的中断号 -
模式1需手动重装初值,模式2自动重装
希望这篇博客能让你彻底看懂定时器中断,并能自己写出精确的定时程序。如果有疑问,欢迎在评论区交流!