定时器1代码
043.5.4 定时器1代码
#include "reg52.h"
#include <intrins.h>
sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
//sbit KEY1 = P3^2;
//sbit KEY2 = P3^3;
//sbit SUN = P3^7;
typedef unsigned char uchar;
typedef unsigned int uint;
uint tim_count = 0;
void Delay_xms(uint xms) //@11.0592MHz
{
uchar data i, j;
while(xms)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
//初始化
//TMOD = 0x01;
TMOD &= 0x0F; //高四位清0 保留低四位
TMOD |=0x10;
TH1 = 0XFC;
TL1 = 0X66;//0xDC00;
TF1 = 0;
ET1 = 1;
EA = 1;
TR1 = 1;
while(1)
{
Delay_xms(1000);
}
}
void Timer1_Routine(void) interrupt 3
{
TH1 = 0XFC;
TL1 = 0X66;//0xDC00;
tim_count++;
// if(tim_count > 100)
// {
// LED3 = ~LED3;
// tim_count = 0;
// }
tim_count = tim_count%500;
if(0 == tim_count)
LED4 = ~LED4;
}
一、定时器0代码优化
本节先优化定时器0实验中的软件计数,再使用定时器计算器验证初值,最后将程序完整迁移到定时器1。最终程序的工作链路是:
11.0592 MHz系统时钟
│ 12T分频
▼
Timer1每约1 ms溢出一次
│
▼
进入interrupt 3,重装0xFC66
│
▼
tim_count累计并对500取余
│ 达到500次
▼
P1.3翻转,LED4改变亮灭状态
1.1 例题1: 定时器0代码优化思路
计数器可以通过取余运算限制在固定区间内:
tim_count++;
tim_count = tim_count % 100;
if (tim_count == 0)
LED4 = ~LED4;
当除数为100时,tim_count的变化规律是:
初始值0
│ 第1次中断:先加1,再对100取余
▼
1 → 2 → 3 → … → 98 → 99 → 0 → 1 → …
▲
└─ 第100次中断时,100 % 100 = 0
余数一定满足$0\leq r<100$,所以变量在0~99之间循环。每当余数等于0,就表示又累计了100次中断。如果定时器每10毫秒中断一次,LED状态翻转间隔就是:
$ 100\times10\ \text{ms}=1\ \text{s} $
这段逻辑与注释中的if (tim_count > 100)并不完全等价。变量从0开始,每次先自增时,> 100要等到第101次中断才成立,对应约1.01秒;取余写法在第100次中断时准确触发。若采用比较方式,严谨写法是:
if (++tim_count >= 100)
{
tim_count = 0;
LED4 = ~LED4;
}
取余写法紧凑直观,但8051执行除法的成本通常高于递增和比较。对于LED实验,两种正确写法都可使用;对中断执行时间要求较高时,更适合使用递增、比较、清零。
编译并烧录后,LED4仍约每1秒翻转一次。也可以暂时将取余数设成4,观察它是否每4次中断触发一次,以验证软件分频逻辑。
1.2 例题2: 定时器0的配置与初值设置
定时器初值既可使用公式计算,也可以使用定时器计算器生成,但工具结果仍要与晶振频率、12T/6T设置和工作模式相互核对。
在11.0592 MHz晶振、12T、16位模式1下,初值公式是:
$ X=65536-\frac{T_{\mu s}\times f_{MHz}}{12} $
常用结果如下:
| 目标时间 | 理论计数次数 | 采用的整数次数 | 16位初值 | 高、低字节 |
|---|---|---|---|---|
| 10 ms | 9216 | 9216 | 0xDC00 | THx=0xDC,TLx=0x00 |
| 1 ms | 921.6 | 922 | 0xFC66 | THx=0xFC,TLx=0x66 |
其中x代表定时器编号:Timer0使用TH0/TL0,Timer1使用TH1/TL1。10毫秒结果能够整除,因此0xDC00是精确的理论初值;1毫秒需要把921.6取成整数,定时器计算器选择922次,对应0xFC66。
1.3 例题3: 定时器计算器的使用
使用下载软件右侧的“定时器计算器”时,应依次确认以下参数:
| 选项 | 本实验设置 | 选错后的影响 |
|---|---|---|
| 时钟频率 | 11.0592 MHz | 初值与开发板实际计数速度不匹配 |
| 定时长度 | 10 ms或1 ms | 生成的溢出周期不同 |
| 时间单位 | 毫秒 | 把微秒、毫秒混用会产生1000倍误差 |
| 定时器 | Timer0或Timer1 | 生成的THx/TLx、TRx/TFx编号不同 |
| 工作模式 | 16位、不自动重载 | 模式2是8位自动重载,计算方式不同 |
| 定时器时钟 | 12T(FOSC/12) | 6T时计数速度是12T的2倍 |
设置完成后点击“生成C代码”,可以读取TMOD、TH0/TL0或TH1/TL1、TFx、TRx等配置。10毫秒、Timer0、模式1、12T会得到TH0=0xDC、TL0=0x00;1毫秒会得到TH0=0xFC、TL0=0x66。
计算器还可能生成:
AUXR &= 0x7F;
这条语句不能脱离具体芯片型号直接复制。本开发板使用的STC89C52RC确实具有地址为0x8E的AUXR,但该型号只在AUXR.1和AUXR.0定义了EXTRAM、ALEOFF,并且数据手册将它标为只写寄存器;AUXR.7不是Timer0/1的12T选择位,读改写形式也不适合只写寄存器。本芯片Timer0/1的6T或12T速度通过下载软件的“使能6T(双倍速)模式”选项设置。本实验不勾选该选项,使用12T,因此代码中不需要计算器生成的这条AUXR语句。
工具的正确使用原则是“生成后再核对”,而不是整段无条件复制:
- 对照芯片数据手册确认寄存器是否存在、各位含义是否相同。
- 对照下载选项确认12T或6T与计算器一致。
- 用公式复算计数次数和初值。
- 模式1必须在中断服务函数里再次装载
THx/TLx。
二、定时器1配置
课程中将5.2定时器0工程复制为5.3定时器1工程,再逐项迁移配置。Timer0和Timer1的基本计数原理相同,但控制位、计数寄存器、中断允许位和中断号必须成组切换:
| 功能 | Timer0 | Timer1 |
|---|---|---|
TMOD所属区域 | 低4位 | 高4位 |
| 16位模式1值 | 低4位0001,即0x01 | 高4位0001,即0x10 |
| 计数寄存器 | TH0/TL0 | TH1/TL1 |
| 溢出标志 | TF0 | TF1 |
| 运行控制 | TR0 | TR1 |
| 中断允许 | ET0 | ET1 |
| Keil中断号 | interrupt 1 | interrupt 3 |
| 中断向量 | 000BH | 001BH |
EA是所有中断共用的总开关,从Timer0迁移到Timer1时仍然设置为1。函数名可以自行命名,但建议使用Timer1_Routine,让名称、所用硬件和中断号保持一致。
三、定时器1编程
3.1 定时器1的配置与修改
TMOD的高4位控制Timer1,低4位控制Timer0:
TMOD位: 7 6 5 4 3 2 1 0
字段: GATE1 C/T1 M1_1 M0_1 GATE0 C/T0 M1_0 M0_0
└──────── Timer1 ────────┘ └──────── Timer0 ────────┘
Timer1选择模式1时,高4位应为0001,因此只修改高4位的写法是:
TMOD &= 0x0F; // 0000 1111:高4位清零,低4位保留
TMOD |= 0x10; // 0001 0000:将Timer1高4位设置成0001
例如,假设执行前TMOD=0x2D:
0010 1101 TMOD
AND 0000 1111 0x0F
= 0000 1101 保留Timer0低4位,清零Timer1高4位
0000 1101
OR 0001 0000 0x10
= 0001 1101 Timer1高4位变为0001,Timer0低4位仍为1101
最终GATE1=0、C/T1=0、M1_1M0_1=01:Timer1由TR1控制启动,使用内部系统时钟,工作在16位模式1。直接执行TMOD=0x10也能配置Timer1,但会同时清除Timer0低4位;掩码操作更适合两个定时器可能同时使用的程序。
迁移时不能只把TMOD中的0换成1,而应根据寄存器功能逐项替换:
TH0/TL0 ─► TH1/TL1
TF0 ─► TF1
ET0 ─► ET1
TR0 ─► TR1
interrupt 1 ─► interrupt 3
3.2 定时器1相关寄存器介绍
Timer1模式1所需寄存器如下:
| 寄存器或位 | 所在位置 | 本程序值 | 作用 |
|---|---|---|---|
TMOD.GATE1 | TMOD.7 | 0 | Timer1只受TR1控制,不与INT1门控 |
TMOD.C/T1 | TMOD.6 | 0 | 选择内部时钟,作为定时器使用 |
TMOD.M1_1/M0_1 | TMOD.5/.4 | 01 | 选择16位模式1 |
TH1/TL1 | 8DH/8BH | 0xFC/0x66 | 组成16位计数初值0xFC66 |
TCON.TF1 | TCON.7 | 初始化为0 | 溢出标志,溢出时由硬件置1 |
TCON.TR1 | TCON.6 | 1 | 启动Timer1 |
IE.ET1 | IE.3 | 1 | 允许Timer1中断 |
IE.EA | IE.7 | 1 | 打开总中断 |
Timer1模式1的内部信号链可以理解为:
C/T1=0:内部时钟
11.0592 MHz SYSclk ──► ÷12 ───────────────┐
├─► 计数开关 ─► TL1 ─进位─► TH1
T1/P3.5外部脉冲 ─────── C/T1=1 ───────────┘ ▲ │
│ ▼
TR1 AND │ 溢出置TF1
(NOT GATE1 OR INT1) ──────┘ │
▼
ET1=1且EA=1时请求中断
│
▼
interrupt 3(001BH)
本实验不使用T2MOD。Timer2是另一套定时器,由T2CON等寄存器控制;Timer1只使用上表中的Timer1相关位。
3.3 定时器1的初值计算与设置
最终程序按以下顺序初始化:
TMOD &= 0x0F;
TMOD |= 0x10;
TH1 = 0xFC;
TL1 = 0x66;
TF1 = 0;
ET1 = 1;
EA = 1;
TR1 = 1;
不是所有相关位都应设置为1:TF1要先清零,GATE1、C/T1和M1_1也为0,只有M0_1、ET1、EA、TR1需要置1。将TR1放在最后,可以确保模式、初值和中断开关都准备完成后再开始计数。
定时器溢出时,硬件把TF1置1;CPU响应Timer1中断后,硬件会清除TF1。模式1不会自动恢复0xFC66,所以每次进入中断服务函数都要再次写TH1和TL1。
3.4 例题1: 定时器1的配置与中断处理
从Timer0工程迁移时,编译通过只能证明语法和链接没有错误,不能证明寄存器与中断源已经匹配。视频中的第一次烧录没有看到LED按预期闪烁,排查时应按以下顺序检查:
- 确认烧录软件打开的是5.3工程最新生成的HEX文件,可通过文件路径和时间戳判断。
- 确认
TMOD高4位为0001,而不是继续配置低4位。 - 确认主函数使用
TH1/TL1、TF1、ET1、TR1。 - 确认中断函数也用
TH1/TL1重新装载Timer1。 - 确认函数声明为
interrupt 3。 - 确认
EA=1,且LED端口与程序中的LED4=P1^3一致。
下载软件中的“程序文件”时间戳非常有用:如果编译之后时间没有更新,往往说明加载了另一个工程的HEX文件,即使代码窗口看起来正确,板上运行的也可能不是刚编译的程序。
3.5 例题2: 定时器1中断函数修改与测试
Timer0迁移到Timer1时,中断服务函数内部也必须迁移:
void Timer1_Routine(void) interrupt 3
{
TH1 = 0xFC;
TL1 = 0x66;
// 计数与LED处理
}
如果仍然写TH0/TL0,即使Timer1中断能够进入,重新装载的也是Timer0。Timer1第一次从0xFC66溢出后会从0x0000继续计数;在12T、11.0592 MHz下,从0计满一次约需71.111毫秒,后续周期便不再是约1毫秒。
如果寄存器已换成TH1/TL1,但声明仍是interrupt 1,Keil会把该函数放到Timer0的中断入口000BH。Timer1溢出时CPU查找的是001BH,因此这个函数仍不会因Timer1溢出而执行。两项必须同时正确:
Timer1配置正确
├─ 中断函数重装TH1/TL1
└─ 中断号使用interrupt 3
│
▼
LED才能按Timer1节拍翻转
3.6 定时器1中断号与函数名的规范
传统8051兼容中断的查询次序如下:
| 中断源 | Keil中断号 | 中断向量 |
|---|---|---|
| 外部中断0 | 0 | 0003H |
| Timer0 | 1 | 000BH |
| 外部中断1 | 2 | 0013H |
| Timer1 | 3 | 001BH |
| 串口 | 4 | 0023H |
因此Timer1函数必须声明为:
void Timer1_Routine(void) interrupt 3
3是中断查询次序号,不是中断优先级。函数名Timer1_Routine是供程序员和编译器符号管理使用的,硬件最终根据中断向量进入程序;函数名可以不同,但名称与功能一致能显著减少把Timer0和Timer1混用的风险。
3.7 例题3: 定时器1实现LED闪烁
最终中断函数先重新装载0xFC66,再累计500个约1毫秒节拍:
tim_count++;
tim_count = tim_count % 500;
if (tim_count == 0)
LED4 = ~LED4;
LED4与P1.3之间的实际电路为:
点亮时的电流方向
─────►
+5 V ──► LED4 ──► R11(10 kΩ)──► P1.3
│
P1.3输出0时吸收电流
│
GND
这是低电平点亮结构:P1.3输出0时,+5V → LED4 → R11 → P1.3 → GND形成回路,LED4点亮;P1.3输出1时,两端电位接近,LED4熄灭。LED4 = ~LED4;就是在输出0和1之间翻转。
电阻R11负责限制LED电流,避免LED和单片机I/O口过流。定时器和LED之间没有一根直接的硬件连线;Timer1位于单片机内部,它先产生中断,CPU再执行取反语句,最终由P1.3改变外部LED电路状态:
晶振X1 ─► 单片机内部Timer1 ─► TF1中断请求 ─► CPU执行中断函数
│
▼
P1.3输出翻转
│
▼
LED4外部电流通路改变
板上11.0592 MHz晶体X1连接在XTAL1/XTAL2之间,两端分别通过C9、C10(各22 pF)接地,与单片机内部振荡电路共同产生系统时钟。这个时钟在12T模式下除以12后驱动Timer1计数,最终把晶振的稳定频率转化为肉眼可见的LED节拍。
3.8 定时器1的计数频率与初值调整
最终程序使用:
TH1 = 0xFC;
TL1 = 0x66;
两者组成16位初值0xFC66。Timer1距离溢出还需计数:
$ 65536-\text{0xFC66}=65536-64614=922 $
12T模式下的计数频率为:
$ f_{T1}=\frac{11.0592\ \text{MHz}}{12}=921600\ \text{Hz} $
所以理论溢出时间为:
$ T=\frac{922}{921600}\ \text{s}\approx1.000434\ \text{ms} $
理想的1毫秒需要921.6次计数,硬件只能计整数次,取922次的理论误差约为+0.0434%。中断响应和软件重装寄存器还会带来少量额外延迟,因此实际周期会再略长一些。
代码行末的//0xDC00描述的是10毫秒初值;当前真正参与运行的是TH1=0xFC与TL1=0x66组成的0xFC66。改变基础定时时间时,主函数和中断函数中的两组初值必须一起更新。
3.9 定时器1的闪烁频率调整与测试
基础中断约为1毫秒时,软件累计次数决定LED状态翻转间隔:
| 取余数 | 理论翻转间隔 | 完整亮灭周期 | 观察效果 |
|---|---|---|---|
| 100 | 约100 ms | 约200 ms | 闪烁很快,摄像头画面可能看似常亮或常灭 |
| 500 | 约500 ms | 约1 s | 肉眼容易看清亮灭变化 |
| 1000 | 约1 s | 约2 s | 每秒改变一次状态 |
最终代码采用tim_count % 500,因此LED4约每500毫秒翻转一次。这里的“每500毫秒闪烁一次”是指每500毫秒改变一次亮灭状态;从“亮→灭→再次亮”的完整周期约为1秒。
摄像头按固定帧率采样,快速闪烁的LED可能恰好在每帧曝光时处于相近状态,产生看似不闪或忽明忽暗的混叠现象。缩短拍摄距离、调整帧率,或者直接用肉眼观察,都能更清楚地确认约100毫秒翻转的效果。
调整闪烁节拍时应遵循以下顺序:设置TH1/TL1基础周期,计算需要累计的中断次数,同时修改中断函数中的重装值,编译并确认HEX时间戳,重新烧录,最后观察LED。主循环中的Delay_xms(1000)不是LED4的计时来源;Timer1中断会在延时函数执行期间照常发生。