本章以片上定时器为切入点带大家简单了解模式寄存器,为下一章中断做准备。
在介绍定时器之前,需要先了解以下内容。
CPU时序的有关知识
① 振荡周期:为单片机提供定时信号的振荡源的周期(晶振周期或外加振荡周期);
② 状态周期:2个振荡周期为1个状态周期,用S表示。振荡周期又称为S周期或时钟周期;
③ 机器周期:1个机器周期包含6个状态周期,12个振荡周期;
④ 指令周期:完成1条指令所占用的全部事件,以机器周期为单位。
即 1机器周期 = 6状态周期 = 12振荡周期
比如,外接晶振为12MHz时,51单片机相关周期的具体值为:
振荡周期=1/12us;
状态周期=2x振荡周期=1/6us;
机器周期=6x状态周期=12x振荡周期=1us;
指令周期=1 ~ 4us;
上面的内容是我复制这篇文章的一部分,这是一个比较正式的、教科书式的定时器讲解,如果你有兴趣可以自己翻阅一下。当然,我们的课程内容不会那么学院派,上面的知识你只需要先简单理解就可以了。简单来说,我们的C语言是要翻译成机器命令来进行的,还记得我们的延时函数吗。
void delay(){
for(i=0;i<300;i++)
for(j=0;j<300;j++)
}
我们用这段函数来实现了先前流水灯的延迟方法, 但如果让你做一个红绿灯呢?红绿灯需要一个准确的计时,执行delay()命令所得到的延时是很难量化的。或者,你需要做一个电子闹钟也是同样的道理。
还值得注意的一点是,如果使用delay()函数,其本质是让cpu进行无意义的计算来实现的延迟,而使用定时器与cpu工作是并行的,期间可以执行其他命令。
在一些比较原始且需要不断进行通信的单片机,也需要计时器来进行特定的波特率设置来保证能够正确的传输数据。
IO控制中的状态寄存器
所以,学习定时计数器就很重要了,如果让你写51单片机的汇编代码,这一块的内容将占据比较大的篇幅,但所幸的是 <reg51.h> 已经将大部分内容给我们包装好了,所以我们下面简单介绍一下什么是状态寄存器。
我们最早接触到的单片机寄存器其实就是 IO,还记得 P0 = 0x0F; 的意思吗?这是通过 reg51.h的库把 P0这个变量锁定到了51单片机 P0 IO口的控制寄存器上,当你赋值 0x0F之后,实际上单片机的内部是这样做的。

也就是说,一切代码对单片机的操作本质上就是更改单片机内部的寄存器,而后单片机的内部电路再将寄存器的变更映射成输出结果。可以用一个伪代码来理解(这只是方便你理解这个过程,实际上这是由电路控制的且比这个复杂得多)。
bool P0_reg[7]; // 定义8位数组
for (i = 0; i <= 7; i++>)
if(P0_reg[i] == 1) {//打开对应IO开关; }
else //关闭对应IO开关;
所以,我们通过更改了8位IO寄存器P0的值实现了单片机的IO控制。我想此时此刻你应该对单片机的寄存器有一个初步的理解了,那么接下来将介绍定时计数器中的状态寄存器。
51单片机中的定时计数器
TMOD寄存器
51单片机中共有两个定时计数器,我们首先介绍TMOD寄存器。

也许你发现了,无论是IO寄存器还是TMOD寄存器都是8位的,这也就是称AT89C51是八位单片机的原因。
我们可以看到 7-4与3-0的内容是相同的,这是因为两个定时计数器分别占用TMOD模式寄存器的四位, 其中M0与M1用于设定内置于芯片中的计数器模式

C / T是用于选定定时计数器的模式 =0为定时模式; =1为计数模式。
GATE是门控位,即定时计数器工作是否受 P3.2与P3.3影响。若GATE=0,表示计数器计数与否与两端口电压状态无关;GATA=1时,计数器是否计数要参考引脚的状态,即P3.2为高时T0才计数,P3.3为高时T1才计数。(可自行查阅手册)
方式1下的 TH TL TF寄存器
51单片机的方式1是使用最多的计数器方法,所以我们讲解方式1。
在方式1下,定时器由两个8位寄存器THx(高字节)和TLx(低字节)组成(x可以是0或1,代表定时器0或定时器1),共同构成一个16位的计数器。当定时器启动后,每经过一个机器周期,计数器会自动加1。如果使用的是定时功能,那么计数源是内部系统时钟经过12分频后的脉冲;如果是计数功能,则计数源是对应IO(P3.2 P3.3)外部输入的脉冲;当计数器从全1变为全0时(即计满),会产生溢出,此时会设置TFx(Timer Flag x)标志位,并且如果中断被使能的话,还会触发相应的中断服务程序。
实操方式1定时10ms闪烁灯泡
假设我们想用定时器来实现一个特定的时间间隔(比如10毫秒),我们需要根据晶振频率计算出合适的初值。例如我们现在使用的51单片机使用12MHz的晶振:
简单来说,当处于计数模式时,P3.4,P3.5引脚对应的IO接收到一次高电平便进行一次计数。而当处于定时器模式时,每过一个机器周期进行一次计数。当计数器计满时(也就是达到65536后),运行一次计数器所绑定的中断函数。
中断并不是这一章想要讲解的内容,但我目前可以告诉你的是,你可以认为计数器计满后,会运行所绑定的函数。
现在一个机器周期等于12个时钟周期,即1us。
方式1最大计数为65536次,所以最大的时间间隔是65536 * 1us = 65.536ms。
如果要设定一个10ms的定时,首先计算出这段时间内的机器周期数:10ms / 1us = 10000个周期。
然后从最大值减去这个周期数得到初值:65536 - 10000 = 55536。
将这个值转换为十六进制并分配给THx和TLx寄存器:THx = 55536 / 256, TLx = 55536 % 256。
其工作方式如下图

我们还是使用之前的电路

代码如下
#include <reg51.h>
unsigned int toggle = 0;
//你可以认为计数器计满后运行一下这个函数
void Timer0_ISR(void) interrupt 1 {
TH0 = 0x79; // 将初值置回
TL0 = 0xA0;
TF0 = 0; // 清除溢出标志
if(toggle) {
P0 = 0xFF; // 设定P0输出高电平
toggle = 0;
} else {
P0 = 0x00; // 设定P0输出低电平
toggle = 1;
}
}
void main(void) {
/*定时器寄存器设置*/
TMOD = 0x01; // 设置定时器0为方式1
TH0 = 0x79; // 加载初始值
TL0 = 0xA0;
TR0 = 1; // 启动定时器0
/*可以暂时忽略这两个寄存器设置*/
ET0 = 1; // 使能定时器0中断
EA = 1; // 开启总中断
while(1); //让单片机主程序一直执行空指令
}
作业
- 以上面的代码为框架尝试使用定时器计数器的计数模式,每按三下按键翻转所有的led灯,提供视频发送到实验室邮箱。