Kernel Serial Peripheral Interface (SPI) HIL
TRD:
Working Group: Kernel
Type: Documentary
Status: Draft
Author: Philip Levis, Alexandru Radovici
Draft-Created: 2021/08/13
Draft-Modified: 2021/08/13
Draft-Version: 2
Draft-Discuss: tock-dev@googlegroups.com
Abstract
This document proposes hardware independent layer interface (HIL) for a serial peripheral interface (SPI) bus in the Tock operating system kernel. It describes the Rust traits and other definitions for this service as well as the reasoning behind them. This document is in full compliance with TRD1.
Note that this HIL has not been implemented yet in the master branch of Tock -- this is a working document as the HIL is designed.
1 Introduction
The serial peripheral interface (SPI) is a standard bus design for processors and microcontrollers to exchange data with sensors, I/O devices, and other off-chip compoments. The bus is clocked. The device driving the clock is called a "master" or "controller" and the device whose clock is driven is called a "slave" or "peripheral". A SPI bus has three data lines: the clock (CLK), data from the controller to the peripheral (MOSI) and data from the peripheral to the controller (MISO). A SPI bus does not have addressing. Instead, peripherals have a chip select (CS) pin. When a peripheral's chip select line is brought low, it receives data on MOSI and sends data on MISO. A controller can connect to CS pins on many different devices and share the bus between them by explicitly controlling which ones are active.
The SPI HIL is in the kernel crate, in module hil::spi
. It provides seven main
traits:
kernel::hil::spi::Configure
: provides an abstraction of configuring a SPI bus by setting its data rate, phase, and polarity.kernel::hil::spi::Controller
: allows a client for a SPI in controller mode to send and receive data.kernel::hil::spi::ControllerDevice
: combinesConfigure
andController
to provide an abstraction of a SPI bus in controller mode for a client that is bound to a specific chip select (e.g., a sensor driver). It allows a client to send and receive data as well as configure the bus for (only) its own operations.kernel::hil::spi::ChipSelect
: allows a client to change which chip select is active on a SPI bus incontroller mode.kernel::hil::spi::ControllerBus
: combinesControllerDevice
andChipSelect
to allow a client to issue SPI operations on any chip select. It also supports initializing the bus hardware. This trait is intended to be implemented by a chip implementation.kernel::hil::spi::PeripheralDevice
: extendsConfigure
and provides an abstraction of a SPI bus in peripheral mode. It allows a client to learn when it is selected, to send and receive data, and configure the bus for its own operations.kernel::hil::spi::PeripheralBus
: extendsPeripheralDevice
to support initializing the bus hardware. This trait is intended to be implemented by a chip peripheral implementation.kernel::hil::spi::Bus
: represents a SPI bus that can be dynamically changed between controller and peripheral modes. This trait is intended to be implemented by a chip implementation.
A given board MUST NOT include an implementation of more than one of
the ControllerBus
, PeripheralBus
, and Bus
traits for a given SPI
bus. these traits are mutually exclusive.
This document describes these traits and their semantics.
2 Configure
trait
The Configure
trait allows a client to set the data rate (clock
frequency) of the SPI bus as well as its polarity and phase. Polarity
controls whether the clock line is high or low when the bus is
idle. Phase controls on which clock edges the bus clocks data in and
out. It also allows configuring whether data is sent most significant
bit first or least significant bit first.
#![allow(unused)] fn main() { pub enum DataOrder { MSBFirst, LSBFirst, } pub enum ClockPolarity { IdleLow, IdleHigh, } pub enum ClockPhase { SampleLeading, SampleTrailing, } pub trait Configure { fn set_rate(&self, rate: u32) -> Result<u32, ErrorCode>; fn get_rate(&self) -> u32; fn set_polarity(&self, polarity: ClockPolarity) -> Result<(), ErrorCode>; fn get_polarity(&self) -> ClockPolarity; fn set_phase(&self, phase: ClockPhase) -> Result<(), ErrorCode>; fn get_phase(&self) -> ClockPhase; fn set_data_order(&self, order: DataOrder) -> Result<(), ErrorCode>; fn get_data_order(&self) -> DataOrder; } }
All of the set methods in Configure
can return an error. Valid errors are:
- INVAL (
set_rate
only): the parameter is outside the allowed range - NOSUPPORT (
set_polarity
,set_phase
,set_data_order
): the parameter provided cannot be supported. For example, a SPI bus that cannot have anIdleHigh
polarity returns NOSUPPORT if a client tries to set it to have this polarity. - OFF (all): the bus is currently powered down in a state that does not allow configuring it.
- BUSY (all): the bus is in the midst of an operation and cannot currently change its configuration.
- FAIL (all): some other error occurred.
The set_rate
method returns a u32
in its success case. This is the
actual data rate set, which may differ from the one passed, e.g., due
to clock precision or prescalars. The actual rate rate set MUST be
less than the rate
passed. If no rate can be set (e.g., the rate
is too small), set_rate
MUST return Err(INVAL)
.
The relationship of phase and polarity follows the standard SPI specification[1]:
+------------+------------------+-------------+----------------+----------------+ | Polarity | Phase | Idle Level | Data Out | Data In | +------------+------------------+-------------+----------------+----------------+ | IdleLow | SampleLeading | Low | Rising Edge | Falling Edge | | IdleLow | SampleTrailing | Low | Falling Edge | Rising Edge | | IdleHigh | SampleLeading | High | Rising Edge | Rising Edge | | IdleHigh | SampleTrailing | High | Falling Edge | Falling Edge | +------------+------------------+-------------+----------------+----------------+
If the SPI bus is in the middle an outstanding operation
(Controller::read_write_bytes
or Peripheral::read_write_bytes
),
calls to Configure
to set values MUST return BUSY.
3 Controller
, ControllerDevice
, and ControllerClient
traits
The Controller
trait allows a client to send and receive data on a SPI bus
in controller mode:
#![allow(unused)] fn main() { pub trait Controller<'a> { fn set_client(&self, client: &'a dyn ControllerClient); fn read_write_bytes( &self, write_buffer: &'a mut [u8], read_buffer: Option<&'a mut [u8]>, len: usize, ) -> Result<(), (ErrorCode, &'a mut [u8], Option<&'a mut [u8]>)>; } pub trait ControllerClient<'a> { fn read_write_done( &self, write_buffer: &'a mut [u8], read_buffer: Option<&'a mut [u8]>, len: usize, status: Result<(), ErrorCode>, ); } }
The read_write_bytes
method always takes a buffer to write and has
an optional buffer to read into. For operations that do not need to
read from the SPI peripheral, the read_buffer
can be None
.
If the call to read_write_bytes
returns Ok(())
, the implementation
MUST issue a callback to the SpiControllerClient
when it
completes. If the call returns an Err
, the implementation MUST NOT
issue a callback, except if the ErrorCode
is BUSY
. In this case,
the implementation issues a callback for the outstanding operation but
does not issue a callback for the failed one. If it returns Err
, the
implementation MUST return the buffers passed in the call. Valid
ErrorCode
values for an Err
result are:
- BUSY: the SPI is busy with another call to
read_write_bytes
and so cannot complete the request. - OFF: the SPI is off and cannot accept a request.
- INVAL: the length value is 0, or one of the buffers passed has length 0.
- RESERVE: there is no client for a callback.
SIZE
: one of the buffers passed is smaller thanlen
:len
bytes cannot be transferred.- FAIL: some other failure condition.
The set_client
method sets which callback to invoke when a
read_write_bytes
call completes. The read_write_done
callback MUST
return the buffers passed in the call to read_write_bytes
. The len
argument is the number of bytes read/written. The status
argument
indicates whether the SPI operation completed successfully. It may
return any of the ErrorCode
values that can be returned by
read_write_bytes
: these represent asynchronous errors (e.g., due to
queueing).
The ControllerDevice
trait combines Controller
and Configure
traits. It provides the abstraction of being able to read/write to
the bus and adjust its configuration.
#![allow(unused)] fn main() { pub trait ControllerDevice<'a>: Controller<'a> + Configure<'a> {} }
4 ChipSelect
and ControllerBus
The ChipSelect
trait allows a client to change which chip select is
active on the SPI bus. Because different SPI hardware can provide
different numbers of chip selects, the actual chip select value is an
associated type. This associated type is typically an enum
so a chip
implementation can statically verify that clients pass only valid chip
select values.
#![allow(unused)] fn main() { pub trait ChipSelect { type Value: Copy; fn set_chip_select(&self, cs: Self::Value) -> Result<(), ErrorCode>; fn get_chip_select(&self) -> Self::Value); } }
The ControllerBus
trait combines ControllerDevice
and ChipSelect
to provide the full abstraction of a SPI bus. It is the trait that
chip SPI implementations provide. In addition to ControllerDevice
and ChipSelect
, ControllerBus
includes an init
method. This
init
method initializes the hardware to be a SPI controller and is
typically called at boot.
#![allow(unused)] fn main() { pub trait ControllerBus<'a>: ControllerDevice<'a> + ChipSelect { fn init(&self) -> Result<(), ErrorCode>; } }
The Err
result of init
can return the following ErrorCode
values:
- OFF: not currently powered so can't be initialized.
- RESERVE: no clock is configured yet.
- FAIL: other failure condition.
A client using a ControllerBus
can exchange data with multiple SPI
peripherals, switching between them with ChipSelect
. Calls to
Configure
modify the configuration of the current chip select, which
are stateful. Changing the chip select uses the last configuration
set for that chip select. For example,
#![allow(unused)] fn main() { bus.set_chip_select(1); bus.set_phase(SampleLeading); bus.set_chip_select(2); bus.set_phase(SampleTrailing); bus.set_chip_select(1); bus.read_write_bytes(...); // Uses SampleLeading }
will have a SampleLeading phase in the final write_byte_bytes
call,
because the configuration of chip select 1 is saved, and restored when
chip select is set back to 1.
5 Peripheral
and PeripheralClient
traits
When a chip acts as a SPI peripheral, it does not drive the
clock. Instead, it response to the clock of the controller. In some
cases, the peripheral must be able to respond with a bit of data
before it has even received one (e.g., if phase is set to
SampleLeading
). As a result, a peripheral read/write request may
never complete if the controller never issues a request of its
own. The peripheral has to provide read and write buffers in
anticipation of a controller request. Unlike a controller, which must
always write data, a peripheral can only read, only write, or read and
write.
#![allow(unused)] fn main() { pub trait Peripheral { fn set_client(&self, client: &'static dyn PeripheralClient); fn read_write_bytes(&self, write_buffer: Option<&'static mut [u8]>, read_buffer: Option<&'static mut [u8]>, len: usize,) -> Result< (), (ErrorCode, Option<&'static mut [u8]>, Option<&'static mut [u8]>, ), >; fn set_write_byte(&self, write_byte: u8); } pub trait PeripheralClient { fn chip_selected(&self); fn read_write_done( &self, write_buffer: Option<&'static mut [u8]>, read_buffer: Option<&'static mut [u8]>, len: usize, status: Result<(), ErrorCode>, ); } }
The Peripheral
API differs from the Controller
in three ways:
read_write_bytes
has an optional write buffer,- clients have a
chip_selected
callback, and - a peripheral can set its write as a single-byte value.
When a controller brings the chip select line low, the implementation
calls the chip_selected
callback to inform the peripheral that an
operation is starting. Because a controller may begin clocking data
almost immediately after the chip select is brought low (e.g., a SPI
clock tick, so in some cases a few hundred nanoseconds. Because this
is faster than the chip_selected
callback can typically be issued,
the client SHOULD have already made a read_write_bytes
or
set_write_byte
call, so the SPI hardware has a byte ready to send.
The set_write_byte
call sets the byte that the SPI peripheral should
write to the controller. The peripheral will write this byte on each
SPI byte operation until the next call to set_write_byte
or
read_write_bytes
with a write buffer argument.
The read_write_bytes
method takes two Option
types: one for the
write buffer and one for the read buffer. The SPI peripheral will read
bytes written by the controller into the read buffer, and will write
out the bytes in the write buffer to the controller. If no write
buffer is provided, the bytes the peripheral will write are
undefined. If read_write_bytes
returns Ok(())
, the request was
accepted and the implementation MUST issue a callback when the request
completes or has an error. The valid ErrorCode
values for
read_write_bytes
are:
- BUSY: the SPI is busy with another call to
read_write_bytes
and so cannot complete the request. - OFF: the SPI is off and cannot accept a request.
- INVAL: the
len
parameter was 0 or both buffers wereNone
. - RESERVE: there is no client for a callback.
- SIZE: one of the passed buffers is smaller than
len
.
The read_write_done
callback is called when the outstanding
read_write_bytes
request completes. The len
argument is how many
bytes were read/written. It may differ from the len
passed to
read_write_bytes
if one of the buffers is is shorter, or if an error
occured. It may return any of the ErrorCode
values that can be
returned by read_write_bytes
: these represent asynchronous errors
(e.g., due to arbitration).
6 PeripheralDevice
and PeripheralBus
traits
The PeripheralDevice
trait represents the standard client
abstraction of a SPI peripheral. It combines Peripheral
and
Configure
:
#![allow(unused)] fn main() { pub trait PeripheralDevice<'a>: Peripheral<'a> + Configure {} }
PeripheralBus represents the lowest-level hardware abstraction of a
SPI peripheral. It is the trait that chip implementations typically
implement. It is PeripheralDevice
plus an init()
method for
initializing hardware to be a SPI peripheral:
#![allow(unused)] fn main() { pub trait PeripheralBus<'a>: PeripheralDevice<'a> { fn init(&self) -> Result<(), ErrorCode>; } }
The Err
result of init
can return the following ErrorCode
values:
- OFF: not currently powered so can't be initialized.
- FAIL: other failure condition.
7 Bus
trait
The ControllerBus
and PeripheralBus
traits are intended for use
cases when a given SPI block is always used as either or a controller
or always used as a peripheral. Some systems, however, require the bus
to change between these roles. For example, a board might export the
bus over an expansion header, and whether it behaves as a peripheral
or controller depends on what it's plugged into and which userspace
processes run.
The Bus
trait allows software to dynamically change a SPI bus
between controller and peripheral mode.
#![allow(unused)] fn main() { pub trait Bus<'a>: PeripheralDevice<'a> + ControllerBus<'a> { fn make_controller(&self) -> Result<(), ErrorCode>; fn make_peripheral(&self) -> Result<(), ErrorCode>; fn is_controller(&self) -> bool; fn is_peripheral(&self) -> bool; } }
If software invokes a Peripheral
operation while the bus is in
controller mode, the method MUST return OFF. If software invokes a
Controller
operation while the bus is in peripheral mode, the method
MUST return off. Changing the controller chip select while the device
is in peripheral mode changes the chip select configuration of the
controller but MUST NOT have an effect on peripheral mode.
When a Bus
first starts and is initialized, it MUST be in controller
mode, as the init()
method is part of the ControllerBus
trait.
8 Capsules
This section describes the standard Tock capsules for SPI communication.
9 Implementation Considerations
10 Authors' Address
Philip Levis
409 Gates Hall
Stanford University
Stanford, CA 94305
USA
pal@cs.stanford.edu
Alexandru Radovici <msg4alex@gmail.com>