定时器0代码
042.5.3 定时器0代码
#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 &= 0xF0; //低四位清0 保留高四位
TMOD |=0x01;
TH0 = 0XDC;
TL0 = 0X00;//0xDC00;
TF0 = 0;
ET0 = 1;
EA = 1;
TR0 = 1;
while(1)
{
Delay_xms(1000);
}
}
void Timer0_Routine(void) interrupt 1
{
TH0 = 0XDC;
TL0 = 0X00;//0xDC00;
tim_count++;
// if(tim_count > 100)
// {
// LED3 = ~LED3;
// tim_count = 0;
// }
tim_count = tim_count%100;
if(0 == tim_count)
LED4 = ~LED4;
}
一、定时器0代码
这段程序让定时器0每隔约10毫秒产生一次中断,再用软件累计100次中断,将LED4的状态每隔约1秒翻转一次。完整链路如下:
11.0592 MHz晶振
│
▼
12T分频:11.0592 MHz ÷ 12 = 921.6 kHz
│
▼
Timer0从0xDC00递增到0xFFFF,再溢出
│ 共计数9216次,约10 ms
▼
TF0置位 ──► Timer0中断 ──► 重装0xDC00
│
▼
累计100次中断
│
▼
LED4状态翻转
1.1 定时器0代码编写过程
- 定时器0代码的基本步骤是:选择工作模式、装载计数初值、清除溢出标志、打开中断、启动定时器、编写中断服务函数。
- 工程可以由4.2外部中断实验复制并重命名为5.2,再删除外部中断初始化和对应的中断服务函数,保留端口定义与毫秒延时函数。
- 本程序没有单独封装定时器初始化函数,而是在
main()开头依次完成初始化;功能上与调用独立的Timer0_Init()函数相同。 while(1)中的Delay_xms(1000)代表主程序仍在执行自己的任务。定时器0到期后,CPU会暂时中断主循环,执行Timer0_Routine(),返回后再继续主循环。因此,LED4的1秒节拍来自定时器中断,并不依赖这个延时函数。
程序使用定时器0的模式1,即由TH0和TL0共同组成16位加法计数器:
高8位TH0 低8位TL0
┌──────────┐ ┌──────────┐
│ 0xDC │ │ 0x00 │
└────┬─────┘ └────┬─────┘
└────────┬────────────┘
▼
16位初值0xDC00
│ 每来一个定时脉冲加1
▼
0xDC00 → … → 0xFFFF
│ 再来一个脉冲
▼
0x0000,同时TF0置1
模式1不会自动恢复0xDC00。如果中断服务函数不重新写入TH0、TL0,下一轮就会从0x0000开始计数,周期将不再是10毫秒。
1.2 定时器0代码初始化过程
TMOD是定时器0和定时器1共用的8位工作模式寄存器,高4位属于定时器1,低4位属于定时器0:
| 位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 定时器 | T1 | T1 | T1 | T1 | T0 | T0 | T0 | T0 |
| 位名 | GATE1 | C/T1 | M1_1 | M0_1 | GATE0 | C/T0 | M1_0 | M0_0 |
| 本实验T0设置 | — | — | — | — | 0 | 0 | 0 | 1 |
定时器0低4位0001的含义是:
| 位 | 值 | 作用 |
|---|---|---|
GATE0 | 0 | 只由TR0控制启动,不要求INT0引脚同时为高电平 |
C/T0 | 0 | 使用内部系统时钟,工作在定时方式 |
M1_0 M0_0 | 01 | 选择模式1,即16位定时器/计数器 |
直接执行TMOD = 0x01;会把8位全部改写:定时器0确实会进入模式1,但定时器1的高4位也会被清零。在明确不使用定时器1、并且希望同时初始化两个定时器时,这种写法可以使用;在各模块分别配置外设的程序中,保留无关位更安全。
程序分两步只修改低4位:
TMOD &= 0xF0;
TMOD |= 0x01;
第一句等价于TMOD = TMOD & 0xF0;。按位与运算只有“两个输入位都为1”时结果才是1,所以它会保留高4位并清零低4位:
| A | B | A & B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
第二句等价于TMOD = TMOD | 0x01;。按位或运算只要有一个输入位是1,结果就是1,因此它只把最低位置1:
| A | B | A | B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
假设操作前TMOD = 0x1D,完整过程为:
原值: 0001 1101 (0x1D)
掩码: 1111 0000 (0xF0)
按位与:0001 0000 (0x10,高4位保留,低4位清零)
结果: 0001 0000 (0x10)
掩码: 0000 0001 (0x01)
按位或:0001 0001 (0x11,T0低4位变为0001)
初始化代码的执行顺序及作用如下:
| 代码 | 作用 |
|---|---|
TMOD &= 0xF0; | 清零定时器0的模式控制位,保留定时器1设置 |
| `TMOD | = 0x01;` |
TH0 = 0xDC; TL0 = 0x00; | 装入10毫秒定时初值0xDC00 |
TF0 = 0; | 清除可能残留的定时器0溢出标志 |
ET0 = 1; | 允许定时器0提出中断请求 |
EA = 1; | 打开CPU总中断开关 |
TR0 = 1; | 最后启动定时器0 |
ET0与EA必须同时为1,中断请求才能被CPU响应;TR0只负责让定时器开始计数。即使TR0 = 1,只要ET0或EA为0,定时器仍可计数和置位TF0,但CPU不会进入定时器0中断服务函数。
1.3 定时器0代码编写过程
16位计数器共有$2^{16}=65536$个状态,范围是0x0000~0xFFFF。在传统8051的12T定时方式下,定时器每12个晶振周期加1。板载晶振频率为11.0592 MHz,因此计数频率和单次计数时间为:
$ f_{timer}=\frac{11.0592\ \text{MHz}}{12}=921.6\ \text{kHz} $
$ t_{tick}=\frac{1}{921600}\ \text{s}\approx1.085069\ \mu\text{s} $
如果从0开始计满65536次,单次溢出的最长时间约为:
$ 65536\times1.085069\ \mu\text{s}\approx71.111\ \text{ms} $
所以16位定时器不能在一次溢出中直接得到1秒,程序先产生10毫秒的基础节拍,再累计100次。
晶振电路由X1、C9和C10组成:X1是11.0592 MHz晶体,连接在单片机的XTAL1和XTAL2之间;C9、C10各为22 pF,分别从晶体两端接地。
X1:11.0592 MHz
单片机 XTAL2 o────────────[ 晶体 ]────────────o XTAL1 单片机
│ │
C9 22 pF C10 22 pF
│ │
GND GND
晶体不是给定时器单独供电的元件。它与单片机内部振荡放大器、两只负载电容共同构成振荡回路,产生稳定的系统时钟。C9、C10既参与形成晶体要求的负载电容,也帮助振荡器稳定启动;它们不是串联在定时器计数通路中的“分频电容”。系统时钟在芯片内部经过12T分频后,才成为本实验定时器0的计数脉冲。
需要特别确认下载软件中的定时器速度设置。本实验按12T计算,且下载界面的“使能6T(双倍速)模式”未选中。如果芯片改为6T模式,定时器每6个时钟加1,10毫秒内会计数18432次,初值应改为0xB800;继续使用0xDC00会使中断周期约缩短为5毫秒。
定时初值计算公式
设模式1的装载初值为$X$、期望定时时间为$T$、晶振频率为$f_{osc}$。在12T模式下,从$X$计数到溢出需要$65536-X$个计数脉冲:
$ T=(65536-X)\times\frac{12}{f_{osc}} $
移项得到初值:
$ X=65536-\frac{T\times f_{osc}}{12} $
单位必须成对匹配:
- 若$T$使用秒,$f_{osc}$就使用赫兹;
- 若$T$使用微秒,$f_{osc}$就使用兆赫兹,因为$1\ \text{MHz}=1$个周期/微秒。
因此,在本实验采用的“微秒 + 兆赫兹”单位下,可以直接写成:
$ X=65536-\frac{T_{\mu s}\times f_{MHz}}{12} $
这个公式只适用于当前的16位模式1和12T设置。位数或每次计数所需的晶振周期改变后,$65536$或分母也要随之改变。
10毫秒初值计算
10毫秒等于10000微秒。代入11.0592 MHz晶振频率:
$ N=\frac{10000\times11.0592}{12}=9216 $
这里的$N$是10毫秒内需要的计数次数。装载初值应为:
$ X=65536-9216=56320 $
将56320转换为十六进制:
$ 56320=\text{0xDC00} $
16位值按高、低字节分别写入两个8位寄存器:
0xDC00 = 1101 1100 0000 0000₂
└─TH0─┘ └─TL0─┘
TH0 = 0xDC
TL0 = 0x00
因此程序中的TH0 = 0xDC; TL0 = 0x00;与10毫秒目标一致。硬件计数过程是0xDC00 → … → 0xFFFF → 0x0000;从0xDC00到溢出恰好经过$0x10000-0xDC00=0x2400=9216$个计数脉冲。
1.4 中断服务函数
Keil C51使用interrupt 1声明定时器0中断服务函数:
void Timer0_Routine(void) interrupt 1
这里的1是Keil规定的中断号,不是中断优先级,也不是“第1行代码”。定时器0的中断向量地址为000BH。满足TF0 = 1、ET0 = 1和EA = 1等条件后,CPU跳转到该向量,再执行此函数;CPU响应中断时会清除TF0。
函数一开始重新装载初值:
TH0 = 0xDC;
TL0 = 0x00;
这一步使下一次溢出仍以10毫秒为基础周期。随后进行软件分频:
tim_count++;
tim_count = tim_count % 100;
if (0 == tim_count)
LED4 = ~LED4;
tim_count每次中断先加1,再对100取余,数值按1, 2, …, 99, 0循环。每当它变回0,就说明正好经过100次中断:
$ 100\times10\ \text{ms}=1000\ \text{ms}=1\ \text{s} $
注释中的if (tim_count > 100)若启用,会在第101次中断才进入条件,得到约1.01秒。希望不用取模并严格累计100次时,可以写成if (++tim_count >= 100),然后在条件内将计数器清零。当前实际执行的取模写法没有这个差一问题。
LED4在电路中接到P1.3,连接关系为:
电流方向(点亮时)
─────►
+5 V ──► LED4 ──► R11(10 kΩ)──► P1.3
│
P1.3输出0时内部下拉到GND
这是低电平点亮电路:
| P1.3输出 | LED两端是否形成电流通路 | LED4状态 |
|---|---|---|
| 0 | +5V → LED4 → R11 → P1.3 → GND | 亮 |
| 1 | P1.3接近+5V,LED两端压差很小 | 灭 |
LED4 = ~LED4;每执行一次就把P1.3的逻辑状态取反,因此LED4每隔约1秒改变一次亮灭状态。这里“每隔1秒闪烁”指每1秒翻转一次;若观察完整的“亮→灭→亮”周期,周期约为2秒。
最后还要注意两个实际误差来源:
- 模式1需要在中断函数中用软件重装初值。从溢出、CPU响应中断到写完
TL0之间会经过若干机器周期,所以实测周期会比理论10毫秒略长;普通指示灯实验通常可以忽略,精密定时则要测量并补偿。 - 中断服务函数应尽量短。8051执行取模运算的代价高于简单比较;资源紧张时可采用递增、比较、清零的写法。若主循环也要读取由中断修改的
tim_count,应把它声明为volatile,并注意8051读取16位变量时的原子性;本程序只在中断函数中使用该变量,不存在这个共享问题。
下载运行后,主循环持续执行延时函数,定时器0仍独立计数并周期性打断主程序;LED4约每1秒翻转一次,说明“硬件10毫秒节拍 + 软件累计100次”的定时链路已经工作。