Abstraction & Configuration Layer for Devices

Introduction

The Oberon code contains many direct references to FPGA implementation specifics, too many for my taste, in particular IO addresses as well as control and status bits. While it’s not a goal to rectify this situation in general, extensions and changes should refrain from this design and coding approach, and introduce simple abstraction, or configuration, modules that represent the hardware specifics.

Goal

Ideally, a change of the FPGA implementation should only require to recompile these configuration modules, maybe also the corresponding driver modules, but not system or program modules – of course only as long as the changed FPGA implementation does not require changes of the configuration and driver modules' exported (public) interface.

One for All

A simple and straightforward approach might seem to be to keep all definitions in one system-wide module. However, changing one IO address or status bit in the FPGA would potentially require to recompile the whole operating system, with all that entails, including recreating the boot file with the Inner Core. Obviously not a practical solution.

Single Devices

In the simplest case, we have exactly one device in the FPGA, hence the device driver module directly contains the hardware specific definitions, for example, the watchdog.

MODULE WatchDog;

  CONST 
    WatchDogAdr = -100;
    Timeout = 100; (* ms *)
    IsEnabled = TRUE;

  PROCEDURE* Reset*;
  BEGIN
    SYSTEM.PUT(WatchDogAdr, Timeout)
  END Reset;

  (* ... *)

  PROCEDURE* Enabled*(): BOOLEAN;
  BEGIN
    RETURN IsEnabled
  END Enabled;

END WatchDog.

Should the IO address of the watchdog change, or the settings for Timeout and IsEnabled, the client module Oberon.mod does not need to be recompiled. (The IsEnabled value could also be read from a configuration file, but that’s not the topic here.)

Multiple Devices, Different Drivers

If we can have more than one hardware device, such as with RS232 devices, and even different types thereof, for example with or without buffer, the definitions are in a separate module, such as RS232dev.mod. These definition modules define a Device type, including procedures for initialisation. The procedures in the driver modules then simply expect a corresponding device parameter.

This also allows different types of drivers for the same device. For example, a driver for a buffered RS232 device can

  • simply write to the buffer and not care about buffer overflow
  • write to the buffer until it’s full, and report back how many bytes have been written
  • write to the buffer until it’s full, then fall back to busy waiting for the rest of the data to be written
  • write to the buffer until it’s full, then suspend the process until the buffer is empty again, and repeat this until all bytes were written out. Obviously this is only possible if the requester is a process.

RS232dev.mod

MODULE RS232dev;

  CONST
    (* device IDs *)
    Dev1* = 0; (* buffered, connected to Astrobe's terminal *)
    Dev2* = 1; (* buffered *)
    Dev3* = 2; (* buffered *)
    Devices = {Dev1 .. Dev3};

    (*** STATUS register bits: read from status addr *)
    (** unbuffered devices *)
    RXNE* = 0;    (* Rx reg non empty, ie. byte received *)
    TXE* = 1;     (* Tx reg empty, ie. ready to send *)

    (* buffered devices *)
    RXBNE* = 0;        (* Rx buffer not empty, ie. data received *)
    TXBNF* = 1;        (* Tx buffer not full, ie. ready to send *)
    RXBF* = 2;         (* Rx buffer full *)
    TXBE* = 3;         (* Tx buffer empty *)
    (* ... *)

    (*** CONTROL bits: write to status addr *)
    (* unbuffered and buffered devices *)
    SLOW* = 0;   (* slow mode, fast = 0 is default mode *)

    (*** IO addresses and buffer sizes *)
    Dev0DataAdr = -56;
    Dev0StatusAdr = -52;
    Dev0TxBufSize = 63;
    Dev0RxBufSize = 127;

    Dev1DataAdr = -76;
    Dev1StatusAdr = -72;
    Dev1TxBufSize = 255;
    Dev1RxBufSize = 63;

    (* ... *)

  TYPE
    Device* = POINTER TO DeviceDesc;
    DeviceDesc* = RECORD(Texts.TextDeviceDesc)
      dataAdr*, statusAdr*: INTEGER;
      txBufSize*, rxBufSize*: INTEGER;
      (* ... *)
    END;

  PROCEDURE* Init*(dev: Device; deviceNo: INTEGER);
  BEGIN
    ASSERT(deviceNo IN Devices);
    CASE deviceNo OF
      0: dev.dataAdr   := Dev0DataAdr;
         dev.statusAdr := Dev0StatusAdr;
         dev.txBufSize := Dev0TxBufSize;
         dev.rxBufSize := Dev0RxBufSize
    (* ... *)
    END
  END Init;

END RS232dev.

Note how the two basic status bits for buffered and unbuffered RS232 devices are chosen so that the buffered devices can also be used by a driver that does not assume any buffer.

RS232bc.mod

As example, here’s the driver to write to fill the buffer, and allow the client to check the number of characters written.

MODULE RS232bc;

 VAR Count*: INTEGER;

  PROCEDURE* PutChars*(dev: RS232dev.Device; data: ARRAY OF CHAR; n: INTEGER);
  BEGIN
    ASSERT(n <= LEN(data));
    Count := 0;
    WHILE (Count < n) & SYSTEM.BIT(dev.statusAdr, TxNotFull) DO
      SYSTEM.PUT(dev.dataAdr, ORD(data[Count]));
      INC(Count)
    END
  END PutChars;

END RS232bc.

RS232bc.mod Usage

MODULE M;

VAR dev: RS232dev.Device;

BEGIN
  NEW(dev); RS232dev.Init(dev, RS232dev.Dev2);
  RS233bc.PutChars(dev, "test", 4);
  Out.String("written: "); Out.Int(RS232bc.Count, 0); Out.Ln
END M.

Signal Wiring

DevSignals.mod is an example for a definition/configuration module with only constants, as it simply represents which status bits are wired in the FPGA to be used as device signals.

MODULE DevSignals;

  CONST
    RS232dev1RxBufNonEmpty* = 0;
    RS232dev1TxBufEmpty* = 1;

    RS232dev2RxBufNonEmpty* = 2;
    RS232dev2TxBufEmpty* = 3;

    RS232dev3RxBufNonEmpty* = 4;
    RS232dev3TxBufEmpty* = 5;

    (* ... *)

END DevSignals.

Other Modules

Other modules that implement the hardware abstraction concept include:

  • ProcTimers.mod
  • ProcDevSig.mod
  • SysCtrl.mod
  • ProcessorInfo.mod
  • SPIdev.mod (plus drivers, work in progress)
  • I2Cdev.mod (plus drivers, planned)