Expand description

Blocking SPI master mode traits.

Bus vs Device

SPI allows sharing a single bus between many SPI devices. The SCK, MOSI and MISO lines are wired in parallel to all the devices, and each device gets a dedicated chip-select (CS) line from the MCU, like this:

SCK
SCK
MISO
MISO
MOSI
MOSI
CS_1
CS_1
CS_2
CS_2
MCU
MCU
SCK
SCK
MISO
MISO
MOSI
MOSI
CS
CS
SPI DEVICE 1
SPI DEVICE 1
SCK
SCK
MISO
MISO
MOSI
MOSI
CS
CS
SPI DEVICE 2
SPI DEVICE 2
Text is not SVG - cannot display

CS is usually active-low. When CS is high (not asserted), SPI devices ignore all incoming data, and don’t drive MISO. When CS is low (asserted), the device is active: reacts to incoming data on MOSI and drives MISO with the response data. By asserting one CS or another, the MCU can choose to which SPI device it “talks” to on the (possibly shared) bus.

This bus sharing is common when having multiple SPI devices in the same board, since it uses fewer MCU pins (n+3 instead of 4*n), and fewer MCU SPI peripherals (1 instead of n).

However, it poses a challenge when building portable drivers for SPI devices. The driver needs to be able to talk to its device on the bus, while not interfering with other drivers talking to other devices.

To solve this, embedded-hal has two kinds of SPI traits: SPI bus and SPI device.

Bus

SPI bus traits represent exclusive ownership over the whole SPI bus. This is usually the entire SPI MCU peripheral, plus the SCK, MOSI and MISO pins.

Owning an instance of an SPI bus guarantees exclusive access, this is, we have the guarantee no other piece of code will try to use the bus while we own it.

There’s 3 bus traits, depending on the bus capabilities.

  • SpiBus: Read-write access. This is the most commonly used.
  • SpiBusRead: Read-only access, for example a bus with a MISO pin but no MOSI pin.
  • SpiBusWrite: Write-only access, for example a bus with a MOSI pin but no MISO pin.

Device

SpiDevice represents ownership over a single SPI device selected by a CS pin in a (possibly shared) bus. This is typically:

  • Exclusive ownership of the CS pin.
  • Access to the underlying SPI bus. If shared, it’ll be behind some kind of lock/mutex.

An SpiDevice allows initiating transactions against the target device on the bus. A transaction consists of asserting CS, then doing one or more transfers, then deasserting CS. For the entire duration of the transaction, the SpiDevice implementation will ensure no other transaction can be opened on the same bus. This is the key that allows correct sharing of the bus.

The capabilities of the bus (read-write, read-only or write-only) are determined by which of the SpiBus, SpiBusRead SpiBusWrite traits are implemented for the Bus associated type.

For driver authors

When implementing a driver, it’s crucial to pick the right trait, to ensure correct operation with maximum interoperability. Here are some guidelines depending on the device you’re implementing a driver for:

If your device has a CS pin, use SpiDevice. Do not manually manage the CS pin, the SpiDevice implementation will do it for you. Add bounds like where T::Bus: SpiBus, where T::Bus: SpiBusRead, where T::Bus: SpiBusWrite to specify the kind of access you need. By using SpiDevice, your driver will cooperate nicely with other drivers for other devices in the same shared SPI bus.

pub struct MyDriver<SPI> {
    spi: SPI,
}

impl<SPI> MyDriver<SPI>
where
    SPI: SpiDevice,
    SPI::Bus: SpiBus, // or SpiBusRead/SpiBusWrite if you only need to read or only write.
{
    pub fn new(spi: SPI) -> Self {
        Self { spi }
    }

    pub fn read_foo(&mut self) -> Result<[u8; 2], MyError<SPI::Error>> {
        let mut buf = [0; 2];

        // `transaction` asserts and deasserts CS for us. No need to do it manually!
        self.spi.transaction(|bus| {
            bus.write(&[0x90])?;
            bus.read(&mut buf)
        }).map_err(MyError::Spi)?;

        Ok(buf)
    }
}

