Expand description
Use a SERCOM peripheral for SPI transactions
Using an SPI peripheral occurs in three steps. First, you must supply
gpio Pins to create a set of Pads. Next, you combine the Pads
with other pieces to form a Config struct. Finally, after configuring
the peripheral, you enable it to yield a functional Spi struct.
Transactions are performed using traits from the Embedded HAL crates,
specifically those from the spi,
serial, and
blocking modules.
§Crating a set of Pads
An SPI peripheral can use up to four Pins as Sercom pads. However,
only certain Pin combinations are acceptable. All Pins must be mapped to
the same Sercom, and for SAMx5x chips they must also belong to the same
IoSet. This HAL makes it impossible to use invalid Pin combinations,
and the Pads struct is responsible for enforcing these constraints.
A Pads type takes five parameters: the first specifies the Sercom, and
the remaining four type parameters, DI, DO, CK and SS, represent the
Data In, Data Out, Sclk and SS pads respectively. Each of these type
parameters is an OptionalPad and defaults to NoneT. A Pad is just
a Pin configured in the correct PinMode that implements IsPad. The
bsp_pins! macro can be used to define convenient type
aliases for Pad types.
use atsamd_hal::gpio::{PA08, PA09, AlternateC};
use atsamd_hal::sercom::{Sercom0, spi};
use atsamd_hal::typelevel::NoneT;
type Miso = Pin<PA08, AlternateC>;
type Sclk = Pin<PA09, AlternateC>;
type Pads = spi::Pads<Sercom0, Miso, NoneT, Sclk>;Alternatively, you can use the PadsFromIds alias to define a set of Pads
in terms of PinIds instead of Pins. This is useful when you don’t
have Pin aliases pre-defined.
use atsamd_hal::gpio::{PA08, PA09};
use atsamd_hal::sercom::{Sercom0, spi};
use atsamd_hal::typelevel::NoneT;
type Pads = spi::PadsFromIds<Sercom0, PA08, NoneT, PA09>;Instances of Pads are created using the builder pattern. Start by creating
an empty set of Pads using Default. Then pass each respective Pin
using the corresponding methods. For SAMD21 and SAMx5x chips, the builder
methods automatically convert each pin to the correct PinMode. However,
due to inherent ambiguities, users must manually configure PinModes for
SAMD11 chips.
use atsamd_hal::target_device::Peripherals;
use atsamd_hal::gpio::Pins;
use atsamd_hal::sercom::{Sercom0, spi};
let mut peripherals = Peripherals::take().unwrap();
let pins = Pins::new(peripherals.PORT);
let pads = spi::Pads::<Sercom0>::default()
.sclk(pins.pa09)
.data_in(pins.pa08)
.data_out(pins.pa11);To be accepted by the Config struct as a set of ValidPads, the
Pads must do two things:
- Specify
SomePadforCKand at least one ofDIorDO - Use a valid combination of
PadNums, so that thePadsimplementDipoDopo
§Configuring the peripheral
Next, create a Config struct, which represents the SPI peripheral in its
disabled state. A Config is specified with three type parameters: the
Pads type; an OpMode, which defaults to Master; and a Size
type that varies by chip. Size essentially acts as a trait alias. On
SAMD11 and SAMD21 chips, it represents the CharSize, which can either be
EightBit or NineBit. While on SAMx5x chips, it represents the
transaction Length in bytes, using type-level numbers provided by the
typenum crate. Valid transaction lengths, from U1 to U255, are
re-exported in the lengths sub-module.
use atsamd_hal::gpio::{PA08, PA09};
use atsamd_hal::sercom::{Sercom0, spi};
use atsamd_hal::sercom::spi::Master;
use atsamd_hal::typelevel::NoneT;
// SAMD11/SAMD21-specific imports
use atsamd_hal::sercom::spi::NineBit;
// SAMx5x-specific imports
use atsamd_hal::sercom::spi::lengths::U2;
type Pads = spi::PadsFromIds<Sercom0, PA08, NoneT, PA09>;
// SAMD11/SAMD21 version
type Config = spi::Config<Pads, Master, NineBit>;
// SAMx5x version
type Config = spi::Config<Pads, Master, U2>;For simplicity, this module ignores character size on SAMx5x chips. Instead,
the SPI peripheral is always configured to use 32-bit extension mode and the
hardware LENGTH counter. Note that, due to a hardware bug, ICSPACE must
be at least one when using the length counter. See the silicon errata for
more details.
Upon creation, the Config takes ownership of both the Pads and the
PAC Sercom struct. It takes a reference to the PM or MCLK, so that
it can enable the APB clock, and it takes a frequency to indicate the GCLK
configuration. Users are responsible for correctly configuring the GCLK.
use atsamd_hal::time::U32Ext;
// Not shown: configure GCLK for 10 MHz
// SAMD11/SAMD21 version
let pm = peripherals.PM;
let sercom = peripherals.SERCOM0;
let freq = 10.mhz();
let config = spi::Config::new(&pm, sercom, pads, freq);
// SAMx5x version
let mclk = peripherals.MCLK;
let sercom = peripherals.SERCOM0;
let freq = 10.mhz();
let config = spi::Config::new(&mclk, sercom, pads, freq);The Config uses two different APIs for configuration. For most
parameters, it provides get_ and set_ methods that take &self and
&mut self respectively, e.g. get_bit_order and
set_bit_order. However, because Config tracks
the OpMode and Size at compile-time, which requires changing the
corresponding type parameters, Config also provides a builder-pattern API,
where methods take and return self, e.g. bit_order.
Once configured, the enable method consumes the Config and returns an
enabled Spi struct that can be used for transactions. Because the
enable function takes the Config as self, the builder-pattern API is
usually the more ergonomic option.
use embedded_hal::spi::MODE_1;
// SAMD11/SAMD21 version
let spi = spi::Config::new(&pm, sercom, pads, freq)
.baud(1.mhz())
.char_size::<NineBit>()
.bit_order(BitOrder::LsbFirst)
.spi_mode(MODE_1)
.enable();
// SAMx5x version
let spi = spi::Config::new(&mclk, sercom, pads, freq)
.baud(1.mhz())
.length::<U2>()
.bit_order(BitOrder::LsbFirst)
.spi_mode(MODE_1)
.enable();To be accepted as a ValidConfig, the Config must have a set of
ValidPads that matches its OpMode. In particular, the SS pad must
be NoneT for Master mode, where the user is expected to handle it
manaully. But it must be SomePad in MasterHWSS and Slave modes,
where it is controlled by the hardware.
§Using a functional Spi peripheral
An Spi struct has two type parameters. The first is the corresponding
Config, while the second represents its Capability, i.e. Rx,
Tx or Duplex. The enable function determines the Capability
automaically from the set of ValidPads.
use atsamd_hal::gpio::{PA08, PA09};
use atsamd_hal::sercom::{Sercom0, spi};
use atsamd_hal::sercom::spi::{Master, Rx};
use atsamd_hal::typelevel::NoneT;
// SAMD11/SAMD21-specific imports
use atsamd_hal::sercom::spi::NineBit;
// SAMx5x-specific imports
use atsamd_hal::sercom::spi::lengths::U2;
type Pads = spi::PadsFromIds<Sercom0, PA08, NoneT, PA09>;
// SAMD11/SAMD21 version
type Config = spi::Config<Pads, Master, NineBit>;
// SAMx5x version
type Config = spi::Config<Pads, Master, U2>;
type Spi = spi::Spi<Config, Rx>;Only Spi structs can actually perform transactions. To do so, use the
various embedded HAL traits, like spi::SpiBus,
embedded_io::Read, embedded_io::Write,
embedded_hal_nb::serial::Read, or
embedded_hal_nb::serial::Write. See the
impl_ehal module documentation for more details about the specific trait
implementations, which vary based on Size and Capability.
use nb::block;
use crate::ehal_02::spi::FullDuplex;
block!(spi.send(0xAA55));
let rcvd: u16 = block!(spi.read());§Flushing the bus
The SpiBus methods do not flush the bus when a
transaction is complete. This is in part to increase performance and allow
for pipelining SPI transactions. This is true for both sync and async
operation. As such, you should ensure you manually call
flush when:
- You must synchronize SPI activity and GPIO activity, for example before deasserting a CS pin.
- Before deinitializing the SPI peripheral.
Take note that the SpiDevice
implementations automatically take care of flushing, so no further flushing
is needed.
See the embedded-hal spec for more information.
§PanicOnRead and PanicOnWrite
Some driver libraries take a type implementing embedded_hal::spi::SpiBus
or embedded_hal::spi::SpiDevice, even when they only need to receive or
send data, but not both. A good example is WS2812 addressable LEDs
(neopixels), which only take a data input. Therefore, their protocol can be
implemented with a Tx Spi that only has a MOSI pin. In another
example, often LCD screens only have a MOSI and SCK pins. In order to
unnecessarily tying up pins in the Spi struct, and provide an escape
hatch for situations where constructing the Spi struct would otherwise
be impossible, we provide the PanicOnRead and PanicOnWrite wrapper
types, which implement embedded_hal::spi::SpiBus.
As the names imply, they panic if an incompatible method is called. See
Spi::into_panic_on_write and Spi::into_panic_on_read.
PanicOnRead and PanicOnWrite are compatible with DMA.
§Using SPI with DMA dma
This HAL includes support for DMA-enabled SPI transfers. Use
Spi::with_dma_channels (Duplex and Rx), and
Spi::with_tx_channel (Tx-only) to attach DMA channels to the Spi
struct. A DMA-enabled Spi implements the blocking
embedded_hal::spi::SpiBus, embedded_io::Write and/or
embedded_io::Read traits, which can be used to perform SPI transactions
which are fast, continuous and low jitter, even if they are preemped by a
higher priority interrupt.
// Assume channel0 and channel1 are configured `dmac::Channel`, and spi a
// fully-configured `Spi`
// Create data to send
let buffer: [u8; 50] = [0xff; 50];
// Attach DMA channels
let spi = spi.with_dma_channels(channel0, channel1);
// Perform the transfer
spi.write(&mut buffer)?;§async operation async
An Spi can be used for async operations. Configuring a Spi in
async mode is relatively simple:
- Bind the corresponding
SERCOMinterrupt source to the SPIInterruptHandler(refer to the module-levelasync_haldocumentation for more information). - Turn a previously configured
Spiinto aSpiFutureby callingSpi::into_future - Optionally, add DMA channels to RX, TX or both using
SpiFuture::with_rx_dma_channelandSpiFuture::with_tx_dma_channel. The API is exactly the same whether DMA channels are used or not. - Use the provided async methods for reading or writing to the SPI
peripheral.
SpiFutureimplementsembedded_hal_async::spi::SpiBus.
SpiFuture implements AsRef<Spi> and AsMut<Spi> so that it can be
reconfigured using the regular Spi methods.
§Considerations when using async Spi with DMA async dma
- An
Spistruct must be turned into anSpiFutureby callingSpi::into_futurebefore callingwith_dma_channel. The DMA channel itself must also be configured in async mode by usingDmaController::into_future. If a DMA channel is added to theSpistruct before it is turned into anSpiFuture, it will not be able to use DMA in async mode.
// This will work
let spi = spi.into_future().with_dma_channels(rx_channel, tx_channel);
// This won't
let spi = spi.with_dma_channels(rx_channel, tx_channel).into_future();§Safety considerations
In async mode, an SPI+DMA transfer does not require 'static source and
destination buffers. This, in theory, makes its use unsafe. However it is
marked as safe for better ergonomics, and to enable the implementation of
the embedded_hal_async::spi::SpiBus trait.
This means that, as an user, you must ensure that the Futures
returned by the embedded_hal_async::spi::SpiBus methods may never be
forgotten through forget or by wrapping them with a ManuallyDrop.
The returned futures implement Drop and will automatically stop any
ongoing transfers; this guarantees that the memory occupied by the
now-dropped buffers may not be corrupted by running transfers.
This means that using functions like futures::select_biased to implement
timeouts is safe; transfers will be safely cancelled if the timeout expires.
This also means that should you forget this Future after its first
poll call, the transfer will keep running, ruining the now-reclaimed
memory, as well as the rest of your day.
awaiting is fine: theFuturewill run to completion.- Dropping an incomplete transfer is also fine. Dropping can happen, for example, if the transfer doesn’t complete before a timeout expires.
- Dropping an incomplete transfer without running its destructor is unsound and will trigger undefined behavior.
async fn always_ready() {}
let mut buffer = [0x00; 10];
// This is completely safe
spi.read(&mut buffer).await?;
// This is also safe: we launch a transfer, which is then immediately cancelled
futures::select_biased! {
_ = spi.read(&mut buffer)?,
_ = always_ready(),
}
// This, while contrived, is also safe.
{
use core::future::Future;
let future = spi.read(&mut buffer);
futures::pin_mut!(future);
// Assume ctx is a `core::task::Context` given out by the executor.
// The future is polled, therefore starting the transfer
future.as_mut().poll(ctx);
// Future is dropped here - transfer is cancelled.
}
// DANGER: This is an example of undefined behavior
{
use core::future::Future;
use core::ops::DerefMut;
let future = core::mem::ManuallyDrop::new(spi.read(&mut buffer));
futures::pin_mut!(future);
// To actually make this example compile, we would need to wrap the returned
// future from `i2c.read()` in a newtype that implements Future, because we
// can't actually call as_mut() without being able to name the type we want
// to deref to.
let future_ref: &mut SomeNewTypeFuture = &mut future.as_mut();
future.as_mut().poll(ctx);
// Future is NOT dropped here - transfer is not cancelled, resulting un UB.
}As you can see, unsoundness is relatively hard to come by - however, caution should still be exercised.
Re-exports§
Modules§
- impl_
ehal - lengths
- Valid transaction
Lengths from thetypenumcrate - pads
- Define a container for a set of SERCOM pads
- size
- Define a trait to track the transaction
Length, which represents theConfigSizefor SAMx5x chips
Structs§
- Config
- A configurable SPI peripheral in its disabled state
- Duplex
- Type-level variant of the
Capabilityenum for duplex transactions - Flags
- Interrupt bit flags for SPI transactions
- Interrupt
Handler - Interrupt handler for async SPI operarions
- Panic
OnRead - Wrapper type around a
Spithat allows usingembedded_hal::spi::SpiBuseven though it only has TX capability. Will panic if any write-adjacent method is used (ie,read,transfer, andtransfer_in_place). - Panic
OnWrite - Wrapper type around a
Spithat allows usingembedded_hal::spi::SpiBuseven though it only has RX capability. Will panic if any write-adjacent method is used (ie,write,transfer,transfer_in_place, andflush). - Rx
- Type-level variant of the
Capabilityenum for simplex,Receive-only transactions - Spi
- An enabled SPI peripheral that can perform transactions
- SpiFuture
asyncversion ofSpi.- Status
- Status bit flags for SPI transactions
- Tx
- Type-level variant of the
Capabilityenum for simplex,Transmit-only transactions
Enums§
- BitOrder
- Define the bit order of transactions
- Eight
Bit - This type is not present with the selected feature set, defined for documentation only
- Error
- Error
enumfor SPI transactions - Master
OpModevariant for Master mode- MasterHWSS
OpModevariant for Master mode with hardware-controlled slave select- NineBit
- This type is not present with the selected feature set, defined for documentation only
- Phase
- Clock phase.
- Polarity
- Clock polarity.
- Slave
OpModevariant for Slave mode
Constants§
- MODE_0
- Helper for CPOL = 0, CPHA = 0.
- MODE_1
- Helper for CPOL = 0, CPHA = 1.
- MODE_2
- Helper for CPOL = 1, CPHA = 0.
- MODE_3
- Helper for CPOL = 1, CPHA = 1.
- RX_
FLAG_ MASK - TX_
FLAG_ MASK
Traits§
- AnyConfig
- Type class for all possible
Configtypes - AnySpi
- Type class for all possible
Spitypes - Atomic
Size - Marker trait for transaction
Sizes that can be completed in a single read or write of theDATAregister - Capability
- Type-level enum representing the simplex or duplex transaction capability
- Char
Size - This trait is not present with the selected feature set, defined for documentation only
- Master
Mode - Marker trait for Master operating modes
- OpMode
- Type-level enum representing the SPI operating mode
- Receive
- Sub-set of
Capabilityvariants that can receive data, i.e.RxandDuplex - Size
- Trait alias whose definition varies by chip
- Transmit
- Sub-set of
Capabilityvariants that can transmit dat, i.e.TxandDuplex - Valid
Config - Marker trait for valid SPI
Configurations
Type Aliases§
- Data
Width - Type alias for the width of the
DATAregister - Default
Size - Type alias for the default
Sizetype, which varies by chip - Specific
Config - Type alias to recover the specific
Configtype from an implementation ofAnyConfig - Specific
Spi - Type alias to recover the specific
Spitype from an implementation ofAnySpi - SpiFuture
Duplex - Convenience type for a
SpiFuturewith RX and TX capabilities - SpiFuture
Duplex Dma - Convenience type for a
SpiFuturewith RX and TX capabilities in DMA mode. - SpiFuture
Rx - Convenience type for a
SpiFuturewith RX capabilities - SpiFuture
RxDma - Convenience type for a
SpiFuturewith RX capabilities in DMA mode. - SpiFuture
Tx - Convenience type for a
SpiFuturewith TX capabilities - SpiFuture
TxDma - Convenience type for a
SpiFuturewith TX capabilities in DMA mode.