Expand description

A Future-like interface for SPI transactions

An SpiFuture takes ownership of an Spi struct and a [u8]-like buffer. It then executes a full-duplex SPI transaction using iterrupts. On each RXC or DRE interrupt, the SpiFuture reads or sends STEP bytes of the buffer, where STEP is a value that depends on CharSize, for SAMD11 & SAMD21 chips, or Length, for SAMD51 & SAME5x chips.

The provided buffer must implement AsRef and AsMut for [u8], it must have an appropriate length (see below), and it must have a 'static lifetime, i.e. it must be owned or a &'static mut reference.

SpiFuture has extra, optional capabilities as well. It can accept a function or closure that will be called on completion of the transaction, acting like a Waker. And it can take a GPIO Pin to use as the SS line. If provided, the Pin will be set low at the beginnging of the transfer and brought high at completion.

Calling start will enable the DRE and RXC interrupts and begin the transaction.

use atsamd_hal::gpio::{Pin, PA10, PushPullOutput};
use atsamd_hal::sercom::spi::AnySpi;
use atsamd_hal::sercom::spi_future::SpiFuture;

fn wake_up() {
    //...
}

fn use_future(spi: impl AnySpi, ss: Pin<PA10, PushPullOutput>) {
    let buf = [0_u8; 12];
    let future = SpiFuture::new(spi, buf)
        .with_waker(wake_up)
        .with_ss(ss);
    future.start();
}

When sending and receiving finish, the SpiFuture will automatically disable the DRE and RXC interrupts. To test whether an SpiFuture is complete, use the poll method. While the transaction is in progress, it will return Poll::Pending. When the transaction is complete, it will return Poll::Ready. Once complete, you can consume the SpiFuture and free the constituent pieces. Doing so before the transfer has completed is unsafe.

The actual transfer is performed by the send and recv methods, which should be called from the DRE and RXC interrupt handlers, respectively.

STEP size and buffer length

For SAMD11 & SAMD21 chips, STEP is equal to the number of bytes in the corresponding the CharSize::Word type, i.e. 1 for EightBit and 2 for NineBit. For SAMD51 & SAME5x chips, STEP is equal to the Length or 4, whichever is less.

The provided buffer must have an appropriate length. For SAMD11 & SAMD21 chips, as well as SAMDx5x chips with Length <= 4, a single write of STEP bytes represents an entire SPI transaction. In these cases, the provided buffer must represent an integer number of transactions. For example, a SAMD51 Spi struct with a Length of 3 could use buffers of length 3, 6, 9, etc. For longer Length values, the provided buffer must represent exactly one SPI transaction, so the buffer length must be equal to Length. For example, a SAMD51 Spi struct with a Length of 17 could only use a buffer with exactly 17 bytes.

Keep in mind that SpiFuture works only with [u8]-like things, which can introduce some limitations.

Suppose you plan to execute NineBit transactions with a SAMD21 chip. Your data may come in the form of a [u16] slice. However, to use it with SpiFuture, you will need reformulate it as a [u8] slice. The easiest way to do so is probably to transmute the slice to [u8] or cast the reference to &'static mut [u8]. Both of these operations are sound, because u8 has no alignment requirements.

In another scenario, suppose you wanted to use a SAMx5x chip with a transaction Length of 3 bytes. Your data might come in the form of a [u32] slice. In this situation, it would not be appropriate to transmute or cast the data to a [u8] slice. SpiFuture expects the data to be a byte-packed [u8] slice, so the extra byte in each u32 makes it incompatible.

RTIC example

The RTIC framework provides a convenient way to store a statically allocated SpiFuture, so that it can be accessed by both the interrupt handlers and user code. The following example shows how SpiFutures might be used for a series of transactions. It was written for a SAMx5x chip, and it uses features from the latest release of RTIC, v0.6-alpha.0.

use core::task::Poll;
use atsamd_hal::gpio::{PA08, PA09, PA10, PA11, Pin, PushPullOutput};
use atsamd_hal::sercom::Sercom0;
use atsamd_hal::sercom::pad::{IoSet1, Pad0, Pad1, Pad3};
use atsamd_hal::sercom::spi::{self, Master, lengths::U12};
use atsamd_hal::sercom::spi_future::SpiFuture;

type Pads = spi::Pads<Sercom0, IoSet1, (Pad0, PA08), (Pad3, PA11), (Pad1, PA09)>;
type SS = Pin<PA10, PushPullOutput>;
type Spi = spi::Spi<spi::Config<Pads, Master, U12>>;
type Future = SpiFuture<Spi, [u8; 12], SS, fn()>;

//...

#[resources]
struct Resources {
    #[task_local]
    #[init(None)]
    opt_spi_ss: Option<(Spi, SS)>,

    #[lock_free]
    #[init(None)]
    opt_future: Option<Future>,
}

#[task(resources = [opt_spi_ss, opt_future])]
fn task(ctx: task::Context) {
    let task::Context { opt_spi_ss, opt_future } = ctx;
    match opt_future {
        Some(future) => {
            if let Poll::Ready(_) = future.poll() {
                let (spi, buf, ss) = opt_future.take().unwrap().free();
                *opt_spi_ss = Some((spi, ss));
                consume_data(buf);
            }
        }
        None => {
            if let Some((spi, ss)) = opt_spi_ss.take() {
                let buf: [u8; 12] = produce_data();
                let future = opt_future.get_or_insert(
                    SpiFuture::new(spi, buf)
                        .with_waker(|| { task::spawn().ok(); })
                        .with_ss(ss)
                );
                future.start();
            }
        }
    }
}

#[task(binds = SERCOM0_0, resources = [opt_future])]
fn dre(ctx: dre::Context) {
    ctx.resources.opt_future.as_mut().unwrap().send();
}

#[task(binds = SERCOM0_2, resources = [opt_future])]
fn rxc(ctx: rxc::Context) {
    ctx.resources.opt_future.as_mut().unwrap().recv();
}

//...

Structs

A Future-like interface for SPI transactions

Traits

Trait used to verify the SpiFuture buffer length
Trait used to control the SS line during an SpiFuture transaction