Scheduling (Device Signalled)

Introduction

Unless interrupts are used, peripheral devices have to be polled to enquire their state, for example the readiness to receive more data, or the availability of a measurement from the controlled system (environment). Polling by busy-waiting in the control process itself is usually not applicable in a multi-process program.

Considering interrupt-driven designs, the RISC processor has exactly one interrupt input line. With more than one peripheral device, this would mean to wire all interrupt signals together, and distinguish the actually occurring interrupt in the handler software, for example by polling all relevant devices' state via peripheral IO upon interrupt, or via an extended interrupt controller in the FPGA.

Please note that this shall in no way be read as argument against interrupt-driven designs in general. It’s an alternative approach, keeping strictly to the concept of cooperative scheduling. Real-time constraints can only be met if the timely handling of the device signals can be guaranteed with the overall process schedule – a basic principle of cooperative scheduling without interrupts.

Overview

The basic idea is to delegate the polling to the scheduler, but in a very efficient manner, making use of FPGA logic. As with process timing, the scheduler – ie. the Oberon Loop – only checks if the ready bit for a process is set, at a defined IO address. Note that the scheduling algorithm here does not use process priorities, as described here.

Oberon.Loop is extended accordingly:

MODULE Oberon;

  (* ... *)

  VAR cp: Process; (* the current process, in a linked ring of processes *)

  PROCEDURE Loop;
  BEGIN
    IF Console.Available() THEN
      (* command and upload handling *)
    ELSE
      IF cp # NIL THEN
        cp := cp.next;
        IF ProcDevSig.ProcessReady(cp.pcNo) THEN  (* device signal handling *)
          ProcDevSig.ResetSignals(cp.pcNo);
          cp.state := Active;
          Coroutines.Transfer(loop, cp.cor) 
        ELSIF ProcTimers.ProcessReady(cp.pcNo) THEN   (* process timing *)
          ProcTimers.SetPeriod(cp.pcNo, cp.period);
          IF cp.state # Suspended THEN
            cp.state := Active;
            Coroutines.Transfer(loop, cp.cor)
          END
        END
      END
    END
  END Loop;

  (* ... *)

END Oberon.

ProcDevSig.mod

ProcDevSig.mod provides the following API to control the FPGA-based controller.

MODULE ProcDevSig;
  IMPORT SYSTEM;

  CONST
    DevSigAdr = -144; (* write awaited device signal *)
    PrReadyAdr = DevSigAdr;   (* read process ready *)
    ProcNumShift = 27;

  PROCEDURE* SetSignal*(pn: INTEGER; devSig: INTEGER);
  BEGIN
    SYSTEM.PUT(DevSigAdr, LSL(pn, ProcNumShift) + LSL(1, devSig))
  END SetSignal;

  PROCEDURE* ResetSignals*(pn: INTEGER);
    SYSTEM.PUT(DevSigAdr, LSL(pn, ProcNumShift))
  END ResetSignals;

  PROCEDURE* ProcessReady*(pn: INTEGER): BOOLEAN;
    RETURN SYSTEM.BIT(PrReadyAdr, pn)
  END ProcessReady;

END ProcDevSig.

As with the process timers, each process has its own device signal controller. The current FPGA design allows for up to 32 processes.

The IO address for each controller is the same, the number of the addressed timer is transmitted as bits [31:27] with the corresponding data, ie. the signal number (as bit). Obviously, this limits the number of possible device signals to 27.

Device Timeout

Peripheral devices can fail to respond, hence we need a timeout, else a process might await a device signal forever. Waiting “forever” must be allowed as well, though, for example with an RS232 device listening to an input terminal.

Await Device Signal Machinery

Below the procedure in Oberon.mod, to be called from a process, to await a specific device signal. Device signals are numbered, according to their wiring in the FPGA. A module DevSignals.mod represents that wiring in Oberon code.

MODULE Oberon;

  PROCEDURE AwaitDevSig(devSig: INTEGER; timeout: INTEGER; VAR res: INTEGER);
  BEGIN
    ASSERT(timeout >= 0);
    ProcDevSig.SetSignal(cp.cpNo, devSig);
    IF timeout = 0 THEN   (* no timeout requested, ie. wait forever *)
      cp.state := AwaitDevSig
    ELSE
      ProcTimers.SetDelay(cp.cpNo);   (* use process timers for timeout *)
      cp.state := AwaitDevSigTo
    END;
    Coroutines.Transfer(cp.cor, loop);
    res := cp.retVal (* see below *)
  END AwaitDevSig;

