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 crate, 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 or six type parameters, depending on the chip. The first type always specifies the Sercom. On SAMx5x chips, the second type specifies the IoSet. 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;

// SAMx5x-specific imports
use atsamd_hal::sercom::pad::IoSet1;

type Miso = Pin<PA08, AlternateC>;
type Sclk = Pin<PA09, AlternateC>;

// SAMD11/SAMD21 version
type Pads = spi::Pads<Sercom0, Miso, NoneT, Sclk>;
// SAMx5x version
type Pads = spi::Pads<Sercom0, IoSet1, 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;

// SAMx5x-specific imports
use atsamd_hal::sercom::pad::IoSet1;

// SAMD21 version
type Pads = spi::PadsFromIds<Sercom0, PA08, NoneT, PA09>;
// SAMx5x version
type Pads = spi::PadsFromIds<Sercom0, IoSet1, 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};

// SAMx5x-specific imports
use atsamd_hal::sercom::pad::IoSet1;

let mut peripherals = Peripherals::take().unwrap();
let pins = Pins::new(peripherals.PORT);
// SAMD21 version
let pads = spi::Pads::<Sercom0>::default()
    .sclk(pins.pa09)
    .data_in(pins.pa08)
    .data_out(pins.pa11);
// SAMx5x version
let pads = spi::Pads::<Sercom0, IoSet1>::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 SomePad for CK and at least one of DI or DO
  • Use a valid combination of PadNums, so that the Pads implement DipoDopo

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;
use atsamd_hal::sercom::pad::IoSet1;

// SAMD11/SAMD21 version
type Pads = spi::PadsFromIds<Sercom0, PA08, NoneT, PA09>;
type Config = spi::Config<Pads, Master, NineBit>;

// SAMx5x version
type Pads = spi::PadsFromIds<Sercom0, IoSet1, PA08, NoneT, PA09>;
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;
use atsamd_hal::sercom::pad::IoSet1;

// SAMD11/SAMD21 version
type Pads = spi::PadsFromIds<Sercom0, PA08, NoneT, PA09>;
type Config = spi::Config<Pads, Master, NineBit>;
type Spi = spi::Spi<Config, Rx>;

// SAMx5x version
type Pads = spi::PadsFromIds<Sercom0, IoSet1, PA08, NoneT, PA09>;
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::FullDuplex, serial::Read or 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 embedded_hal::spi::FullDuplex;

block!(spi.send(0xAA55));
let rcvd: u16 = block!(spi.read());

Modules

Implement embedded_hal traits for Spi structs
Valid transaction Lengths from the typenum crate

Structs

A configurable SPI peripheral in its disabled state
Type-level variant of the Capability enum for duplex transactions
Interrupt bit flags for SPI transactions
Container for a set of SERCOM pads
Type-level variant of the Capability enum for simplex, Receive-only transactions
An enabled SPI peripheral that can perform transactions
Status bit flags for SPI transactions
Type-level variant of the Capability enum for simplex, Transmit-only transactions

Enums

Define the bit order of transactions
Error enum for SPI transactions
OpMode variant for Master mode
OpMode variant for Master mode with hardware-controlled slave select
Clock phase
Clock polarity
OpMode variant for Slave mode

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

Type class for all possible Config types
Type class for all possible Spi types
Marker trait for transaction Sizes that can be completed in a single read or write of the DATA register
Type-level enum representing the simplex or duplex transaction capability
Map an OptionalPadNum to its corresponding DIPO value
Configure the DIPO and DOPO fields based on a set of Pads
Map an OptionalPadNum to its corresponding DOPO value
Marker trait for transaction Lengths greater than four
Type-level enum representing the SPI transaction length, in bytes
Marker trait for Master operating modes
Type-level enum representing the SPI operating mode
Type-level function to recover the OptionalPad types from a generic set of Pads
Sub-set of Capability variants that can receive data, i.e. Rx and Duplex
Trait alias whose definition varies by chip
Marker trait for statically known transaction Lengths
Sub-set of Capability variants that can transmit dat, i.e. Tx and Duplex
Marker trait for valid SPI Configurations
Marker trait for valid sets of Pads

Type Definitions

Type alias for the width of the DATA register
Type alias for the default Size type, which varies by chip
Marker type for a run-time dynamic Length
Define a set of Pads using PinIds instead of Pins
Type alias to recover the specific Config type from an implementation of AnyConfig
Type alias to recover the specific Spi type from an implementation of AnySpi
Type alias to recover the Word type from an implementation of Length