Extended SPI Device

The Stock SPI Device

The original SPI device appears to be mainly designed to serve as controller for the SD card, as used by Kernel.mod and the boot loader:

  • The transmission rate can be set to fast, but this also sets the length of the transmitted and received data to 32 bits, from 8 bits at slower speed. That is, the two are linked, and other data lengths, in particular 16 bits, are not possible. Of course one can transmit/receive twice 8 bits.

  • The data is always transmitted least significant byte (LSByte) first. The bits of each byte are transmitted with the most significant bit (MSBit) first.

Sitronix ST7735

I’ll use the Sitronix ST7735 controller chip for LC displays as example that can be used with the stock SPI device, but can profit of a few extensions. See Astrobe’s example code for its use.

The ST7735 requires 8, 16, and 32 bit data. To send 8 bit data, such as commands, the SPI device must be switched to slow mode – which works, of course, but switching to slow transmission solely to transmit 8 bits of data is an unnecessary restriction.

The chip requires MSByte first data transmission, hence to send a 32 bit integer requires to shuffle the number’s bytes around to the LSByte-first transmission sequence (from the aforementioned example code):

PROCEDURE SendWord(data: INTEGER);
  VAR: b: ARRAY 4 OF BYTE;
BEGIN
  SPI.Select(SPI2 + SPIFast);
  b[0] := ASR(data, 24);
  b[1] := ASR(data, 16);
  b[2] := ASR(data, 8);
  b[3] := data;
  SPI.SendWord(b)
END SendWord;

Furthermore, when operated in 16 bit colour mode (ie. not 18 bits), the ST7735 requires 16 bits of data to be transmitted for each pixel, making a 16 bit mode very useful. The stock SPI device needs to transmit two 8 bit data items – invariably at low speed.

The Extended SPI Device

The Extended SPI device is an evolution of the stock one as follows:

  • Separation of data length (or width) and transfer speed selection.
  • Addition of an MSByte first transfer mode.
  • Addition of a 16 bit transfer mode.
  • Addition of an integrated generic auxiliary control line.

The control line allows to toggle, for example, the command/data mode of the ST7735 without resorting to GPIO pin operations.

The Extended SPI device is controlled by a corresponding control register in RISC5Top.v, in traditional RISC fashion. I might move this control register into the device itself to further modularise and simplify the top Verilog file. The buffered Extended SPI device requires an on-device control register anyway, and adding it to the non-buffered device would also increase consistency in this respect.

The Extended SPI device’s control register’s bits are:

  • [2:0]: chip select
  • [3]: fast transfer
  • [4]: 32 bit transfer
  • [5]: 16 bit transfer
  • [6]: MSByte first
  • [8]: generic control line

Bits [4:0] correspond to how Chris has wired his version of the extended SPI device in the forthcoming release of Astrobe for RISC5, in order to be compatible, and to be able to swap in this Extended Device to work with the Kernel.mod as adapted by Chris for the new release.

The SPI Driver API – Basis SPIdev.mod

According to the the principles and approach outlined here, there’s a module SPIdev.mod that represents the SPI devices instantiated in the FPGA.

MODULE SPIdev;
  CONST
    (* device IDs *)
    Dev1* = 0;  (* unbuffered, original design, used by file system *)
    Dev2* = 1;  (* buffered, extended variant *)
    Dev3* = 2;  (* unbuffered, extended variant *)
    Devices = {Dev1 .. Dev3};

    (* status *)
    RDY* = 0; 

    (* control *)
    (* Chip Select: wired is 0, range is [2:0] *)
    FSTE* = 3;    (* send/receive in fast mode *)
    D32*  = 4;    (* transmitted data = 32 bits *)
    D16*  = 5;    (* transmitted data = 16 bits *)
    MSBF* = 6;    (* send MSByte first *)
    CON*  = 8;    (* aux control bit *)

    (* config data *)
    (* ... *)
    Dev2DataAdr = -116;
    Dev2StatusAdr = -112;

  TYPE
    Device* = POINTER TO DeviceDesc;
    DeviceDesc* = RECORD
      dataAdr*, statusAdr*: INTEGER;
      ctrlReg*: SET;
      (* ... *)
    END;

  PROCEDURE Init*(dev: Device; deviceNo: INTEGER);
  BEGIN
    ASSERT(deviceNo IN Devices);
    CASE deviceNo OF

    (* ... *)

    | 2: dev.dataAdr   := Dev2DataAdr;
         dev.statusAdr := Dev2StatusAdr;
    END
  END Init;