#[derive(Copy, Clone, Debug)]
enum MyError<SPI> {
    Spi(SPI),
    // Add other errors for your driver here.
}

If your device does not have a CS pin, use SpiBus (or SpiBusRead, SpiBusWrite). This will ensure your driver has exclusive access to the bus, so no other drivers can interfere. It’s not possible to safely share a bus without CS pins. By requiring SpiBus you disallow sharing, ensuring correct operation.

pub struct MyDriver<SPI> {
    spi: SPI,
}

impl<SPI> MyDriver<SPI>
where
    SPI: SpiBus, // or SpiBusRead/SpiBusWrite if you only need to read or only write.
{
    pub fn new(spi: SPI) -> Self {
        Self { spi }
    }

    pub fn read_foo(&mut self) -> Result<[u8; 2], MyError<SPI::Error>> {
        let mut buf = [0; 2];
        self.spi.write(&[0x90]).map_err(MyError::Spi)?;
        self.spi.read(&mut buf).map_err(MyError::Spi)?;
        Ok(buf)
    }
}

#[derive(Copy, Clone, Debug)]
enum MyError<SPI> {
    Spi(SPI),
    // Add other errors for your driver here.
}

If you’re (ab)using SPI to implement other protocols by bitbanging (WS2812B, onewire, generating arbitrary waveforms…), use SpiBus. SPI bus sharing doesn’t make sense at all in this case. By requiring SpiBus you disallow sharing, ensuring correct operation.

For HAL authors

HALs must implement SpiBus, SpiBusRead and SpiBusWrite. Users can combine the bus together with the CS pin (which should implement OutputPin) using HAL-independent SpiDevice implementations such as the ones in embedded-hal-bus.

HALs may additionally implement SpiDevice to take advantage of hardware CS management, which may provide some performance benefits. (There’s no point in a HAL implementing SpiDevice if the CS management is software-only, this task is better left to the HAL-independent implementations).

HALs must not add infrastructure for sharing at the SpiBus level. User code owning a SpiBus must have the guarantee of exclusive access.

Flushing

To improve performance, SpiBus implementations are allowed to return before the operation is finished, i.e. when the bus is still not idle.

When using a SpiBus, call flush to wait for operations to actually finish. Examples of situations where this is needed are:

  • To synchronize SPI activity and GPIO activity, for example before deasserting a CS pin.
  • Before deinitializing the hardware SPI peripheral.

When using a SpiDevice, you can still call flush on the bus within a transaction. It’s very rarely needed, because transaction already flushes for you before deasserting CS. For example, you may need it to synchronize with GPIOs other than CS, such as DCX pins sometimes found in SPI displays.

For example, for write operations, it is common for hardware SPI peripherals to have a small FIFO buffer, usually 1-4 bytes. Software writes data to the FIFO, and the peripheral sends it on MOSI at its own pace, at the specified SPI frequency. It is allowed for an implementation of write to return as soon as all the data has been written to the FIFO, before it is actually sent. Calling flush would wait until all the bits have actually been sent, the FIFO is empty, and the bus is idle.

This still applies to other operations such as read or transfer. It is less obvious why, because these methods can’t return before receiving all the read data. However it’s still technically possible for them to return before the bus is idle. For example, assuming SPI mode 0, the last bit is sampled on the first (rising) edge of SCK, at which point a method could return, but the second (falling) SCK edge still has to happen before the bus is idle.

Structs

SPI mode

Enums

SPI error kind
Clock phase
Clock polarity

Constants

Helper for CPOL = 0, CPHA = 0
Helper for CPOL = 0, CPHA = 1
Helper for CPOL = 1, CPHA = 0
Helper for CPOL = 1, CPHA = 1

Traits

SPI error
SPI error type trait
Read-write SPI bus
Flush support for SPI bus
Read-only SPI bus
Write-only SPI bus
SPI device trait