pw_spi#
Stable
pw_spi
provides a set of interfaces for communicating with Serial
Peripheral Interface (SPI) responders attached to a target. It also provides an
interface for implementing SPI responders.
Overview#
The pw_spi
module provides a series of interfaces that facilitate the
development of SPI responder drivers that are abstracted from the target’s
SPI hardware implementation. The interface consists of these main classes:
Initiator - Interface for configuring a SPI bus, and using it to transmit and receive data.
ChipSelector - Interface for enabling/disabling a SPI responder attached to the bus.
Device - primary HAL interface used to interact with a SPI responder.
Responder - Interface for implementing a SPI responder.
pw_spi
relies on a target-specific implementations of Initiator and ChipSelector to
be defined, and injected into Device objects
which are used to communicate with a given responder attached to a target’s SPI
bus.
Examples#
Constructing a SPI device#
constexpr pw::spi::Config kConfig = {
.polarity = pw::spi::ClockPolarity::kActiveHigh,
.phase = pw::spi::ClockPhase::kRisingEdge,
.bits_per_word = pw::spi::BitsPerWord(8),
.bit_order = pw::spi::BitOrder::kLsbFirst,
};
auto initiator = pw::spi::MyInitator();
auto mutex = pw::sync::VirtualMutex();
auto selector = pw::spi::MyChipSelector();
auto device = pw::spi::Device(
pw::sync::Borrowable<Initiator>(initiator, mutex), kConfig, selector);
This example demonstrates the construction of a Device from its object dependencies and configuration data; where
MyDevice
and MyChipSelector
are concrete implementations of the
Initiator and ChipSelector interfaces, respectively.
The use of Borrowable in the interface provides a mutual-exclusion wrapper for the injected Initiator, ensuring that transactions cannot be interrupted or corrupted by other concurrent workloads making use of the same SPI bus.
Once constructed, the device
object can then be passed to functions used to
perform SPI transfers with a target responder.
Performing a transfer#
pw::Result<SensorData> ReadSensorData(pw::spi::Device& device) {
std::array<std::byte, 16> raw_sensor_data;
constexpr std::array<std::byte, 2> kAccelReportCommand = {
std::byte{0x13}, std::byte{0x37}};
// This device supports full-duplex transfers
PW_TRY(device.WriteRead(kAccelReportCommand, raw_sensor_data));
return UnpackSensorData(raw_sensor_data);
}
The ReadSensorData()
function implements a driver function for a contrived
SPI accelerometer. The function performs a full-duplex transfer with the
device to read its current data.
As this function relies on the device
object that abstracts the details
of bus-access and chip-selection, the function is portable to any target
that implements its underlying interfaces.
Performing a multi-part transaction#
pw::Result<SensorData> ReadSensorData(pw::spi::Device& device) {
std::array<std::byte, 16> raw_sensor_data;
constexpr std::array<std::byte, 2> kAccelReportCommand = {
std::byte{0x13}, std::byte{0x37}};
// Creation of the RAII `transaction` acquires exclusive access to the bus
pw::spi::Device::Transaction transaction =
device.StartTransaction(pw::spi::ChipSelectBehavior::kPerTransaction);
// This device only supports half-duplex transfers
PW_TRY(transaction.Write(kAccelReportCommand));
PW_TRY(transaction.Read(raw_sensor_data))
return UnpackSensorData(raw_sensor_data);
// Destruction of RAII `transaction` object releases lock on the bus
}
The code above is similar to the previous example, but makes use of the
Transaction
API in Device to perform separate,
half-duplex Write()
and Read()
transfers, as is required by the sensor
in this example.
The use of the RAII transaction
object in this example guarantees that
no other thread can perform transfers on the same SPI bus
(Initiator) until it goes out-of-scope.
Responding to an initiator#
MyResponder responder;
responder.SetCompletionHandler([](ByteSpan rx_data, Status status) {
// Handle incoming data from initiator.
// ...
// Prepare data to send back to initiator during next SPI transaction.
responder.WriteReadAsync(tx_data, rx_data);
});
// Prepare data to send back to initiator during next SPI transaction.
responder.WriteReadAsync(tx_data, rx_data)
Mocking transactions#
MockInitiator is a generic mocked backend
for Initiator
that is specifically intended for use when developing drivers
for SPI devices. It’s structured around a set of “transactions” where each
transaction contains a write, a read, and a status. A transaction list can then
be passed to the MockInitiator
, where each consecutive call to
read/write will iterate to the next transaction in the list. Example:
using pw::spi::MakeExpectedTransactionlist;
using pw::spi::MockInitiator;
using pw::spi::MockWriteTransaction;
constexpr auto kExpectWrite1 = pw::bytes::Array<1, 2, 3, 4, 5>();
constexpr auto kExpectWrite2 = pw::bytes::Array<3, 4, 5>();
auto expected_transactions = MakeExpectedTransactionArray(
{MockWriteTransaction(pw::OkStatus(), kExpectWrite1),
MockWriteTransaction(pw::OkStatus(), kExpectWrite2)});
MockInitiator spi_mock(expected_transactions);
// Begin driver code
ConstByteSpan write1 = kExpectWrite1;
// write1 is ok as spi_mock expects {1, 2, 3, 4, 5} == {1, 2, 3, 4, 5}
Status status = spi_mock.WriteRead(write1, ConstByteSpan());
// Takes the first two bytes from the expected array to build a mismatching
// span to write.
ConstByteSpan write2 = pw::span(kExpectWrite2).first(2);
// write2 fails as spi_mock expects {3, 4, 5} != {3, 4}
status = spi_mock.WriteRead(write2, ConstByteSpan());
// End driver code
// Optionally check if the mocked transaction list has been exhausted.
// Alternatively this is also called from MockInitiator::~MockInitiator().
EXPECT_EQ(spi_mock.Finalize(), OkStatus());
API reference#
Moved: pw_spi