Sometimes, timers are easier Mar 2017
Multi-tasking is a great mechanism, but there is a drawback: each task needs its own stack. In the case of Forth, it’s even worse because each task needs both a return stack and a data stack.
In its current configuration, multi.fs is set to 64 elements on each stack, which translates to a whopping 512 bytes of RAM needed (plus about 20 bytes for the task descriptors) - per task!
On an embedded µC, with 8 .. 20 KB of RAM, this really isn’t very convenient.
Stacks are needed when you want to write each task as if it ruled the world, so to speak: run in loops, and leave state on the data and return stacks while going through the logic of the code.
But there is another ways to get a lot of independent work done: callbacks. And while forcing callbacks onto an application (as NodeJS does) is not a good idea, there are nevertheless many cases where just some callbacks can easily handle things.
The difference between callbacks and tasks comes down to not leaving state on the stack: in a callback world, something triggers your code, it does its thing, and then it returns. If it needs to do more, it can schedule additional callbacks, usually via some sort of timer mechanism. The key here is the word “return”: callbacks cannot leave stuff on either stack, they need to manage all state between invocations elsewhere, i.e. in variables and buffers.
Meet the
timed
module and it’s documentation,
both written and generously contributed by Thomas Lohmüller
(@tht on GitHub). With timed
, calling your code some
time in the future, either once or periodically, is a piece of cake.
Here is the multi-tasking demo from the previous article, now using a timer instead of a task:
include ../flib/any/timed.fs
PA12 constant LED1
OMODE-PP LED1 io-mode! ;
timed-init
: blink ( -- ) LED1 iox! ; \ toggle LED1
' blink 200 0 call-every \ set up a periodic callback
That’s all there is to it. By default, we have eight timer “slots”, and in the
above code, we’ve set up a periodic callback every 200 ms in slot zero
(”' blink
” means “the address of blink
”).
For a slightly larger example with 4 LEDs blinking at different rates, see ex/timers.fs.
But here’s the interesting bit of how this has been implemented:
- the
timed
package defines and starts a task (!) to run in the background - so we can still stop all timers by entering
singletask
, as with tasks - timing will only be accurate if all other tasks play nice and call
pause
regularly
In this demo there is not much difference evident, but when you have
a lot of activities and timeouts, the timed
module can help manage it
all. It can be configured to handle any number of timers (and it’s much more
lightweight than using task stacks: a timer uses only 16 bytes).
So what this design does, is simply to merge the two concepts: we’re still using
the cooperative multi-tasker to create the illusion of parallelism, but now
the simple one-shot and periodic callbacks are all contained within a single task,
avoiding the per-task memory and switching overhead. The timed
background task
merely keeps track of what to call back, and when, and offers three simple words
to manage these activities in whatever way you need:
call-after
sets up a one-shot call in a specified slotcall-every
sets up a periodic call in a specified slotcall-never
cancels the callback associated with a specified slot
See the documentation page for examples and additional details.
With multi-tasking and timers, we now have some nice tools to deal with more complex tasks.