END SPIdev.

SPIdev.Device represents the SPI device in software, initialised using SPIdev.Init. The various status and control CONSTs represent the Verilog wiring.

The SPI Driver API

Building upon SPIdev.mod, SPIu.mod implements the actual driver for the Extended SPI device. This driver uses busy waiting.

It is assumed that the device and the driver can be used by different processes. For example, one process could operate an LC display using chip select zero (CS[0]), while another process could read a temperature sensor on CS[1]. Hence, the hardware cannot hold state between single API procedure calls. In particular, a process cannot assume an unaltered SPI control register between API calls.

Of course, these processes would need to coordinate their device access, but that’s something to solve on the inter-process coordination level, not in the device driver.

MODULE SPIu;

  IMPORT SYSTEM, SPIdev;

  CONST
    (* status *)
    RDY = SPIdev.RDY;
    
    (* control *)
    FSTE* = {SPIdev.FSTE};
    MSBF* = {SPIdev.MSBF};
    D8*   = {};
    D16*  = {SPIdev.D16};
    D32*  = {SPIdev.D32};
    CON*  = {SPIdev.CON};
    COFF* = {};

  PROCEDURE* SetControl*(dev: SPIdev.Device; ctrlReg: SET);
  BEGIN
    dev.ctrlReg := ctrlReg
  END SetControl;

  PROCEDURE* Select*(dev: SPIdev.Device; ctrl: SET);
  BEGIN
    SYSTEM.PUT(dev.statusAdr, dev.ctrlReg + ctrl)
  END Select;

  PROCEDURE* Deselect*(dev: SPIdev.Device; ctrl: SET);
  BEGIN
    SYSTEM.PUT(dev.statusAdr, ctrl)
  END Deselect;

  PROCEDURE* Put*(dev: SPIdev.Device; data: INTEGER);
  BEGIN
    SYSTEM.PUT(dev.dataAdr, data);
    REPEAT UNTIL SYSTEM.BIT(dev.statusAdr, RDY)
  END Put;

  PROCEDURE PutBytes*(dev: SPIdev.Device; data: ARRAY OF BYTE; n: INTEGER);
    VAR i: INTEGER;
  BEGIN
    ASSERT(n <= LEN(data));
    FOR i := 0 TO n - 1 DO
      SYSTEM.PUT(dev.dataAdr, data[i]);
      REPEAT UNTIL SYSTEM.BIT(dev.statusAdr, RDY)
    END
  END PutBytes;

  (* ... *)

END SPIu.

The basic concept is a separation between SPI control data that is constant for the duration of the use of the device, and control data that can be adjusted for each API call, or series of API calls. The chip select line, for example, will be constant, while the data transfer length will change. The transfer speed, or the MSByte first selection, can or cannot be constant, depending on the application at hand.

  • SPI.SetControl sets the constant bits of the SPI control register
  • SPI.Select sets the variable bits, valid until subsequent SPI.Select calls, or until SPI.Deselect

The general usage pattern is thus:

MODULE M;

  IMPORT SPIdev, SPI := SPIu;

  VAR
    dev: SPIdev.Device;
    data: ARRAY 16 OF BYTE;