END Oberon.

Oberon.Loop With Device Timeout

Here’s the extended Oberon.Loop, handling process timing, awaiting device signals, and device signal timeouts.

MODULE Oberon;

  PROCEDURE Loop;
  BEGIN
    IF Console.Available() THEN
      (* command and upload handling *)
    ELSE
      IF cp # NIL THEN
        cp := cp.next;
        IF ProcDevSig.ProcessReady(cp.pcNo) THEN
          ProcDevSig.ResetSignals(cp.pcNo);
          IF cp.state = AwaitDevSigTo THEN
            ProcTimers.CancelDelay(cp.pcNo)
          END;
          cp.state := Active;
          cp.retVal := OK;
          Coroutines.Transfer(loop, cp.cor)
        ELSIF ProcTimers.ProcessReady(cp.pcNo) THEN
          ProcTimers.SetPeriod(cp.pcNo, cp.period);
          cp.retValue := OK;
          IF cp.state = AwaitDevSigTo THEN
            ProcDevSig.ResetSignals(cp.pcNo);
            cp.retVal := Timeout
          END;
          IF cp.state IN -{Suspended, AwaitDevSig} THEN
            cp.state := Active;
            Coroutines.Transfer(loop, cp.cor)
          END
        END
      END
    END
  END Loop;

END Oberon.

As described here, delays take precedence over period timing, hence we know a process awaiting a signal got a timeout when entering the ELSIF branch.

The process TYPE has an additional field, retValue, to allow to report back the outcome of awaiting a device signal, namely signal received or timeout. The return value is an integer, with OK = 0.

RS232 Driver

We now can write an RS232 driver making use of device signals, for example for an FGPA-buffered RS232 device.

MODULE RS232b;

  IMPORT SYSTEM, Oberon, RS232dev, Out;

  CONST
    TxFull = RS232dev.TXBF;   (* transmit buffer full *)
    OK = 0; (* no error *)

  PROCEDURE PutChars*(dev :RS232dev.Device; data: ARRAY OF CHAR; n: INTEGER; VAR res: INTEGER);
    VAR cnt: INTEGER;
  BEGIN
    cnt := 0;
    WHILE cnt < n DO
      Out.String(">>"); Out.Ln; (* not part of the driver, just for the test below *)
      WHILE (cnt < n) & ~SYSTEM.BIT(dev.statusAdr, TxFull) DO
        SYSTEM.PUT(dev.dataAdr, ORD(data[cnt]));
        INC(cnt)
      END;
      IF cnt < n THEN
        Oberon.AwaitDevSig(dev.txSig, dev.txTimeout, res);
        IF res # OK THEN cnt := n END
      END
    END
  END PutChars;

  (* ... *)

END RS232b.

This driver uses the device type in module RS232dev.mod, which forms the configuration layer for all RS232 devices instantiated in the FPGA. See below how it is instantiated and initialised, including the device signals.

MODULE RS232dev;

  TYPE
    Device* = POINTER TO DeviceDesc;
    DeviceDesc* = RECORD(Texts.TextDeviceDesc)
      dataAdr*, statusAdr*: INTEGER;
      txTimeout*, rxTimeout*: INTEGER;
      txSig*, rxSig*: INTEGER
    END;

END RS232dev.

The timeouts can be set to different values for transmit and receive, but are assumed to be the same for all uses of this specific device. This is less flexible than a specific per-use timeout, but keeps the API to the device driver narrower from the process programmer’s point of view, and the timeout is consistent and easy to change. A design decision that might need to be reconsidered, if only for specific application cases.

Demo

The following small test program demonstrates the use and effect of the device signal equipped RS232 device. The transmit buffer of the selected device RS232dev.Dev3 holds 31 characters, the receive buffer 15 characters.

