定时器是单片机里非常重要的一个模块,必须熟练掌握,本篇按这样的顺序展开:
在说定时器之前,先提一个比较重要的东西,传统51单片机是12T的,而15单片机是1T的,单片机复位后定时器默认处于12T模式,可以通过AUXR寄存器设置定时器工作在1T模式
12T单片机的机器周期为 12/晶振频率
1T 单片机的机器周期为 1 /晶振频率
显然对于1T单片机的处理速度会比12T单片机要高,不过上面的公式并不代表单片机的真实处理速度相差12倍,对于不同的指令速度差是不一样的
机器周期不同使得定时器的计数频率不同,因此需要格外注意
定时器相关寄存器主要有以下这些
需要重点掌握的是TCON、TMOD、TL0/1、TH0/1、IE、T2L、T2H、AUXR
STC15F2K60S2单片机上有定时器0、1、2、3、4,定时器3、4我也没怎么了解,有兴趣可以自己查阅手册
1、TCON:控制寄存器,一般记住每个位,通过位寻址单独设置每一位
TFx是溢出标志位,在溢出后必须清除该位,定时器才会重新计时,若定时器工作在自动重装载模式,该位自动清除
TRx是运行控制位,置1时使能定时器x,置0时失能定时器x
IEx、ITx使用较少,有兴趣可以自行查阅手册
2、TMOD:工作模式寄存器,高四位管理定时器1,低四位管理定时器0,由于不可位寻址,必须整体赋值
GATE:使用较少,有兴趣可以自行查阅手册
:置1用作计数器,置0用作定时器
M1、M0:两位有四种组合(00~11)表示四种工作方式,其中最常用的是工作方式0(M1M0 = 00)
3、
TLx、T2L是定时器x计数值的低8位
THx、T2H是定时器x计数值的高8位(当用作8位自动重装载模式,TH就是重装值,计数值只存在于TL中)
4、IE:中断允许寄存器,可以位寻址,一般进行位操作(IP是中断优先级控制寄存器,用得少)
EA:置1使能总中断,置0失能总中断
ETx(x为0/1):开启定时器x的中断
另外定时器2中断受IE2寄存器中的ET2位控制,该寄存器不能位寻址
5、AUXR:辅助寄存器,非常重要,能设置定时器0/1输入时钟是否进行12分频、选择串口1的波特率来源(定时器1/2),同时作为定时器2的控制、工作模式寄存器,另外由于这个寄存器不存在于51,如果使用51的头文件,必须自行定义该寄存器地址(对于T2L、T2H以及以后讲到的P4口也一样):
sfr AUXR = 0x8e;
sfr T2H = 0xd6;
sfr T2L = 0xd7;
定时器0、1功能几乎完全相同,定时器1和定时器2可以用作波特率发生器即作为串口的时钟,以定时器0为例,有四种工作方式,其中最常用的是方式0:16位自动重装载模式,工作原理图如下
时钟来源有2:SYSclk(可通过AUXR的T0x12位 选择不分频或12分频)、IO口中的T0引脚的计数序列;
计数序列输入后,只有在TR0 = 1,输入有效;
在工作模式0下,设置TL0、TH0并启动定时器,定时器会将TL0、TH0的值拷贝值RL_TL0、RL_TH0作为重装值
当TL0、TH0溢出,TF0将被硬件值1,如果开启了中断(EA=1、ET0=1),则程序指针指向中断服务程序,暂时离开主程序
在不开启中断时,需要在主程序中扫描TF0并置0
在开启中断时,如果工作在非自动重装载模式,需要在中断程序软件清除TF0;如果工作在自动重装载模式,中断发生后TF0会自动硬件清除
计数初值的计算:
选择系统频率:如果使用到串口,希望串口有较好的工作可靠性,最好选择11.0592MHz,因为这是与波特率是整数倍的关系,能够整除;如果不使用串口,希望计时器工作尽可能精确,最好选择12MHz,尤其是12T模式下,系统时钟被12分频,如果用12MHz / 12 = 1MHz为整数,而11.0592MHz / 12不是整数,会出现误差
初值计算:16位模式下,计数范围为0~65535,若选择11.0592MHz系统频率,定时器工作在12T模式下
每次计数间隔为t1 = 12 * 1 / 11.0592 us
最大定时时长为t2 = 65535 * t1 us
期望定时时长为t3,则计数初值 Count = (最大定时时长t2 - 定时时长t3)* 11.0592 / 12 = 65535 - t3 * 11.0592 / 12
高8位:TH1 = Count / 256;
低8位:TL1 = Count % 256;
定时器一般用于作为计时、任务调度,我比较喜欢将定时器0用作计时,定时器1用作任务调度,因为默认情况下定时器0的中断优先级高于定时器1,这样可以尽可能保证计时的准确性
1、定时器0配置为工作方式0,每2ms中断发生一次中断(系统频率12MHz)
void TimerInit(void){
TMOD&=0xf0;
TH0=(65535-2000)/256;
TL0=(65535-2000)%256;
ET0=1;
EA=1;
TR0=1;
}
2、中断程序框架
void Timer0(void)interrupt 1{
}
interrupt x表示这是一个中断服务程序,x是中断向量,0~4分别表示为外部中断0、定时器中断0、外部中断1、定时器中断1、串口中断的向量
3、基本用法
#define T_MAX 100
#define TASK1_TIME 5
#define TASK2_TIME 10
void Timer1()interrupt 3{
static unsigned int uiCnt;
uiCnt++;
if(uiCnt >= T_MAX)
{
uiCnt = 0;
}
if(uiCnt % TASK1_TIME == 0)
{
Task1();
}
if(uiCnt % TASK2_TIME == 0)
{
Task2();
}
}
1、定义计数变量,按照中断间隔递增计时(注意要限制最大值及时清除)
2、设置任务执行周期,按一定的间隔调度任务
使用上面的方法,每添加一个任务,需要找到相应的中断服务程序进行修改,这样显然比较麻烦,理想的做法应该是,我们想添加任务,不需要找到该段程序,而是直接通过代码将任务添加到中断服务程序,那么有没有这样的办法呢?显然是有的,做法如下:
在初始化定时器的时候,定义一个定时器任务调度列表,然后在定时器中断服务程序中对这个列表循环扫描,检测到需要执行的任务调用相应子程序即可
为了将任务加入定时器任务列表中,需要:
1、定义一个任务列表(函数指针数组)
2、编写一个注册函数TimerFunRegister,用于将任务入口函数注册到任务列表中
3、在任务初始化后调用TimerFunRegister注册任务
到这里已经可以将定时器完全独立为一个模块(对于按键等模块也可以这么做),在编写调试无误后,以后基本不需要再打开定时器相关的代码文件,只需要在应用程序相关文件编程任务程序,调用TimerFunRegister将其注册到定时器任务列表中即可,如果有需要,可以加入一个注销函数,方法类似注册,大家可以自己动手尝试
代码如下:
#include "stdlib.h"
#define FUNC_NUM_MAX 10
#define T_MAX 50
typedef void (*TimerFunc)(int uiTimes);
TimerFunc g_aTimerFun[FUNC_NUM_MAX] = {NULL};
int TimerFunRegister(void *pFun)
{
unsigned int uiIndex;
if(pFun == NULL)
{
return -1;
}
for(uiIndex = 0; uiIndex < FUNC_NUM_MAX; uiIndex++)
{
if(g_aTimerFun[uiIndex] == NULL)
{
g_aTimerFun[uiIndex] = (BTimerFunc)pFun;
return 0;
}
}
return -1;
}
void Timer1(void)interrupt 3
{
static unsigned int uiTimes;
unsigned int uiIndex;
uiTimes++;
if(uiTimes >= T_MAX)
uiTimes = 0;
for(uiIndex = 0; uiIndex < FUNC_NUM_MAX; uiIndex++)
{
if(g_aTimerFun[uiIndex] == NULL)
continue ;
g_aTimerFun[uiIndex](uiTimes);
}
}
任务注册方法:
//包含头文件
void MyTask(int uitimes)
{
}
void TaskInit(void)
{
/*
***任务相关初始化
*/
TimerFunRegister(MyTask);
}
void Init(void)
{
TimerInit();
TaskInit();
}
void main(void)
{
Init();
while(1)
{
}
}