BEGIN
  NEW(dev); SPIdev.Init(dev, SPIdev.Dev3);        (* Dev3 = Extended SPI, unbuffered, in FPGA *)
  SPI.SetControl(dev, {0} + SPI.FSTE + SPI.MSBF); (* CS[0], fast, MSByte first *)

  SPI.Select(dev, SPI.D8 + SPI.CON);  (* select chip, 8 bit data, auxiliary ctrl line = on *)
  SPI.Put(dev, 0CCAAH);               (* send 8 bits, ie. 0AAH *)

  SPI.Select(dev, SPI.D32);           (* select chip, 32 bits data, aux ctrl line = off *)
  SPI.Put(dev, 0F0AACCAAH);           (* send all 32 bits MSByte first *)
  SPI.Put(dev, 0CCAAF0AAH);           (* still/again send all 32 bits *)

  SPI.Select(dev, SPI.D8);            (* select chip, 8 bit data, auxiliary ctrl line = off *)
  SPI.PutBytes(dev, data, 6);         (* send 6 BYTEs of the array *)

  SPI.Deselect(dev, SPI.CON);         (* deselect chip, set aux ctrl line = on *)
END M.

This API is very simple and transparent, yet versatile.

  • One could argue that the explicit selection of 8 bits data transfer size for PutBytes is not necessary, as the procedure itself is made to send only BYTEs, but I think APIs should be as regular and consistent as possible.

  • Also, deselecting an SPI chip is not strictly necessary – and SPI.Deselect could be omitted, unless one wants to set the auxiliary control line –, but I think it’s a good practice to actually deselect a chip unless in use, for example to avoid that the chip picks up noise on, say, the MOSI line if left selected.

Usage Example

Let’s use the LCD.mod driver module as example. It is based on the aforementioned Astrobe example code.

MODULE LCD;

  IMPORT SPIdev, SPI := SPIu (* ... *);

  CONST
    SPICtrl = SPI.FSTE + SPI.MSBF;  (* fast, MSByte first *)
    D8   = SPI.D8;
    D16  = SPI.D16;
    D32  = SPI.D32;
    DATA = SPI.CON;
    CMD  = SPI.COFF;

  TYPE
    Device* = POINTER TO DeviceDesc;
    DeviceDesc* = RECORD
      numXpixels*, numYpixels*: INTEGER;  (* display dimensions *)
      x0, y0: INTEGER;                    (* x and y address of the first visible pixel *)
      textColour, bgColour: INTEGER;
      spi: SPIdev.Device;
      rstPin, blPin: INTEGER
    END;

  (* ... *)

  PROCEDURE setX(dev: Device; x1, x2: INTEGER);
  BEGIN
    SPI.Select(dev.spi, D8 + CMD);
    SPI.Put(dev.spi, RASET);
    SPI.Select(dev.spi, D32 + DATA);
    SPI.Put(dev.spi, LSL(x1 + dev.x0, 16) + x2 + dev.x0);
    (* SPI.Deselect omitted for local procedure *)
  END setX;

  (* ... *)

  PROCEDURE DrawChar*(dev: Device; ch: CHAR; x, y: INTEGER);
    CONST NumGlyphPixel0 = NumGlyphPixel -1;
    VAR i, bmp, bmp1, bmp2: INTEGER;
  BEGIN
    Font.CharToBitmap(ch, bmp1, bmp2);
    setX(dev, x, x + GlyphWidth -1);
    setY(dev, y, y + GlyphHeight -1);
    bmp := bmp1;
    SPI.Select(dev.spi, D8 + CMD);
    SPI.Put(dev.spi, RAMWR);
    SPI.Select(dev.spi, D16 + DATA);
    FOR i := 0 TO NumGlyphPixel0 DO
      IF i = 32 THEN bmp := LSL(bmp2, 16) END;
      IF 31 IN BITS(bmp) THEN
        SPI.Put(dev.spi, dev.textColour)
      ELSE
        SPI.Put(dev.spi, dev.bgColour)
      END;
      bmp := LSL(bmp, 1)
    END;
    SPI.Deselect(dev.spi, DATA)
  END DrawChar;

  (* ... *)

  PROCEDURE Init*(dev: Device; spiNo: INTEGER; cfg: Config; cs: INTEGER; rstPin, blPin: INTEGER);
    BEGIN
      (* ... *)
      NEW(dev.spi);
      SPIdev.Init(dev.spi, spiNo);
      SPI.SetControl(dev.spi, SPICtrl + {cs});
      (* ... *)
    END Init;
END LCD.