MODULE TestProcessIO;

  IMPORT Oberon, DevSignals, RS232dev, RS232 := RS232p, Out;
  
  CONST
    TxTimeout = 10;
    RxTimeout = 0;    (* no timeout *)
  
  VAR
    p1, p2, p3: Oberon.Process;
    s1, s2, s3: ARRAY 1024 OF BYTE;
    dev: RS232dev.Device;
    data1: ARRAY 128 OF CHAR;
    data2: ARRAY 8 OF CHAR;
    i: INTEGER;
    
  PROCEDURE p1c;
    CONST NumChars = 128;
    VAR res: INTEGER;
  BEGIN
    REPEAT
      Out.String("p1--"); Out.Ln;
      RS232.PutChars(dev, data1, NumChars, res);
      ASSERT(res = 0);
      Out.String("p1--"); Out.Ln;
      Oberon.NextProc
    UNTIL FALSE
  END p1c;
  
  PROCEDURE p2c;
    CONST NumChars = 4;
    VAR res: INTEGER;
  BEGIN
    REPEAT
      RS232.GetChars(dev, data2, NumChars, res);
      data2[NumChars] := 0X;
      Out.String(data2); Out.Ln;
      Oberon.NextProc
    UNTIL FALSE
  END p2c;
  
  PROCEDURE p3c;
  BEGIN
    REPEAT
      Out.String("p3"); Out.Ln;
      Oberon.NextProc
    UNTIL FALSE
  END p3c;
   
  PROCEDURE Run1*;
  BEGIN
    Oberon.InstallProc(p1);
    Oberon.InstallProc(p3)
  END Run1;
  
  PROCEDURE Run2*;
  BEGIN
    Oberon.InstallProc(p1);
    Oberon.InstallProc(p2)
  END Run2;
  
  PROCEDURE Stop*;
  BEGIN
    Oberon.RemoveProc(p1);
    Oberon.RemoveProc(p2);
    Oberon.RemoveProc(p3)
  END Stop; 
  
BEGIN
  NEW(dev); RS232dev.Init(dev, RS232dev.Dev3);
  RS232dev.SetDevSignals(dev, DevSignals.RS232dev3TxBufEmpty, DevSignals.RS232dev3RxBufNonEmpty, TxTimeout, RxTimeout);
  NEW(p1); Oberon.InitProc(p1, p1c, s1, 1000, 0);
  NEW(p2); Oberon.InitProc(p2, p2c, s2, 1000, 0);
  NEW(p3); Oberon.InitProc(p3, p3c, s3, 5, 0);
  FOR i := 0 TO 127 DO
    data1[i] := CHR((i MOD 10) + ORD("1"))
  END
END TestProcessIO.

Process p1 simply repeatedly outputs 128 characters to RS232dev.Dev3, which is connected to another serial terminal than the Astrobe console. p2 repeatedly reads four characters from RS232dev.Dev3, and prints them to the Astrobe console. p3 serves as “sentinel” process to check if it actually gets scheduled while the other processes are awaiting the device signals.

Run1

When we execute Run1, we get in the Astrobe terminal:

p3
p1--
>>
>>
p3
>>
>>
p1--
p3

The temporary test code in the RS232 driver prints “»” when p1 starts to fill the buffer in the FPGA, and the RS232 device starts to transmit concurrently. Because p1 prints 128 characters, and we have a 31 character transmit buffer, p3 can sneak in its output while p1 awaits the second TxEmpty device signal. p3 runs with a period of 5 milliseconds. Note that p1 can transmit 32 characters per RS232.PutChars call, because the first character in the buffer will have been consumed by the RS232 device when the process has finished putting 31 characters into the buffer, allowing one more.

Run2

When we execute Run2, and enter, say “1234” into RS232dev.Dev3, we simply see p2 printing these characters in the Astrobe terminal, as soon as all four requested characters have been entered, all while p1 does its output thing in parallel. Note that the RS232 driver could easily be modified to define and implement other conditions to return the read chars, such as CR entered to read a full line of input, without the need to poll the device for each single character.

Timeouts

The timeout for reading from RS232dev.Dev3 is set to RxTimeout = 0, that is, the timeout feature is disabled, and the process waits forever until the requested four characters are read.

The timeout for writing to the terminal is set to TxTimeout = 10 milliseconds, ie. above what is required to write 31 characters at 115,200 Baud (2.7 ms). If we lower this timeout to 2 milliseconds we get the trap caused by the violation of the ASSERT statement in p1, as expected. Handling the timeout with an ASSERT is just used as simple example here. p1 could also retry, or pursue other recovering strategies.