Timer Management

Timer API

µC/OS-II provides timer services to the application programmer and code to handle timers is found in os_tmr.c. Timer services are enabled when setting OS_TMR_EN to 1 in os_cfg.h.

Timers are down counters that perform an action when the counter reaches zero. The user provides the action through a callback function (or simply callback). A callback is a user-declared function that will be called when the timer expires. The callback can be used to turn a light on or off, start a motor, or perform other actions. However, it is important to never make blocking calls within a callback function (i.e., call OSTimeDly()OSTimeDlyHMSM()OS???Pend(), or anything that causes the timer task to block or be deleted).

Timers are useful in protocol stacks (re-transmission timers, for example), and can also be used to poll I/O devices at predefined intervals.

An application can have any number of timers (limited only by the amount of RAM available). Timer services (i.e. functions) in µC/OS-II start with the OSTmr???() prefix, and the services available to the application programmer are described in the uC/OS-II API Reference section.

The resolution of all the timers managed by µC/OS-II is determined by the configuration constant: OS_TMR_CFG_TICKS_PER_SEC, which is expressed in Hertz (Hz). So, if the timer task (described later) rate is set to 10, all timers have a resolution of 1/10th of a second (ticks in the diagrams to follow). In fact, this is the typical recommended value for the timer task. Timers are to be used with “coarse” granularity.

µC/OS-II provides a number of services to manage timers as summarized in the table below.

Table - Timer API summary
Function NameOperation
OSTmrCreate()Create and specify the operating mode of the timer.
OSTmrDel()Delete a timer.
OSTmrRemainGet()Obtain the remaining time left before the timer expires.
OSTmrStart()Start (or restart) a timer.
OSTmrStateGet()Obtain the current state of a timer.
OSTmrStop()Stop the countdown process of a timer.


A timer needs to be created before it can be used. You create a timer by calling  OSTmrCreate()  and specify a number of arguments to this function based on how the timer is to operate. Once the timer operation is specified, its operating mode cannot be changed unless the timer is deleted and recreated. The function prototype for  OSTmrCreate()  is shown below as a quick reference:


OS_TMR OSTmrCreate (INT32U           dly,              /* Initial delay        */
                    INT32U           period,           /* Repeat period        */
                    INT8U            opt,              /* Options              */
                    OS_TMR_CALLBACK  callback,         /* Fnct to call at 0    */
                    void             *callback_arg,    /* Arg. to callback     */
                    CPU_CHAR         *pname,           /* Name of timer, ASCII */
                    INT8             *perr)


Once created, a timer can be started (or restarted) and stopped as often as is necessary. Timers can be created to operate in one of three modes: One-shot, Periodic (no initial delay), and Periodic (with initial delay).

One-Shot Timers

As its name implies, a one-shot timer will countdown from its initial value, call the callback function when it reaches zero, and stop. The figure below shows a timing diagram of this operation. The countdown is initiated by calling OSTmrStart(). At the completion of the time delay, the callback function is called, assuming a callback function was provided when the timer was created. Once completed, the timer does not do anything unless restarted by calling OSTmrStart(), at which point the process starts over.

You terminate the countdown process of a timer (before it reaches zero) by calling OSTmrStop(). In this case, you can specify that the callback function be called or not.

Figure - One Shot Timers (dly > 0, period == 0)


As shown in the figure below, a one-shot timer can be re-triggered by calling OSTmrStart() before the timer reaches zero. This feature can be used to implement watchdogs and similar safeguards.

Figure - Retriggering a One Shot Timer


Periodic (no initial delay)

As indicated in the figure below, timers can be configured for periodic mode. When the countdown expires, the callback function is called, the timer is automatically reloaded, and the process is repeated. If specifying a delay of zero (i.e., dly == 0) when the timer is created and, when started, the timer immediately uses the “period” as the reload value. You can call OSTmrStart() at any point in the countdown to restart the process.

Figure - Figure - Periodic Timers (dly == 0, period > 0)


Periodic (with initial delay)

As shown in the figure below, timers can be configured for periodic mode with an initial delay that is different than its period. The first countdown count comes from the “dly” argument passed in the OSTmrCreate() call, and the reload value is the “period”. You can call OSTmrStart() to restart the process including the initial delay.

Figure - Periodic Timers (dly > 0, period > 0)


Timer Management Internals

Timer States

