Module embedded_hal::spi

source ·
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

The SpiBus trait represents 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.

Device

The SpiDevice trait 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.

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. 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,
{
    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(&mut [
            Operation::Write(&[0x90]),
            Operation::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. 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,
{
    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. 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. This allows pipelining SPI transfers with CPU work.

When calling another method when a previous operation is still in progress, implementations can either wait for the previous operation to finish, or enqueue the new one, but they must not return a “busy” error. Users must be able to do multiple method calls in a row and have them executed “as if” they were done sequentially, without having to check for “busy” errors.

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.

CS-to-clock delays

Many chips require a minimum delay between asserting CS and the first SCK edge, and the last SCK edge and deasserting CS. Drivers should NOT use Operation::DelayNs for this, they should instead document that the user should configure the delays when creating the SpiDevice instance, same as they have to configure the SPI frequency and mode. This has a few advantages:

  • Allows implementations that use hardware-managed CS to program the delay in hardware
  • Allows the end user more flexibility. For example, they can choose to not configure any delay if their MCU is slow enough to “naturally” do the delay (very common if the delay is in the order of nanoseconds).

Structs

Enums

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