Periodic Timers

Introduction

This code provides a timer system that allows many timers The Forth words in the user accessible group documented below are compatible with the code supplied with MPE's embedded targets, and with VFX Forth. This code assumes the presence of a global value TICKS which holds a time value incremented in milliseconds. The timebase is approximate. Granularity and jitter are affected by the timer ISR and the time taken by your own code to execute. By default, the ticker is set to run every 1..100 ms, usually defined by the EQUate TICK-MS. The source code is in the the file TIMEBASE.FTH.

The file DELAYS.FTH should be compiled after TIMEBASE.FTH. The code to start and stop the timebase system is part of the ticker interrupt system, which is compiled after DELAYS.FTH. If you need to write a new ticker interrupt handler, there will be examples to start from in the <CPU>\DRIVERS folder. The required compilation order is this:


multitasker (optional)
TIMEBASE.FTH (optional)
DELAYS.FTH
Ticker driver

The timer chain is built using a buffer area, and two chain pointers. Each timer is linked into either the free timer chain, or into the active timer chain.

All time periods are in milliseconds. Note that on a 32 bit system, these time periods must be less than 2^31-1 milliseconds, say 596 hours or 24 days, whereas if the code is on a 16 bit system, time periods must be less than 2^15-1 milliseconds, say 32 seconds.

The basics of timers

These basic words are defined for applications to use the timer system. Other words are detailed elswhere in this chapter.


START-TIMERS       \ -- ; must do this first
STOP-TIMERS        \ -- ; closes timers
AFTER              \ xt period -- timerid/0 ; runs xt once after period ms
EVERY              \ xt period -- timerid/0 ; runs xt every period ms
TSTOP              \ timerid -- ; stops the timer
MS                 \ period -- ; wait for period ms

After the timers have been started, actions can be added. The example below starts a timer which puts a character on the debug console every two seconds. Note that when using generic I/O, the output and input devices MUST be specified.


start-timers
: t        \ -- ; will run every 2 seconds
  console opvec !
  [char] * emit
;
' t 2 seconds every  \ returns timer id, use TSTOP to stop it

The item on stack is a timer handle, use TSTOP to halt this timer.

AFTER is very useful for creating timeouts, such as required to determine if something has happened in time. AFTER returns a timerid. If the action you are protecting happens in time, just use TSTOP when the action happens, and the timer will never trigger. If the action does not happen, the timer event will be triggered.

Considerations when using timers

All timers are executed within a single interrupt, and so all timer action words share a common user area. This has some impact on timer action words. Since you do not know in which order timer action words are executed, you must set up any USER variables such as BASE that you use, either directly or indirectly.

The interrupt that handles all the timers does not set IPVEC and OPVEC to a default value. If you use I/O words such as EMIT and TYPE within a timer action, you MUST set IPVEC and OPVEC before using the I/O. For the sake of other timer action routines that may still be using default I/O, it is polite to save and restore IPVEC and OPVEC in your timer action words.

Do not worry about calling TSTOP with a timerid that has already been executed and removed from the active timer chain; if TSTOP cannot find the timer, it will ignore the request.

Under some conditions, the execution time of all the timer routines may be longer than the requested period of the timer. In addition, the timer interrupt may be subject to jitter.

Implementation issues

The following discussion is relevant if you want to modify this code. Functionally equivalent code is provided with MPE's VFX Forth systems. In the Windows environment, timer interrupts are implemented by callbacks and critical sections.

By default, the word DO-TIMERS is run from within the periodic timer interrupt. If interrupts are not re-enabled after resetting the timer interrupt, you may have latency issues if a number of timers is used, or if one of the timer routines takes a considerable time, or if you want to use KEY or EMIT in the timer action. In this case, it would be better to set up the timer routine to RESTART a task which calls DO-TIMERS,e.g.


: TIMER-TASK    \ --
  <initialise>
  BEGIN
    DO-TIMERS STOP
  AGAIN
;

Such a strategy also permits you to use a fast interrupt, say 1 ms, for the clock, and to trigger the TIMER-TASK every say 32 ms. Another strategy is to leave DO-TIMERS in the interrupt routine and for each timer action be to put a different character into a circular buffer (and to restart a task).


16 cqueue: timerQ

: TimerAction    \ --
  <initialise>
  begin
    case  timerQ cqueue>
      [char] A of  ...  endof
      [char] B of  ...  endof
           ...
    endcase
  again
;

Providing that the queue is big enough, no events are lost and event order is preserved. However, an action that takes 20 seconds may/will delay all subsequent actions.

The best handler structure depends heavily on your application design. You need to consider things like

As with all software design, it's usually a trade-off. The default "all in the interrupt" design is good enough for many users. For all the rest, adding a task is a good first step.

Timebase glossary

0 value ticks   \ -- addr ; holds timer count
Get current clock value in milliseconds.

#8 constant #timers     \ -- n ; maximum number of timers
A constant used at compile time to set the maximum number of timers required. Each timer requires RAM as defined by the ITIMER structure.

: do-timers     \ --
Process all the timers in the chain

: after         \ xt period -- timerid/0 ; xt is executed once,
Starts a timer that executes once after the given period. A timer ID is returned if the timer could be started, otherwise 0 is returned.

: every         \ xt period -- timerid/0 ; periodically
Starts a timer that executes every given period. A timer ID is returned if the timer could be started, otherwise 0 is returned. The returned timerID can be used by TSTOP to stop the timer.

: tstop         \ timerid --
Removes the given timer from the active list.