The figure below shows the state diagram of a timer.

Tasks can call OSTmrStateGet() to find out the state of a timer. Also, at any time during the countdown process, the application code can call OSTmrRemainGet() to find out how much time remains before the timer reaches zero (0). The value returned is expressed in “timer ticks.” If timers are decremented at a rate of 10 Hz then a count of 50 corresponds to 5 seconds. If the timer is in the stop state, the time remaining will correspond to either the initial delay (one shot or periodic with initial delay), or the period if the timer is configured for periodic without initial delay.

Figure - Timer State Diagram

(1) The “Unused” state is a timer that has not been created or has been “deleted.” In other words, µC/OS-IIdoes not know about this timer.

(2) When creating a timer or calling OSTmrStop(), the timer is placed in the “stopped” state.

(3) A timer is placed in running state when calling OSTmrStart(). The timer stays in that state unless it’s stopped, deleted, or completes its one shot.

(4) The “Completed” state is the state a one-shot timer is in when its delay expires.


OS_TMR

A timer is a kernel object as defined by the OS_TMR data type (see ucos_ii.h) as shown in the listing below:

The services provided by µC/OS-II to manage timers are implemented in the file os_tmr.c. Timer services are enabled at compile time by setting the configuration constant OS_TMR_EN to 1 in os_cfg.h.

Listing - OS_TMR data type
typedef  struct  os_tmr {                           (1)
    INT8U                OSTmrType;                 (2) 
    OS_TMR_CALLBACK_PTR  OSTmrCallback;             (3) 
    void                *OSTmrCallback;             (4) 
    OS_TMR              *OSTmrNextPtr;              (5) 
    OS_TMR              *OSTmrPrevPtr;                  
    OS_TICK              OSTmrMatch;                (6) 
    OS_TICK              OSTmrDly;                  (7) 
    OS_TICK              OSTmrPeriod;               (8) 
    CPU_CHAR            *OSTmrNamePtr;              (9) 
    OS_OPT               OSTmrOpt;                 (10) 
    OS_STATE             OSTmrState;               (11) 
} OS_TMR;

(1) In µC/OS-II, all structures are given a data type. In fact, all data types start with “OS_” and are all uppercase. When a timer is declared, you simply use OS_TMR as the data type of the variable used to declare the timer.

(2) The structure starts with a “Type” field, which allows it to be recognized by µC/OS-II as a timer. Other kernel objects will also have a “Type” as the first member of the structure. If a function is passed a kernel object, µC/OS-II is able to confirm that it is passed the proper data type. For example, if passing a message queue (OS_Q) to a timer service (for example OSTmrStart()) then µC/OS-II ill be able to recognize that an invalid object was passed, and return an error code accordingly.

(3) The  .OSTmrCallback  member is a pointer to a function that is called when the timer expires. If a timer is created and passed a  NULL  pointer, a callback would not be called when the timer expires.

(4) If there is a non-NULL .OSTmrCallback then the application code could have also specified that the callback be called with an argument when the timer expires. This is the argument that would be passed in this call.

(5) .OSTmrNextPtr and .OSTmrPrevPtr are pointers used to link a timer in a doubly linked list. These are described later.

(6) The .OSTmrMatch field contains a value that is compared to a running count. When the running count matches this value the timer is considered to have expired.

(7) The .OSTmrDly field contains the one-shot time when the timer is configured (i.e., created) as a one-shot timer and the initial delay when the timer is created as a periodic timer. The value is expressed in multiples of 1/OS_TMR_CFG_TICKS_PER_SEC of a second (see os_cfg.h).

(8) The .OSTmrPeriod field is the timer period when the timer is created to operate in periodic mode. The value is expressed in multiples of 1/OS_TMR_CFG_TICKS_PER_SEC of a second (see os_cfg.h).

(9) Each kernel object can be given a name for easier recognition by debuggers or µC/Probe. This member is simply a pointer to an ASCII string which is assumed to be NUL terminated.

(10) The .OSTmrOpt field contains options that are passed to OSTmrCreate().

(11) The .OSTmrState field represents the current state of the timer (see the figure in Timer States).


Even if the internals of the  OS_TMR  data type are understood, the application code should never access any of the fields in this data structure directly. Instead, you should always use the Application Programming Interfaces (APIs) provided with µC/OS-II.

Timer Task

