HardwareSPI¶
Asynchronous SPI device stack for Sming.
Problem statement¶
The ESP8266 has limited I/O and the most useful way to expand it is using serial devices. There are several types available:
- I2C
Can be fast-ish but more limited than SPI, however slave devices generally cheaper, more widely available and simpler to interface as they only require 2 pins. The ESP8266 does have hardware support however, so requires a bit-banging solution. Inefficient.
- I2S
Designed for streaming audio devices but has other uses. See Esp8266 Drivers.
- RS232
Tied in with RS485, Modbus, DMX512, etc. Well-supported with asynchronous driver.
- SPI
Speed generally limited by slave device capability, can be multiplexed (‘overlapped’) onto SPI0 (flash memory) pins. The two SPI modules are identical, but the additional pins for quad/dual modes are only brought out for SPI0. In addition, three user chip selects are available in this mode; there is only one in normal mode although this can be handled using a regular GPIO.
The purpose of this driver is to provide a consistent interface which makes better use of the hardware capabilities.
Classes may inherit from
HSPI::Device
to provide support for specific devices such as Flash memory, SPI RAM, LCD controllers, etc.Device objects are attached to the stack via specified PinSet (overlapped or normal) and chip select.
Multiple concurrent devices are supported, limited only by available chip selects and physical constraints such as wire length, bus speeds.
2 and 4-bit modes are supported via overlap pins.
A
HSPI::Request
object supports transfers of up to 64K. The controller splits these into smaller transactions as required.Asynchronous execution so application does not block during SPI transfer. Application may provide a per-request callback to be notified when request has completed.
Blocking requests are also supported.
Support for moving data between Sming Streams and SPI memory devices using the
HSPI::StreamAdapter
.
SPI system expansion¶
A primary use-case for this driver is to provide additional resources for the ESP8266 using SPI devices such as:
MC23S017 SPI bus expander. Operates at 10MHz (regular SPI) and provides 16 additional I/O with interrupt capability.
High-speed shift registers. These can be wired directly to SPI busses to expand GPIO capability.
Epson S1D13781 display controller. See TFT_S1D13781. Evaluation boards are inexpensive and is a useful way to evaluate display modules with TFT interfaces. The Newhaven NHD-5.0-800480TF-ATXL#-CTP was used during development of this driver.
Bridgetek FT813 EVE TFT display controller. This supports dual/quad modes and clocks up to 30MHz.
NRF24L01 RF transceiver. Rated bus speed is 8MHz but it seems to work fine at 40MHz.
Serial memory devices. The library contains a driver for the IS62/65WVS2568GALL fast serial RAM, which clocks up to 45MHz and supports SDI/SQI modes.
Software operation¶
Overview¶
We have:
Controller: SPI hardware
Device: Slave device on the SPI bus
Request: Details of a transaction for a specific device
The Controller maintains an active list of requests (as a linked-list), but does not own the request objects. These will be allocated by the application, configured then submitted to the Controller for execution.
The Device specifies how a slave is connected to the bus, and that may change dynamically. For example, at reset an SPI RAM may be in SPI mode but can be switched into SDI/SQI modes.
This is device-specific so would be implemented by superclassing HSPI::Device
.
Requests¶
Each HSPI::Request
is split into transactions.
A transaction has four phases: command - address - MOSI - dummy - MISO.
All phases are optional.
The dummy bits are typically used in read modes and specified by the device datasheet.
No data is transferred during this phase.
The ESP8266 hardware FIFO is used for MOSI/MISO phases and is limited to 64 bytes, so larger transfers must be broken into chunks. The driver handles this automatically.
Requests may be executed asynchronously so the call will not block and the CPU can continue with normal operations. An optional callback is invoked when the request has completed. As an example, consider moving a 128KByte file from flash storage into FT813 display memory:
Read the first file chunk into a RAM buffer, submit an SPI request1 to transfer it asynchronously
Read the second file chunk into another RAM buffer, and prepare request2 for that (but do not submit it yet)
When request1 has completed, submit request2 (from the interrupt callback). Schedule a task to read the next chunk and prepare request1.
When request2 has completed, continue from step (2) to submit request1, etc.
Timing¶
A 64-byte data transfer (full hardware FIFO with 1 command byte and 3-byte address) at 26MHz would take 21us (5.25us in QIO mode) or 1680 (420) CPU cycles. To transfer 128Kbytes would take 2048 such transactions, 43ms (11ms for QIO), not including memory copy overheads.
In practice request sizes will be much smaller due to RAM constraints. Nevertheless, at high clock speeds the interrupt rate increases to the point where it consumes more CPU cycles than the actual transfer. The driver therefore disables interrupts in these situations and executes the request in task mode.
Bear in mind that issuing a blocking request will also require all queued requests to complete.
The driver does not currently support out-of-order execution, which might prioritise faster devices.
Pin Set¶
To avoid confusion, we’ll refer to the flash memory SPI bus as SPI0, and the user bus as SPI1. This driver doesn’t support direct use of SPI0 as on most devices it is reserved for flash memory. However, an overlap mode is supported which makes use of hardware arbitration to perform SPI1 transactions using SPI0 pins. This has several advantages:
Liberates three GPIO which would normally be required for MOSI, MISO and SCLK.
Only one additional pin is required for chip select.
Additional 2/4 bits-per-clock modes are available for supported devices.
For the ESP8266, these are the HSPI::PinSet
assignments:
- PinSet::normal
- MISO=GPIO12, MOSI=GPIO13, SCLK=GPIO14. One chip select:
GPIO15 (HSPI CS)
- PinSet::overlap
- MISO=SD0, MOSI=SD1, IO2=SD3, IO3=SD2, SCLK = CLK. Three chip selects:
GPIO15 (HSPI_CS)
GPIO1 (SPI_CS1 / UART0_TXD). This conflicts with the normal serial TX pin which should be swapped to GPIO2 if required.
GPIO0 (SPI_CS2)
- PinSet::manual
Typically a GPIO will be assigned to perform chip select (CS). The application should register a callback function via
HSPI::onSelectDevice()
which performs the actual switching. This MUST be in IRAM.
Note
The connections for IO2/3 look wrong above, but on two different models of SPI RAM chip these have been verified as correct by writing in SPIHD mode and reading in quad mode.
Multiplexed CS¶
Multiple devices can be supported on a single CS using, for example using a HC138 3:8 decoder. The CS line is connected to an enable input, with three GPIO outputs setting A0-2.
A custom controller should be created like this:
class CustomController: public HSPI::Controller
{
public:
bool startDevice(Device& dev, PinSet pinSet, uint8_t chipSelect) override
{
/*
* You should perform any custom validation here and return false on failure.
* For example, if we're only using 3 of the 8 available outputs.
*/
auto addr = chipSelect & 0x07;
if(addr > 3) {
debug_e("Invalid CS addr: %u", addr);
return false;
}
/*
*
*/
onSelectDevice(selectDevice);
/*
* Initialise hardware Controller
*/
auto cs = chipSelect >> 3;
return HSPI::Controller::startDevice(dev, pinSet, cs);
}
private:
void IRAM_ATTR selectDevice(uint8_t chipSelect, bool active)
{
// Only perform GPIO if CS changes as GPIO is expensive
if(active && chipSelect != activeChipSelect) {
auto addr = chipSelect & 0x07;
digitalWrite(PIN_MUXADDR0, addr & 0x01);
digitalWrite(PIN_MUXADDR1, addr & 0x02);
// As we only need 2 address lines, can leave this one
// digitalWrite(PIN_MUXADDR2, addr & 0x03);
activeChipSelect = chipSelect;
}
if(getActivePinSet() == HSPI::PinSet::manual) {
// Set CS output here
}
}
uint8_t activeChipSelect{0};
};
The application should register a callback function via HSPI::onSelectDevice()
allows 8 (or more) SPI devices to share the same bus.
Bits 0-2 of the chipSelect value might be assigned to the GPIO output pins setting
the multiplexer address, with bits 3-7 storing the hardware CS setting.
IO Modes¶
Not to be confused with HSPI::ClockMode
, the HSPI::IoMode
determines how
the command, address and data phases are transferred:
.
Bits per clock
.
IO Mode
Command
Address
Data
Duplex
SPI
1
1
1
Full
SPIHD
1
1
1
Half
DUAL
1
1
2
Half
DIO
1
2
2
Half
SDI
2
2
2
Half
QUAD
1
1
4
Half
QIO
1
4
4
Half
SQI
4
4
4
Half
Note
SDI and SQI are not supported directly by hardware, but is implemented within the driver using the data phase only. For 8-bit command and 24-bit address, this limits each transaction to 60 bytes.
This seems to be consistent with the ESP32 IDF driver, as in spi_ll.h
:
/** IO modes supported by the master. */
typedef enum {
SPI_LL_IO_MODE_NORMAL = 0, ///< 1-bit mode for all phases
SPI_LL_IO_MODE_DIO, ///< 2-bit mode for address and data phases, 1-bit mode for command phase
SPI_LL_IO_MODE_DUAL, ///< 2-bit mode for data phases only, 1-bit mode for command and address phases
SPI_LL_IO_MODE_QIO, ///< 4-bit mode for address and data phases, 1-bit mode for command phase
SPI_LL_IO_MODE_QUAD, ///< 4-bit mode for data phases only, 1-bit mode for command and address phases
} spi_ll_io_mode_t;
Somne devices (e.g. W25Q32 flash) have specific commands to support these modes, but others (e.g. IS62/65WVS2568GALL fast serial RAM) do not, and the SDI/SQI mode setting applies to all phases. This needs to be implemented in the driver as otherwise the user code is more complex than necesssary and performance suffers considerably.
Streaming¶
The HSPI::StreamAdapter
provides support for streaming of data to/from memory devices.
This would be used, for example, to transfer content to or from a FileStream
or FlashMemoryStream
to SPI RAM asynchronously.
Supported devices must inherit from HSPI::MemoryDevice
.
API¶
-
enum
HSPI
::
ClockMode
¶ SPI clock polarity (CPOL) and phase (CPHA)
Values:
-
mode0
= 0x00¶ CPOL: 0 CPHA: 0.
-
mode1
= 0x01¶ CPOL: 0 CPHA: 1.
-
mode2
= 0x10¶ CPOL: 1 CPHA: 0.
-
mode3
= 0x11¶ CPOL: 1 CPHA: 1.
-
-
enum
HSPI
::
IoMode
¶ Mode of data transfer.
Values:
-
SPI
¶ One bit per clock, MISO stage concurrent with MISO (full-duplex)
-
SPIHD
¶ One bit per clock, MISO stage follows MOSI (half-duplex)
-
DUAL
¶ Two bits per clock for Data, 1-bit for Command and Address.
-
DIO
¶ Two bits per clock for Address and Data, 1-bit for Command.
-
SDI
¶ Two bits per clock for Command, Address and Data.
-
QUAD
¶ Four bits per clock for Data, 1-bit for Command and Address.
-
QIO
¶ Four bits per clock for Address and Data, 1-bit for Command.
-
SQI
¶ Four bits per clock for Command, Address and Data.
-
-
enum
HSPI
::
PinSet
¶ How SPI hardware pins are connected.
Values:
-
none
¶ Disabled.
-
normal
¶ Standard HSPI pins.
-
manual
¶ HSPI pins with manual chip select.
-
overlap
¶ Overlapped with SPI 0.
-
Warning
doxygenclass: Cannot find class “HSPI::Request” in doxygen xml output for project “api” from directory: ../api/xml/
Warning
doxygenclass: Cannot find class “HSPI::Data” in doxygen xml output for project “api” from directory: ../api/xml/
-
class
Device
¶ Manages a specific SPI device instance attached to a controller.
Subclassed by HSPI::MemoryDevice
-
class
MemoryDevice
: public HSPI::Device¶ Base class for read/write addressable devices.
Subclassed by HSPI::RAM::IS62_65, HSPI::RAM::PSRAM64
Prepare a write request
-
virtual void
prepareWrite
(HSPI::Request &req, uint32_t address) = 0¶ - Parameters
request
:address
:
-
void
prepareWrite
(HSPI::Request &req, uint32_t address, const void *data, size_t len)¶ - Parameters
request
:address
:data
:len
:
Prepare a read request
-
virtual void
prepareRead
(HSPI::Request &req, uint32_t address) = 0¶ - Parameters
req
:address
:
-
void
prepareRead
(HSPI::Request &req, uint32_t address, void *buffer, size_t len)¶ - Parameters
req
:address
:data
:len
:
Public Functions
-
void
write
(uint32_t address, const void *data, size_t len)¶ Write a block of data.
- Note
Limited by current operating mode
- Parameters
address
:data
:len
:
-
void
read
(uint32_t address, void *buffer, size_t len)¶ Read a block of data.
- Note
Limited by current operating mode
- Parameters
address
:data
:len
:
-
virtual void
Warning
doxygenclass: Cannot find class “HSPI::SpiRam” in doxygen xml output for project “api” from directory: ../api/xml/
-
class
Controller
¶ Manages access to SPI hardware.
Public Types
-
using
SelectDevice
= void (*)(uint8_t chipSelect, bool active)¶ Interrupt callback for custom Controllers.
For manual CS (
PinSet::manual) the actual CS GPIO must be asserted/de-asserted.- Parameters
chipSelect
:active
: true when transaction is about to start, false when completed
Expanding the SPI bus using a HC138 3:8 multiplexer, for example, can also be handled here, setting the GPIO address lines appropriately.
Public Functions
-
void
end
()¶ Disable HSPI controller.
- Note
Reverts HSPI pins to GPIO and disables the controller
-
void
onSelectDevice
(SelectDevice callback)¶ Set interrupt callback to use for manual CS control (PinSet::manual) or if CS pin is multiplexed.
- Note
Callback MUST be marked IRAM_ATTR
-
virtual bool
startDevice
(Device &dev, PinSet pinSet, uint8_t chipSelect)¶ Assign a device to a CS# using a specific pin set. Only one device may be assigned to any CS.
Custom controllers should override this method to verify/configure chip selects, and also provide a callback (via
onSelectDevice()
).
-
void
configChanged
(Device &dev)¶ Devices call this method to tell the Controller about configuration changes. Internally, we just set a flag and update the register values when required.
-
using
-
class
StreamAdapter
¶ Helper class for streaming data to/from SPI devices.
References¶
Source Code (submodule, may be patched).
Used by¶
TFT_S1D13781 Library
Environment Variables¶
HSPI_ENABLE_STATS
HSPI_ENABLE_TESTPINS
HSPI_TESTPIN1
HSPI_TESTPIN2