Buffered Extended SPI Device

Introduction

The Extended SPI device adds some functionality to the original device, such as separation of transfer speed and data length, or MSByte first transmission. To better support processes and avoiding busy waiting for the device, I added buffers to this Extended SPI device.

The Buffers

Based on the MO of any SPI device, we obviously need transmit and receive buffers. However, because each single transmission (MOSI line) and corresponding reception (MISO line) can operate with different control parameters, we also need a buffer for the control data.

For example, take this auxiliary procedure from the LCD driver as outlined here, and you see that the two data transmissions SPI.Put use 8 and 32 bit data lengths, respectively, and they toggle the auxiliary control line between CMD and DATA. These control data sets need to be stored in a parallel, synchronised buffer to the actual data in the transmit buffer:

MODULE LCD;

  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;

END LCD.

The Control Register

If the transmit buffer is non-empty, and the SPI device is ready to transmit, the oldest value from the control data buffer is read into the internal control register, which will hold the corresponding control bits for the SPI device during the transmission and reception.

In parallel (actually: one clock cycle later), the corresponding value from the transmit buffer is read into the SPI device’s shift register, and the transmission is started.

The Control Data Input Register

From the above description follows that each value in the transmission buffer requires a corresponding value in the parallel control data buffer. This would mean that the following API calls would not work, as there is no control data written for the second SPI.Put:

  SPI.Select(dev, SPI.D32);
  SPI.Put(dev, 0F0AACCAAH);
  SPI.Put(dev, 0CCAAF0AAH);

Obviously this could be amended by adding an SPI.Select call:

  SPI.Select(dev, SPI.D32);
  SPI.Put(dev, 0F0AACCAAH);
  SPI.Select(dev, SPI.D32);
  SPI.Put(dev, 0CCAAF0AAH);

To avoid this need, an input register for the control data is added. The SPI.Select call writes to this input register, and the control data buffer is fed from this input data register, not the bus directly. Hence as long a the control data is the same for a series of SPI.Put calls, there’s no need to issue additional SPI.Select calls.

The NORX and RST Controls

The unbuffered SPI device does not need to care about the data received in parallel to the transmission. Unlike SPI devices in, say, certain Cortex MCUs, the read data does not actually need to be read to reset flags and so on, it will simply be overwritten by the next transmission cycle.

With a receive buffer, this is different, obviously. Yes, the received data can be neglected as well, but if a subsequent SPI.Get operation actually wants to read data from the device, it would read garbage from the previous transmissions.

The buffered SPI device provides to facilities to cope with this:

  • NORX bit: the control data gets an additional control bit, NORX, meaning “no receive”, which can be set for SPI.Put operations, and which suppresses storing the received data into the receive buffer.

  • RST bit: yet another control data bit, RST, resets all buffers. It could be used before an SPI.Get operation to ensure that all buffers are empty and thus in sync for paired transmit and receive operations. Note that this has to be used with caution, it’s easy to delete unsent transmit data unwillingly. I am sure I did while testing and exploring. I am not even sure this is a useful feature in practice, but hey, it’s there for experimentation for now.

In any case, it’s safer to use the NORX option. Or neglect the issue, depending on the application at hand. If, say, the SPI device is only used by one process, and is driving an output-only peripheral, such as an LC display, it does not matter what’s in the receive buffer, and the buffer can also overflow without any consequences. The receive buffer FIFO in the FPGA will simply stop storing additional data if full, but not interfere with the functioning of the SPI transmissions.

SPIdev.mod

The hardware abstraction and definition module SPIdev.mod as described here gets additional CONSTs for the buffer status and the two control bits:

MODULE SPIdev;

  CONST
    (* status *)
    RXBNE* = 0;       (* Rx buffer not empty *)
    TXBNF* = 1;       (* Tx buffer not full *)
    RXBF*  = 2;       (* Rx buffer full *)
    TXBE*  = 3;       (* Tx buffer empty *)
    
    RXCNT0* = 8;      (* Rx counter, LSBit *)
    RXCNT1* = 19;     (* Rx counter, MBBit *)
    
    TXCNT0* = 20;     (* Tx counter, LSBit *)
    TXCNT1* = 31;     (* Tx counter, MBBit *)

    (* control *)
    NORX* = 7;    (* don't receive data into Rx buffer *)
    RST*  = 9;    (* reset the device *)

END SPIdev.

SPIb.mod

Here’s part of a driver module for the buffered SPI device. It fills the buffers until full, then falls back to busy waiting as needed.

MODULE SPIb;

  IMPORT SYSTEM, SPIdev;

  CONST
    (* status *)
    TXBNF = SPIdev.TXBNF; (* Tx buffer not full *)
    
    (* control *)
    FSTE* = {SPIdev.FSTE};
    MSBF* = {SPIdev.MSBF};
    NORX* = {SPIdev.NORX};
    RST*   = {SPIdev.RST};
    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
    REPEAT UNTIL SYSTEM.BIT(dev.statusAdr, TXBNF);
    SYSTEM.PUT(dev.statusAdr, ctrl);
    SYSTEM.PUT(dev.dataAdr, 0)
  END Deselect;
  
  PROCEDURE* Put*(dev: SPIdev.Device; data: INTEGER);
  BEGIN
    REPEAT UNTIL SYSTEM.BIT(dev.statusAdr, TXBNF);
    SYSTEM.PUT(dev.dataAdr, data)
  END Put;

  PROCEDURE* PutBytes*(dev: SPIdev.Device; data: ARRAY OF BYTE; n: INTEGER);
    VAR cnt: INTEGER; txNotFull: BOOLEAN;
  BEGIN
    cnt := 0;
    txNotFull := SYSTEM.BIT(dev.statusAdr, TXBNF);
    WHILE txNotFull & (cnt < n) DO
      SYSTEM.PUT(dev.dataAdr, data[cnt]);
      txNotFull := SYSTEM.BIT(dev.statusAdr, TXBNF);
      INC(cnt)
    ELSIF ~txNotFull & (cnt < n) DO
      txNotFull := SYSTEM.BIT(dev.statusAdr, TXBNF)
    END
  END PutBytes;

  (* ... *)

END SPIb.
  • The API is the same as with SPIu.mod for the unbuffered device.
  • Note how SPI.Deselect also needs to write dummy data, in order to transfer the control data from the input register into the control data buffer, as described above. However, no data is transferred out of the MOSI line, as this is suppressed if all CS bits are zero.

Usage Example

To use the same LCD driver as outlined here with the buffered SPI device, just replace SPIu with SPIb in the IMPORT list:

MODULE LCD;

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

END LCD.