OSTmr_Task() is a task created by µC/OS-II (assumes setting OS_TMR_EN to 1 in os_cfg.h) and its priority is configurable by the user through µC/OS-II’s configuration file app_cfg.h (see OS_TASK_TMR_PRIO). OS_TmrTask() is typically set to a low priority.

OSTmr_Task() is a periodic task and uses the same interrupt source used to generate clock ticks. However, timers are generally updated at a slower rate (i.e., typically 10 Hz or so) and thus, the timer tick rate is divided down in software. If the tick rate is 1000 Hz and the desired timer rate is 10 Hz then the timer task will be signaled every 100th tick interrupt as shown in the figure below.

Figure - Tick ISR and Timer Task relationship


The figure below shows timing diagram associated with the timer management task.

Figure - Timing Diagram

(1) The tick ISR occurs and assumes interrupts are enabled and executes.

(2) The tick ISR signals the tick task that it is time for it to update timers.

(3) The tick ISR terminates, however there might be higher priority tasks that need to execute (assuming the timer task has a lower priority). Therefore, µC/OS-IIruns the higher priority task(s).

(4) When all higher priority tasks have executed, µC/OS-II switches to the timer task and determines that there are three timers that expired.

(5) The callback for the first timer is executed.

(6) The callback for the second expired timer is executed.

(7) The callback for the third expired timer is executed.


There are a few interesting things to notice:

  • Execution of the callback functions is performed within the context of the timer task. This means that the application code will need to make sure there is sufficient stack space for the timer task to handle these callbacks.
  • The callback functions are executed one after the other based on the order they are found in the timer list.
  • The execution time of the timer task greatly depends on how many timers expire and how long each of the callback functions takes to execute. Since the callbacks are provided by the application code they have a large influence on the execution time of the timer task.
  • The timer callback functions must never wait on events because this would delay the timer task for excessive amounts of time, if not forever.
  • Callbacks should execute as quickly as possible.

Timers are inserted in a list by calling  OSTmrStart()  and, a timer must be created before it can be used. 


Listing - Creating and Starting a timer
OS_TMR  MyTmr1;
OS_TMR  MyTmr2;
 
 
void MyTmrCallbackFnct1 (void *p_arg)
{
    /* Do something when timer #1 expires */
}
 
 
void MyTmrCallbackFnct2 (void *p_arg)
{
    /* Do something when timer #2 expires */
}
 
 
void MyTask (void *p_arg)
{
    INT8U  err;
 
 
    while (DEF_ON) {
        :
        MyTmr1 = OSTmrCreate((INT32U         )1,
                             (INT32U         )0,
                             (INT8U          )OS_TMR_OPT_ONE_SHOT,
                             (OS_TMR_CALLBACK)MyTmrCallbackFnct1,
                             (void          *)0,
                             (INT8U         *)"My Timer #1",
                             (INT8U         *)&err);
        /* Check 'err" */
        OSTmrStart ((OS_TMR *)&MyTmr1,
                    (INT8U  *)&err);
        /* Check "err" */
        // Continues in the next code listing! 


The code below shows creating and starting another timer. This is performed “before” the timer task is signaled.


Listing - Creating and Starting a timer - continued
// Continuation of code from previous code listing.
        :
        :
        MyTmr2 = OSTmrCreate((INT32U         )1,
                             (INT32U         )0,
                             (INT8U          )OS_TMR_OPT_ONE_SHOT,
                             (OS_TMR_CALLBACK)MyTmrCallbackFnct2,
                             (void          *)0,
                             (INT8U         *)"My Timer #2",
                             (INT8U         *)&err);
        /* Check 'err" */
        OSTmrStart ((OS_TMR *)&MyTmr2,
                    (INT8U  *)&err);
        /* Check 'err" */
    }
}


When the timer task executes (see OSTmr_Task() in os_tmr.c), it starts by incrementing OSTmrTime and goes through the list of timers and checks to see which timer matches the OSTmrTime value.  Upon match, the timer manager executes the callback function associated with the timer and, if the timer is set to periodic, determines what the next match value is based on the .OSTmrPeriod (assuming periodic mode). If the timer is configured as a one-shot timer then the timer is removed from the list upon expiration.

Timer management occurs at the task level. The list is protected by locking the scheduler. Locking the scheduler impacts task responsiveness of other, higher priority tasks in your application.