嵌入式微系统
上QQ阅读APP看书,第一时间看更新

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变量,根据这个变量决定是否退出。软件定时器的使用非常灵活,一定要在分析透彻后使用,若理解不够,反而会引起不必要的问题。同时在理解的基础上,可以自己修改或增加功能函数,以满足需求。