Oberon.mod Restructured

Overview

Oberon is arguably the central module of the system, as Oberon.Loop is the dispatcher of all activities, be it starting a command, or invoking a task. The body of Oberon.mod is also the entry point for the Outer Core, after the Inner Core was loaded by the boot loader, and initialised by Modules. Consequently, the extensions to Embedded Oberon for Oberon RTS for scheduling and run-time error detection and recovery, as well as the implementation of the more comprehensive process concept were focused on module Oberon. Now it was time to break that growing module up into more suitable and manageable units.

Command and Upload Process

With two CPUs, and a communication channel in-between, multiple processes on one CPU might want to use that channel. Hence, an arbitration and lockout mechanism is required, such as a semaphore or mutex. If also user actions, ie. commands, shall be able to send messages to the other CPU, the user interaction needs to be integrated into this arbitration scheme.

However, user interaction is handled separately from Oberon RTS processes, or tasks in (Embedded) Oberon. Oberon.Loop has the following structure:

MODULE Oberon;

  PROCEDURE Loop*;
    (* VAR *) 
  BEGIN
    REPEAT
      IF Console.Available() > 0 THEN
        Console.Read(ch);
        IF ch = REC THEN
          (* upload file *)
        ELSE
          (* read and execute command *)
        END
      ELSE
        (* schedule process or task *)
      END
    UNTIL FALSE
  END Loop;

END Oberon.

With this structure, the synchronisation for accessing shared resources between the separate handling of commands and uploads on the one hand, and control processes on the other, and would require special program, and possibly hardware, logic. It’s much easier to turn the problem around and implement command and upload handling as process as well.

Oberon.Loop then reads:

MODULE Oberon;

  PROCEDURE Loop*;
    (* VAR *) 
  BEGIN
    REPEAT
      (* schedule process or task *)
    UNTIL FALSE
  END Loop;

END Oberon.

The command and upload process simply implements the code from the original Loop, and is extracted into a separate module Cmds.mod, as there is no need to keep it inside Oberon.mod – the process is thus implemented as any other (control) process. The command process is loaded and started for CPU 0 only, ie. the system that interacts with the Astrobe console, in the body of module Oberon.

The command handler is currently implemented as time-triggered process. The serial interface to the Astrobe console is buffered, with a 256 character receive buffer, ie. the CPU is not involved in receiving the command strings up to this length from the console. Transmitting 256 characters at 115,200 Baud takes about 22 ms, hence checking the buffer every 20 ms would be sufficient to receive even longer command strings. For command strings up to 256 characters, which is the limit of the Astrobe console “command line”, checking every 100 ms is plenty fast from a user experience point of view.

Processes.mod

Oberon.mod contains the TYPE declaration of a process, all the required procedures for initialisation and installation, and for scheduling, such as delays, awaiting device signals, and so on, as a replacement for the rather basic task concept and management of (Embedded) Oberon.

From the start, my intention was to try and implement other process concepts than the currently used coroutines. Coroutines are easy to grasp, allowing to experiment with scheduling, error detection and handling, and so on, in a and straightforward manner. Also, their implementation and the related context switches are very lightweight, thanks to how the compiler generates code. The main drawback is the need for separate stack space for each coroutine, while a task-based implementation only requires one stack for all processes.

Another process concepts, say, based on tasks, might require a slightly different process management and scheduling API. For example, a non-trivial task-based process will require some form of state machinery, and unless the only task-procedure uses internal state dispatching (eg. CASE, IF-ELSIF), there will be more than one tasks per process, and each task will need to “tell” the scheduler how to continue. A coroutine just yields, and will continue from the same point when invoked again.

All process handling is now extracted into a new module Processes.mod, including the scheduler itself, as well as the process and system reset and restart facilities, and the watchdog. Processes.Loop is started from the body of Oberon at the end of the system loading sequence. Going forward, reset and restart, including the watchdog, can possibly be moved back into Oberon, if a unified concept and implementation allows this.

Bottom Line

In lieu of one module Oberon.mod, we now have three modules:

  • Oberon.mod

    • loaded from Modules
    • body is the final phase of the system loading sequence
    • checks for repeated system restarts
    • executes any OnStartup commands
    • loads Commands module
    • concludes the dual CPU shared IO space arbitration and lockout
    • starts the scheduler Processes.Loop
    • contains the trap and abort handlers
  • Commands.mod

    • reads and executes commands from the Astrobe console
    • handles file uploads
    • only runs on CPU 0
  • Processes.mod

    • declares TYPE Process
    • contains all procedures for process management
    • implements the scheduler
    • handles resets and restarts, including the watchdog
    • contains the garbage collector handler GC