1.3.4 软件定时器
MCU51的硬件定时器通常只有三路:Timer1一般用于串口的时钟;Timer2用于系统节拍后;Timer0用于其他一路,这显然不能满足实际使用。为了解决硬件定时器的不足,基于系统节拍基础上,派生出软件定时器来模拟多路定时器,其效果类似硬件定时器,但因为是软件实现的,灵活多变,应用场合广泛,是MS的重要功能,软件定时器本质是延时执行,有以下场合需要用到:
1)闹钟,需要延时多长时间后执行。
2)按键音,按键检测到后,蜂鸣器发声,延时200ms后关闭。
3)进入某一个菜单,超过设定时间没有检测到按键,自动回到默认界面。
4)开启某一项测试,工作一定时间后关闭。
软件定时器本质是利用系统节拍作为计数时钟,每一路计数器都有一个开关,负责是否开启这一路软件定时器,它占用一个bit,最多8路也就是8 bit,恰好一个字节,对应状态变量State。
再模拟多路计数器Delay,最多8路,每一路的计数器的位数是16 bit,相当于最大计数65536,假设节拍为10ms,则最长延时时间为655.36s。Delay由TimerStart函数初始化时赋初值,之后利用节拍倒计时,倒计数为零时,产生一个软件定时器事件,这个时候,根据TimerStart函数初始化时设定的条件来执行注册的回调函数,执行的方式有两种,一种是直接在节拍中执行,一般适合执行开销低的函数;另外一种是抛出软件定时器消息,在大循环中处理注册的回调函数,这个适合执行开销高的函数。图1-13为软件定时器的原理架构图。
图1-13 软件定时器原理架构图
TimerStart(TimerMessage,1000,TimerCallBack);这个函数表示延时1000个节拍,时间到了之后,调用执行TimerCallBack()这个函数。第一个参数TimerMessage表示处理的方式,在大循环消息中执行。
假如延时之后,再自我调用延时函数,则可实现自我循环功能。类似节拍功能,只是这个节拍时间比较长。假如采用两个软件定时器嵌套使用,则可以根据延时时间实现不等长度的周期性工作,比如工作3秒、停7秒,模拟工作状态,老化测试设备高。软件定时器可根据需求应用灵活,读者多多体会。以下是一个软件定时器间隔1000个节拍,自我循环打印的例子。
代码清单1-11:软件定时器示例
void TimerCallBack(void)
{
printf("软件定时器延时执行\n");
TimerStart(TimerMessage, 1000, TimerCallBack);
}
软件定时器由注册开始函数TimerStart、系统节拍例行函数TimerSystickRoutine和停止函数TimerStop组成。首先TimerStart函数需要把软件定时器的工作模式、延时时间、回调函数注册到相关的静态变量中,注册之后,TimerSystickRoutine函数被系统节拍例行调用,每调用一次,延时时间减一,当减到为零时,按存储的处理方式,开始执行存储的回调函数,之后把这一路定时器清空。延时时间及回调函数,大家很容易理解,工作模式难以理解,这是因为软件定时器是基于节拍模拟出来的,那么回调函数的执行,一种是直接在节拍中运行即可,但若这个回调函数速度比较慢,执行时间比较长,故不宜在节拍中执行,于是就增加了另一种模式,即把这个回调函数的地址,通过消息传递到大循环中,在大循环中执行,这部分内容在消息机制中已有讲解。
软件定时器处理模式有两种:一种是直接在节拍例行函数中直接执行,因为在节拍中执行,也就是在中断中执行,所以执行的回调函数不能太长,否则占用中断时间;但有些确实需要占用较长时间的,则采用另外一种处理模式,即通过消息抛出回调函数的函数入口地址,再在大循环中执行。处理模式枚举定义如下。
代码清单1-12:定时器处理类型枚举
typedef enum { TimerSystick = 0; // 系统节拍中处理 TimerMessage = 1; // 大循环消息中处理 }TimerModeEnum;
软件定时器的数据结构比较简单,TimerSum定义了定时器数量,State是一个8 bit类型的定时器就绪表,总共可以标记8路定时器就绪状态。Timer struct定义了延时时间Times和回调函数CallBackFunction类型变量,与后面的TimerBlock数组一起,用于存储延时时间和回调函数地址,Mode定义了工作模式。具体代码如下。
代码清单1-13:定时器数据类型
typedef struct { ushort Times; function Function; }TimerStruct; #define TimerSum 0x4; // 定时器数量,不超过8 bit static Byte idata State = 0; // 工作标记寄存器,共8路 static TimerModeEnum idata Mode; // 工作模式 static TimerStruct idata TimerBlock[TimerSum]; // 注册函数及次数存储数组
软件定时器启动函数TimerStart包括三个参数:mode(工作模式)、TimerModeEnum(枚举类型)、times(16 bit ushort类型),最大65535,MS默认节拍10ms,也就是说最大延时是655.35s,registerFunction是注册函数,直接是无参数的函数名,往往也叫回调函数。软件定时器,不推荐在中断中被调用启动。代码如下。
代码清单1-14:定时器启动
Byte TimerStart(TimerModeEnum mode, ushort times, function registerFunction) { Byte i; EnterCritical(); for(i = 0; i < TimerSum; i++) { if(!GetBit(State, i)) { TimerBlock[i].Times = times; // 注册节拍 TimerBlock[i].Function = registerFunction; // 注册函数 if(mode) // 工作模式 SetBit(Mode, i); else ResetBit(Mode, i); SetBit(State, i); // 置位开启 ExitCritical(); return(i); } } ExitCritical(); return(invalid); }
软件定时器停止函数为TimerStop,参数为id,从TimerStart中获得,值为0,1,2…,代码如下。
代码清单1-15:定时器停止
void TimerStop(Byte id) { if (id >= TimerSum) return; EnterCritical(); ResetBit(State, id); ExitCritical(); }
软件定时器的运行是基于系统节拍的,实现计时,所以必须要有一个节拍例行处理函数。代码如下。
代码清单1-16:定时器系统节拍例行程序
void TimerSystickRoutine(void) { Byte i = 0, stateBackup; if (State == 0x00) return; // 状态表为空,跳出 stateBackup = State; // 复制一份状态表 while (stateBackup) { if ((stateBackup & 0x01) == 1) { if ((--TimerBlock[i].Times) == 0) { // 计数递减到零时 if (GetBit(Mode, i)) // 获取工作模式 PostMessage(MessageTimer,(ushort)TimerBlock[i].Function); // 大循环中处理 else (TimerBlock[i].Function)(); // 系统节拍中处理 ResetBit(State, i); // 关闭这一路定时器 } } stateBackup = stateBackup >> 1; i++; } }
MS的软件定时器是动态的,注册建立一路定时器后,用完就回收,这种定时器适合RAM资源少的芯片,尤其是MCU51这类,以便于多次使用,满足大部分需求。但有些需求希望软件定时器是静态的,可以开始,可以结束,但不会释放这一路定时器,这样id号可一直不变,若再加入复位功能,可以比较简单地用于处理一些超时等待功能。比如,界面等待一个数字按键,若时间超过某一个值,返回默认界面,但在超时时间内再获得一个按键,超时重新复位,等待下一个按键,而这一功能目前动态定时器完成起来较麻烦。此外,模拟软件定时器的自我循环,采用消息的是大循环模式,当它停止时,会出现一个临界态,在大循环中执行时,id已经被释放,这时外界想关闭这个自我循环已关闭不了,要想解决这个问题,最好在大循环中引入一个bool变量,根据这个变量决定是否退出。软件定时器的使用非常灵活,一定要在分析透彻后使用,若理解不够,反而会引起不必要的问题。同时在理解的基础上,可以自己修改或增加功能函数,以满足需求。