EmbeddedRelated.com
Memfault Beyond the Launch

A simple software timer system

March 31, 20132 comments Coded in C

MCUs typically include one or two hardware timers that can be used for various purposes.  A moderately sized application may need to keep track of elapsed time in dozens of places.  One approach is to observe the hardware timer for the passage of a prescribed amount of time past a previously saved value, but this is tedious.  More analogous to timers in higher-level languages such as Java, a timer system that allows timer variables to be registered and then serviced automatically from the system's timekeeping interrupt leads to simpler code.  This implementation is quite simple and consists of three calls:

  1. A call to register timers that are to be decremented every 10 milliseconds
  2. A call to register timers that are to be decremented every 100 milliseconds (1/10th the frequency of the primary timers) 
  3. A function to call every 10ms from the system's periodic interrupt.  (The interrupt should provide the prescaling.)

Of course, there is no fundamental reason why only 10ms or 100ms timers are supported other than that thes are common and useful intervals that allow a wide range of timing using 8-bit variables.  The use of 8-bit variables allows atomic access on 8-bit MCUs.

/**
 * @file
 * Software timer facility.
 *
 * This module implements an unlimited number of 8-bit down-counting 10ms and 
 * 100ms timers.  Timers are actually held in various places by the application
 * code and are registered with this module for service from the system's 
 * timekeeping interrupt.
 *
 * A down-counting timer starts out set to a time interval and is
 * automatically decremented via the system's periodic interrupt.  Check for a
 * zero value to know when the timer has expired:
 *
 * <pre>uint8_t my_timer = 10;
 * timer_register_100ms(&my_timer);
 *
 * for (;;)
 * {
 *   if (my_timer == 0)
 *   {
 *     do_something();
 *     my_timer = 10;
 *   }
 * }</pre>
 *
 * Down-counting timers are restricted to 8 bits so that they can be
 * atomically manipulated outside interrupt code on 8-bit architectures
 * without resorting to disable interrupts.
 *
 * @warning All variables used as timers must be declared
 *          <code>volatile</code>, because they are modified from an interrupt
 *          context that may not be understood by the compiler.  GCC in
 *          particular is known to optimize away timer variables that aren't
 *          declared <code>volatile</code>.
 *
 * <h2>Configuration</h2>
 * The number of available 10ms and 100ms timer slots is set using
 * {@link MAX_100MS_TIMERS} and {@link MAX_10MS_TIMERS}.
 */

#include <stdlib.h>    /* for NULL */
#include <stdint.h>    /* uint8_t, etc. */
#include <stdbool.h>   /* bool type, true, false */

#include "timer.h"

/** Maximum number of 100ms timers that can be registered. */
#define MAX_100MS_TIMERS 10

/** Maximum number of 10ms timers that can be registered. */
#define MAX_10MS_TIMERS  10

/** The polling frequency for the 10ms timers is scaled by this factor to
    service the 100ms timers. */
#define PRESCALE_100MS   10

/* ------------------------------------------------------------------------ */

/** 10ms timer array.  These are pointers to the actual timers elsewhere in
    the application code. */
static volatile uint8_t *timers_10ms [MAX_10MS_TIMERS];

/** 100ms timer array.  These are pointers to the actual timers elsewhere in
    the application code. */
static volatile uint8_t *timers_100ms [MAX_100MS_TIMERS];

bool timer_register_10ms (volatile uint8_t *t)
{
  uint8_t k;
  
  for (k = 0; k < MAX_10MS_TIMERS; ++k)
  {
    if (NULL == timers_10ms[k])
    {
      /* Success--found an unused slot */
      timers_10ms[k] = t;
      return false;
    }
  }
  
  /* Failure */
  return true;
}

bool timer_register_100ms (volatile uint8_t *t)
{
  uint8_t k;
  
  for (k = 0; k < MAX_100MS_TIMERS; ++k)
  {
    if (NULL == timers_100ms[k])
    {
      /* Success--found an unused slot */
      timers_100ms[k] = t;
      return false;
    }
  }
  
  /* Failure */
  return true;
}

void timer_poll (void)
{
  static uint8_t prescaler = PRESCALE_100MS;
  volatile uint8_t *t;
  uint8_t k;
  
  /* Service the 10ms timers */
  for (k = 0; k < MAX_10MS_TIMERS; ++k)
  {
    t = timers_10ms[k];
    
    /* First NULL entry marks the end of the registered timers */
    if (t == NULL)
    {
      break;
    }
    
    if (*t > 0)
    {
      -- *t;
    }
  }
  
  /* Now divide the frequency by 10 and service the 100ms timers every 10th
     time through. */
  if (--prescaler == 0)
  {
    prescaler = PRESCALE_100MS;

    for (k = 0; k < MAX_100MS_TIMERS; ++k)
    {
      t = timers_100ms[k];
      
      if (t == NULL)
      {
        break;
      }
      
      if (*t > 0)
      {
        -- *t;
      }
    }
  }
}

/* Header file */
#if !defined(TIMER_H)
#define TIMER_H

/**
 * @file
 */

#include <stdbool.h>
#include <stdlib.h>

/**
 * Registers a 10-millisecond timer for service.
 *
 * @param[in]  t  pointer to the variable used for timing
 *
 * @retval     true   if registration failed
 * @retval     false  if registration succeeded (normal return)
 */
bool timer_register_10ms (volatile uint8_t *t);

/**
 * Registers a 100-millisecond timer for service.
 *
 * @param[in]  t  pointer to the variable used for timing
 *
 * @retval     true   if registration failed
 * @retval     false  if registration succeeded (normal return)
 */
bool timer_register_100ms (volatile uint8_t *t);

/**
 * Maintains all registered timers.
 *
 * This function should be called from a stable 10-millisecond time base,
 * preferably from an interrupt.
 */
void timer_poll (void);

#endif /* TIMER_H */

Memfault Beyond the Launch