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 EQU
ate 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.
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.
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.
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.
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.