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:
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§
- SPI mode.
Enums§
- SPI error kind.
- SPI transaction operation.
- 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.
- SPI bus.
- SPI device trait.