Processes outlines a few basics about processes, including their implementation based on coroutines or tasks. Below, I describe tasks.
- declares TYPE
- provides all procedures for task-based processes
- implements the scheduler
- handles resets and restarts, including the watchdog
One process is usually implemented by one
Task, which may use different
handlers. Conceptually, I like different handlers for the different states of the process, but a task may well use only one handler and cover all process states within that handler.
Let’s look at a simple example to explain a few basics. It simply prints a buffer of chars to a serial device, using a dedicated handler to drive the buffered serial device. The task starts using a specific handler for initialisation. Hence, we have a task-based process, using three handlers,
MODULE M; IMPORT Tasks, Out, RS232dev, RS232 := RS232bt; CONST Base = 0; Transmit = 1; Ttype = Tasks.Essential; Prio = 1; BaseTimer = 7; (* 1 s scheduler timer *) TxTimer = 0; (* 5 ms scheduler timer *) VAR t: Tasks.Task; states: ARRAY 2 OF Tasks.Handler; (* state handlers *) dev: RS232dev.Device; data: ARRAY 128 OF CHAR; (* test data *) i: INTEGER; txData: RECORD (* transmit data *) data: ARRAY 128 OF CHAR; num, cnt: INTEGER END; PROCEDURE base; (* base handler, running once per second *) VAR i, now: INTEGER; id: Tasks.TaskId; BEGIN now := Tasks.Time(); Tasks.Id(id); Out.Ln; Out.String("==="); Out.Int(now, 10); Out.String(" "); Out.String(id); Out.Ln; FOR i := 0 TO 127 DO (* prepare tx data *) txData.data[i] := data[i] (* just copy test data for demo *) END; txData.num := 128; txData.cnt := 0; Tasks.Next(states[Transmit]); (* continue with tx handler *) Tasks.SetTimer(TxTimer) (* set tx handler on 5 ms timer *) END base; PROCEDURE tx; (* serial transmit handler, running once per 5 ms *) BEGIN RS232.PutChars(dev, txData.data, txData.num, txData.cnt); IF txData.cnt >= txData.num THEN Tasks.Next(states[Base]); (* when all data transmitted, continue with base handler *) Tasks.SetTimer(BaseTimer) (* return to 1 s timer *) END (* else continue with tx handler *) END tx; PROCEDURE init; (* init handler *) VAR now: INTEGER; BEGIN now := Tasks.Time(); Out.String("init"); Out.Int(now, 10); Out.Ln; (* more initialisations here *) Tasks.Next(states[Base]); (* continue with base handler *) Tasks.SetReady (* skip timing, continue immediately *) END init; PROCEDURE Run*; VAR res: INTEGER; BEGIN Tasks.Init(t, init, Ttype, Prio, BaseTimer, "ttx"); Tasks.Install(t, res); ASSERT(res = 0) (* task could be installed, ie. free slot available *) END Run; BEGIN NEW(t); states[Base] := base; states[Transmit] := tx; NEW(dev); RS232dev.Init(dev, RS232dev.Dev1); FOR i := 0 TO 127 DO (* set up test data *) data[i] := CHR((i MOD 10) + ORD("1")) END END M.
Runsets up the task, and installs it;
initis the first handler; it simply prints a message, but in a real task, would do all the required initialisations of the process state etc;
Tasks.Nextto set the subsequent handler to be called by the scheduler;
initalso sets the task to be ready immediately, ie. to skip the timer for the next invocation, using
basesimulates preparing transmit data by simply copying the test data, and setting up the other parameters required for the transmit (see below for the RS232 driver).
Tasks.Nextto set the subsequent handler,
tx, selecting from the array of handlers.
initchanges the timer used via
Tasks.SetTimerto be used by the transmit handler.
txfills the serial buffer, then gets invoked repeatedly by the 5 ms timer until all requested data is transmitted
txsets again sets the
basehandler, as soon as the transmission is complete
- all handlers are stored in procedure variables, so handlers that appear later in the code text can be referred to;
- parameters between handlers are passed using module variables;
- the use of the 5 ms timer for the transmission does not set off the timing for the base handler, it will be invoked exactly one second after the last invocation, as a variant of fixed timing scheduling is used (assuming that the transmission does not last longer than one second, of course!).
Transmit Handler and RS232 Driver
For now, scheduling using device signals is not implemented, hence the transmit handler
tx relies on time-triggered operation, although on a faster scheduling timer than the “main” handler
Here’s the RS232 driver adapted to be used by the inherently stateless task handlers.
MODULE RS232bt; PROCEDURE PutChars*(dev: RS232dev.Device; data: ARRAY OF CHAR; n: INTEGER; VAR ix: INTEGER); VAR num, cnt: INTEGER; BEGIN ASSERT(n <= LEN(data)); ASSERT(n > 0); ASSERT(ix < n); cnt := 0; num := n - ix; WHILE (cnt < num) & SYSTEM.BIT(dev.statusAdr, TxNotFull) DO SYSTEM.PUT(dev.dataAdr, ORD(data[ix])); INC(ix); INC(cnt) END END PutChars; END RS232bt.
n: number of chars to transmit, passed as
ix: start to transmit from this index into
As the above example demonstrates, the state of the serial driver – or the state of the transmission – is kept in VAR parameter
ix, which represents a module variable in the example module.
Most of the changes to a task can only be done by the task itself, such as:
- setting the next handler:
- setting scheduling timers:
- setting task priority (not used above):
- setting immediate ready, skipping the timer:
- setting a delay, overriding the timer:
Aside from the obvious initialisation and installation of the task (
Tasks.Install), the only exception to the above rule are
- suspending a task, ie. to prevent its scheduling:
- (re-)enabling a task to be scheduled:
With tasks being the dynamic building blocks of a control program, allowing one task to make changes to another one would be analogous to allowing direct cross-module manipulations of module interna, with direct impact on the execution logic of the targeted task.
Tasks.Enable cannot make any such changes – and these two facilities will require some more thought and scrutiny.
Task.Init takes a parameter denoting the first handler to run. This is assumed to be the initialisation handler, and it will be separately retained in the task’s data, so that it can be invoked again by the error handling machinery, should a re-initialisation of the task become necessary. Similarly, the initialisation handler can store a separate recovery handler, which can be invoked by the error handling mechanism.
For tasks with only one handler, either because the task is simple in nature, or the only handler covers all possible states, the init handler is stored as “normal” run handler, as init handler, and also as recovery handler (by
Tasks.Install). Hence, conceptually and from the programmer’s point of view, for tasks with only one handler, as for example used in (Embedded) Oberon, there’s no difference and overhead.
More about error detection, handling, and recovery in a forthcoming document.
I’ll describe task scheduling in a forthcoming document.
Tasks All the Way Down
I have also converted the command handler (which also handles uploads) and the remote command handler to tasks. Bottom line, apart from the scheduler itself, all that “happens” in Oberon RTS is executed as a task.
Keeping the process (or task) state in module variables invariably means that only one instance of the task can be created. An easy extension would be to declare an empty record type such as
Tasks.mod, include a pointer to such record in the task’s data, and allow the task’s handler to access this record, which would be a type extension of
StateData to contain the task’s actual state data.