Tasks

Introduction

Processes outlines a few basics about processes, including their implementation based on coroutines or tasks. Below, I describe tasks.

Tasks.mod

Module Tasks.mod:

  • declares TYPE Task
  • 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.

Simple Example

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, init, base, and tx.

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.
  • Run sets up the task, and installs it;
  • init is the first handler; it simply prints a message, but in a real task, would do all the required initialisations of the process state etc;
  • init then uses Tasks.Next to set the subsequent handler to be called by the scheduler;
  • init also sets the task to be ready immediately, ie. to skip the timer for the next invocation, using Tasks.SetReady;
  • base simulates 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).
  • init uses Tasks.Next to set the subsequent handler, tx, selecting from the array of handlers.
  • init changes the timer used via Tasks.SetTimer to be used by the transmit handler.
  • tx fills the serial buffer, then gets invoked repeatedly by the 5 ms timer until all requested data is transmitted
  • tx sets again sets the base handler, as soon as the transmission is complete

Note that

  • 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 base.

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.
  • parameter n: number of chars to transmit, passed as data
  • parameter ix: start to transmit from this index into data

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.

Task Management

Most of the changes to a task can only be done by the task itself, such as:

  • setting the next handler: Tasks.Next
  • setting scheduling timers: Tasks.SetTimer
  • setting task priority (not used above): Tasks.SetPrio
  • setting immediate ready, skipping the timer: Tasks.SetReady
  • setting a delay, overriding the timer: Tasks.DelayMe

Aside from the obvious initialisation and installation of the task (Tasks.Init and Tasks.Install), the only exception to the above rule are

  • suspending a task, ie. to prevent its scheduling: Tasks.Suspend
  • (re-)enabling a task to be scheduled: Tasks.Enable

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.Suspend and Tasks.Enable cannot make any such changes – and these two facilities will require some more thought and scrutiny.

Error Handling

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.

Scheduling

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.

Possible Extension

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 StateData in 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.