| .. _module-pw_spi: |
| |
| ====== |
| pw_spi |
| ====== |
| Pigweed's SPI module provides a set of interfaces for communicating with SPI |
| peripherals attached to a target. |
| |
| -------- |
| Overview |
| -------- |
| The ``pw_spi`` module provides a series of interfaces that facilitate the |
| development of SPI peripheral drivers that are abstracted from the target's |
| SPI hardware implementation. The interface consists of three main classes: |
| |
| - ``pw::spi::Initiator`` - Interface for configuring a SPI bus, and using it |
| to transmit and receive data. |
| - ``pw::spi::ChipSelector`` - Interface for enabling/disabling a SPI |
| peripheral attached to the bus. |
| - ``pw::spi::Device`` - primary HAL interface used to interact with a SPI |
| peripheral. |
| |
| ``pw_spi`` relies on a target-specific implementations of |
| ``pw::spi::Initiator`` and ``pw::spi::ChipSelector`` to be defined, and |
| injected into ``pw::spi::Device`` objects which are used to communicate with a |
| given peripheral attached to a target's SPI bus. |
| |
| Example - Constructing a SPI Device: |
| |
| .. code-block:: cpp |
| |
| 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 selector = pw::spi::MyChipSelector(); |
| auto borrowable_initiator = pw::sync::Borrowable<Initiator&>(initiator); |
| |
| auto device = pw::spi::Device(borrowable_initiator, kConfig, selector); |
| |
| This example demonstrates the construction of a ``pw::spi::Device`` from its |
| object dependencies and configuration data; where ``MyDevice`` and |
| `MyChipSelector`` are concrete implementations of the ``pw::spi::Initiator`` |
| and ``pw::spi::ChipSelector`` interfaces, respectively. |
| |
| The use of ``pw::sync::Borrowable`` in the interface provides a |
| mutual-exclusion wrapper for the the injected ``pw::spi::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 peripheral. |
| |
| Example - Performing a Transfer: |
| |
| .. code-block:: cpp |
| |
| 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. |
| |
| Example - Performing a Multi-part Transaction: |
| |
| .. code-block:: cpp |
| |
| 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 |
| std::optional<pw::spi::Device::Transaction> transaction = |
| device.StartTransaction(pw::spi::ChipSelectBehavior::kPerTransaction); |
| if (!transaction.has_value()) { |
| return pw::Status::Unknown(); |
| ) |
| |
| // 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 ``pw::spi::Device`` to perform separate, half-duplex |
| ``Write()`` and ``Read()`` transfers, as is required by the sensor in this |
| examplre. |
| |
| The use of the RAII ``transaction`` object in this example guarantees that |
| no other thread can perform transfers on the same SPI bus |
| (``pw::spi::Initiator``) until it goes out-of-scope. |
| |
| ------------------ |
| pw::spi Interfaces |
| ------------------ |
| The SPI API consists of the following components: |
| |
| - The ``pw::spi::Initiator`` interface, and its associated configuration data |
| structs. |
| - The ``pw::spi::ChipSelector`` interface. |
| - The ``pw::spi::Device`` class. |
| |
| pw::spi::Initiator |
| ------------------ |
| .. inclusive-language: disable |
| |
| The common interface for configuring a SPI bus, and initiating transfers using |
| it. |
| |
| A concrete implementation of this interface class *must* be defined in order |
| to use ``pw_spi`` with a specific target. |
| |
| The ``spi::Initiator`` object configures the SPI bus to communicate with a |
| defined set of common bus parameters that include: |
| |
| - clock polarity/phase |
| - bits-per-word (between 3-32 bits) |
| - bit ordering (LSB or MSB first) |
| |
| These bus configuration parameters are aggregated in the ``pw::spi::Config`` |
| structure, and passed to the ``pw::spi::Initiator`` via its ``Configure()`` |
| method. |
| |
| .. Note: |
| |
| Throughout ``pw_spi``, the terms "controller" and "peripheral" are used to |
| describe the two roles SPI devices can implement. These terms correspond |
| to the "master" and "slave" roles described in legacy documentation |
| related to the SPI protocol. |
| |
| ``pw_spi`` only supports SPI transfers where the target implements the |
| "controller" role, and does not support the target acting in the |
| "peripheral" role. |
| |
| .. inclusive-language: enable |
| |
| .. cpp:class:: pw::spi::Initiator |
| |
| .. cpp:function:: Status Configure(const Config& config) |
| |
| Configure the SPI bus to communicate using a specific set of properties, |
| including the clock polarity, clock phase, bit-order, and bits-per-word. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure conditions |
| |
| .. cpp:function:: Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer) = 0; |
| |
| Perform a synchronous read/write operation on the SPI bus. Data from the |
| `write_buffer` object is written to the bus, while the `read_buffer` is |
| populated with incoming data on the bus. The operation will ensure that |
| all requested data is written-to and read-from the bus. In the event the |
| read buffer is smaller than the write buffer (or zero-size), any |
| additional input bytes are discarded. In the event the write buffer is |
| smaller than the read buffer (or zero size), the output is padded with |
| 0-bits for the remainder of the transfer. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| pw::spi::ChipSelector |
| --------------------- |
| The ChipSelector class provides an abstract interface for controlling the |
| chip-select signal associated with a specific SPI peripheral. |
| |
| This interface provides a ``SetActive()`` method, which activates/deactivates |
| the device based on the value of the `active` parameter. The associated |
| ``Activate()`` and ``Deactivate()`` methods are utility wrappers for |
| ``SetActive(true)`` and ``SetActive(false)``, respectively. |
| |
| A concrete implementation of this interface class must be provided in order to |
| use the SPI HAL to communicate with a peripheral. |
| |
| .. Note:: |
| |
| `Active` does not imply a specific logic-level; it is left to the |
| implementor to correctly map logic-levels to the device's active/inactive |
| states. |
| |
| .. cpp:class:: pw::spi::ChipSelector |
| |
| .. cpp:function:: Status SetActive(bool active) |
| |
| SetActive sets the state of the chip-select signal to the value |
| represented by the `active` parameter. Passing a value of `true` will |
| activate the chip-select signal, and `false` will deactivate the |
| chip-select signal. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Status Activate() |
| |
| Helper method to activate the chip-select signal |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Status Deactivate() |
| |
| Helper method to deactivate the chip-select signal |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| pw::spi::Device |
| --------------- |
| This is primary object used by a client to interact with a target SPI device. |
| It provides a wrapper for an injected ``pw::spi::Initiator`` object, using |
| its methods to configure the bus and perform individual SPI transfers. The |
| injected ``pw::spi::ChipSelector`` object is used internally to activate and |
| de-actviate the device on-demand from within the data transfer methods. |
| |
| The ``Read()``/``Write()``/``WriteRead()`` methods provide support for |
| performing individual transfers: ``Read()`` and ``Write()`` perform |
| half-duplex operations, where ``WriteRead()`` provides support for |
| full-duplex transfers. |
| |
| The ``StartTransaction()`` method provides support for performing multi-part |
| transfers consisting of a series of ``Read()``/``Write()``/``WriteRead()`` |
| calls, during which the caller is guaranteed exclusive access to the |
| underlying bus. The ``Transaction`` objects returned from this method |
| implements the RAII layer providing exclusive access to the bus; exclusive |
| access locking is released when the ``Transaction`` object is destroyed/goes |
| out of scope. |
| |
| Mutual-exclusion to the ``pw::spi::Initiator`` object is provided by the use of |
| the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is |
| "borrowed" for the duration of a transaction. |
| |
| .. cpp:class:: pw::spi::Device |
| |
| .. cpp:function:: Status Read(Bytespan read_buffer) |
| |
| Synchronously read data from the SPI peripheral until the provided |
| `read_buffer` is full. |
| This call will configure the bus and activate/deactivate chip select |
| for the transfer |
| |
| Note: This call will block in the event that other clients are currently |
| performing transactions using the same SPI Initiator. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Status Write(ConstByteSpan write_buffer) |
| |
| Synchronously write the contents of `write_buffer` to the SPI peripheral. |
| This call will configure the bus and activate/deactivate chip select |
| for the transfer |
| |
| Note: This call will block in the event that other clients are currently |
| performing transactions using the same SPI Initiator. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer) |
| |
| Perform a synchronous read/write transfer with the SPI peripheral. Data |
| from the `write_buffer` object is written to the bus, while the |
| `read_buffer` is populated with incoming data on the bus. In the event |
| the read buffer is smaller than the write buffer (or zero-size), any |
| additional input bytes are discarded. In the event the write buffer is |
| smaller than the read buffer (or zero size), the output is padded with |
| 0-bits for the remainder of the transfer. |
| This call will configure the bus and activate/deactivate chip select |
| for the transfer |
| |
| Note: This call will block in the event that other clients are currently |
| performing transactions using the same SPI Initiator. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Transaction StartTransaction(ChipSelectBehavior behavior) |
| |
| Begin a transaction with the SPI device. This creates an RAII |
| `Transaction` object that ensures that only one entity can access the |
| underlying SPI bus (Initiator) for the object's duration. The `behavior` |
| parameter provides a means for a client to select how the chip-select |
| signal will be applied on Read/Write/WriteRead calls taking place with |
| the Transaction object. A value of `kPerWriteRead` will activate/deactivate |
| chip-select on each operation, while `kPerTransaction` will hold the |
| chip-select active for the duration of the Transaction object. |
| |
| .. cpp:class:: pw::spi::Device::Transaction |
| |
| .. cpp:function:: Status Read(Bytespan read_buffer) |
| |
| Synchronously read data from the SPI peripheral until the provided |
| `read_buffer` is full. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Status Write(ConstByteSpan write_buffer) |
| |
| Synchronously write the contents of `write_buffer` to the SPI peripheral |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| .. cpp:function:: Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer) |
| |
| Perform a synchronous read/write transfer on the SPI bus. Data from the |
| `write_buffer` object is written to the bus, while the `read_buffer` is |
| populated with incoming data on the bus. The operation will ensure that |
| all requested data is written-to and read-from the bus. In the event the |
| read buffer is smaller than the write buffer (or zero-size), any |
| additional input bytes are discarded. In the event the write buffer is |
| smaller than the read buffer (or zero size), the output is padded with |
| 0-bits for the remainder of the transfer. |
| |
| Returns OkStatus() on success, and implementation-specific values on |
| failure. |
| |
| pw::spi::MockInitiator |
| ---------------------- |
| A generic mocked backend for for pw::spi::Initiator. This is specifically |
| intended for use when developing drivers for spi devices. This is structured |
| around a set of 'transactions' where each transaction contains a write, 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. An example of this is shown below: |
| |
| .. code-block:: cpp |
| |
| 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()); |
| |