diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22e79a491..04c235f79 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,6 +185,8 @@ jobs: target: esp32s3 - path: 'components/socket/example' target: esp32 + - path: 'components/spi/example' + target: esp32s3 - path: 'components/st25dv/example' target: esp32s3 - path: 'components/state_machine/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 8ed892d8d..c4c73b878 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -113,6 +113,7 @@ jobs: components/serialization components/smartpanlee-sc01-plus components/socket + components/spi components/st25dv components/state_machine components/t_keyboard diff --git a/components/spi/CMakeLists.txt b/components/spi/CMakeLists.txt new file mode 100644 index 000000000..289418eae --- /dev/null +++ b/components/spi/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES "base_component" "esp_driver_gpio" "esp_driver_spi" "task" + ) diff --git a/components/spi/README.md b/components/spi/README.md new file mode 100644 index 000000000..e33663663 --- /dev/null +++ b/components/spi/README.md @@ -0,0 +1,13 @@ +# SPI Component + +[![Badge](https://components.espressif.com/components/espp/spi/badge.svg)](https://components.espressif.com/components/espp/spi) + +The `Spi` class provides a simple C++ wrapper around the ESP-IDF SPI master +driver. It manages SPI bus initialization, per-device registration, blocking +transfers, queued transactions, and bus acquisition. + +## Example + +The [example](./example) shows how to create an SPI bus, attach a device, and +perform a simple addressed read transaction, with the address phase driven on +MOSI and the response read back on MISO. diff --git a/components/spi/example/CMakeLists.txt b/components/spi/example/CMakeLists.txt new file mode 100644 index 000000000..cf50ea584 --- /dev/null +++ b/components/spi/example/CMakeLists.txt @@ -0,0 +1,21 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py driver task spi" + CACHE STRING + "List of components to include" + ) + +project(spi_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/spi/example/README.md b/components/spi/example/README.md new file mode 100644 index 000000000..5ec95495a --- /dev/null +++ b/components/spi/example/README.md @@ -0,0 +1,10 @@ +# SPI Example + +This example shows the use of the `Spi` component to create an SPI bus, attach a +device, and perform a simple addressed read transaction. + +## How to use example + +The example is intended primarily as a compile-time reference for the `Spi` +API. Update the GPIO assignments to match your hardware before flashing; the +address phase is driven on MOSI and the response is read on MISO. diff --git a/components/spi/example/main/CMakeLists.txt b/components/spi/example/main/CMakeLists.txt new file mode 100644 index 000000000..f5aaee7f5 --- /dev/null +++ b/components/spi/example/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES spi) diff --git a/components/spi/example/main/spi_example.cpp b/components/spi/example/main/spi_example.cpp new file mode 100644 index 000000000..9a874dfaa --- /dev/null +++ b/components/spi/example/main/spi_example.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include "spi.hpp" + +extern "C" void app_main(void) { + //! [spi example] + constexpr spi_host_device_t spi_host = SPI2_HOST; + constexpr auto sclk_gpio = GPIO_NUM_36; + constexpr auto mosi_gpio = GPIO_NUM_38; + constexpr auto miso_gpio = GPIO_NUM_35; + constexpr auto cs_gpio = GPIO_NUM_37; + constexpr uint8_t read_address = 0x80; + + espp::Spi spi({ + .host = spi_host, + .sclk_io_num = sclk_gpio, + .mosi_io_num = mosi_gpio, + .miso_io_num = miso_gpio, + .max_transfer_sz = 16, + }); + + std::error_code ec; + auto device = spi.add_device( + { + .address_bits = 8, + .mode = 0, + .clock_speed_hz = 1 * 1000 * 1000, + .cs_io_num = cs_gpio, + .queue_size = 1, + }, + ec); + if (ec || !device) { + std::printf("Failed to create SPI device: %s\n", ec.message().c_str()); + return; + } + + std::array rx_data{}; + if (!device->read(rx_data, {.address = read_address}, ec)) { + std::printf("SPI read failed: %s\n", ec.message().c_str()); + return; + } + + std::printf("Read bytes: 0x%02x 0x%02x\n", rx_data[0], rx_data[1]); + //! [spi example] +} diff --git a/components/spi/example/sdkconfig.defaults b/components/spi/example/sdkconfig.defaults new file mode 100644 index 000000000..c3667f3e3 --- /dev/null +++ b/components/spi/example/sdkconfig.defaults @@ -0,0 +1,4 @@ +# Common ESP-related +# +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 diff --git a/components/spi/idf_component.yml b/components/spi/idf_component.yml new file mode 100644 index 000000000..21b97a95f --- /dev/null +++ b/components/spi/idf_component.yml @@ -0,0 +1,20 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "SPI component for ESP-IDF" +url: "https://github.com/esp-cpp/espp/tree/main/components/spi" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/spi.html" +examples: + - path: example +tags: + - cpp + - Component + - SPI + - DMA +dependencies: + idf: + version: '>=5.0' + espp/base_component: '>=1.0' + espp/task: '>=1.0' diff --git a/components/spi/include/spi.hpp b/components/spi/include/spi.hpp new file mode 100644 index 000000000..0db5eed67 --- /dev/null +++ b/components/spi/include/spi.hpp @@ -0,0 +1,264 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "base_component.hpp" + +namespace espp { +/// @brief SPI master wrapper and helpers. +/// @details +/// `Spi` owns an SPI bus and can create one or more attached `Device` instances. +/// Those devices support blocking reads/writes, queued transactions, and bus +/// acquisition. +/// +/// \section spi_example Example +/// \snippet spi_example.cpp spi example +class Spi : public BaseComponent { +public: + /// @brief Bus-level configuration for the SPI master. + struct Config { + int isr_core_id = -1; ///< Core on which to initialize the SPI bus. + spi_host_device_t host = SPI2_HOST; ///< SPI host controller to use. + gpio_num_t sclk_io_num = GPIO_NUM_NC; ///< Clock pin. + gpio_num_t mosi_io_num = GPIO_NUM_NC; ///< MOSI pin. + gpio_num_t miso_io_num = GPIO_NUM_NC; ///< MISO pin. + gpio_num_t quadwp_io_num = GPIO_NUM_NC; ///< Quad WP pin. + gpio_num_t quadhd_io_num = GPIO_NUM_NC; ///< Quad HD pin. + int max_transfer_sz = 0; ///< Maximum transfer size in bytes. + uint32_t bus_flags = 0; ///< ESP-IDF bus configuration flags. + int intr_flags = 0; ///< Interrupt allocation flags. + spi_dma_chan_t dma_channel = SPI_DMA_CH_AUTO; ///< DMA channel selection. + bool auto_init = true; ///< Automatically initialize on construction. + Logger::Verbosity log_level = Logger::Verbosity::WARN; ///< Logger verbosity. + }; + + /// @brief Per-device configuration for a device attached to the SPI bus. + struct DeviceConfig { + int command_bits = 0; ///< Number of command bits sent before payload data. + int address_bits = 0; ///< Number of address bits sent before payload data. + int dummy_bits = 0; ///< Number of dummy bits inserted before payload data. + uint8_t mode = 0; ///< SPI mode. + int clock_speed_hz = 1 * 1000 * 1000; ///< Device clock speed. + int input_delay_ns = 0; ///< Input delay in nanoseconds. + gpio_num_t cs_io_num = GPIO_NUM_NC; ///< Chip-select pin. + int queue_size = 1; ///< Queue depth for asynchronous transactions. + uint32_t flags = 0; ///< ESP-IDF device flags. + uint16_t cs_ena_pretrans = 0; ///< CS lead time in SPI bit-cycles. + uint16_t cs_ena_posttrans = 0; ///< CS hold time in SPI bit-cycles. + transaction_cb_t pre_cb = nullptr; ///< Optional pre-transfer callback. + transaction_cb_t post_cb = nullptr; ///< Optional post-transfer callback. + }; + + /// @brief Per-transaction overrides for read/write/transfer helpers. + struct TransactionConfig { + uint16_t command = 0; ///< Command value for this transaction. + uint64_t address = 0; ///< Address value for this transaction. + uint32_t flags = 0; ///< ESP-IDF transaction flags. + size_t rx_length_bits = 0; ///< Optional receive length override, in bits. For this + ///< byte-oriented API it must be a multiple of 8, must not exceed + ///< the RX buffer capacity, and must not exceed the TX bit length + ///< when both TX and RX are used in the same transaction. + }; + + class Device; + struct BusLockState; + + /// @brief RAII helper for holding an acquired SPI device bus lock. + class BusLock { + public: + /// @brief Construct an empty bus lock. + BusLock(); + + BusLock(const BusLock &) = delete; + BusLock &operator=(const BusLock &) = delete; + + /// @brief Move-construct the bus lock. + /// @param other Lock to move from. + BusLock(BusLock &&other) noexcept; + + /// @brief Move-assign the bus lock. + /// @param other Lock to move from. + /// @return Reference to this lock. + BusLock &operator=(BusLock &&other) noexcept; + + /// @brief Release the held bus lock on destruction. + ~BusLock(); + + /// @brief Release the held SPI bus lock, if any. + void release(); + + /// @brief Check whether this object currently owns a bus lock. + /// @return True if a bus lock is held. + explicit operator bool() const; + + private: + explicit BusLock(std::shared_ptr state); + + std::shared_ptr state_{}; + + friend class Device; + }; + + /// @brief Construct the SPI bus wrapper. + /// @param config SPI bus configuration. + explicit Spi(const Config &config); + + /// @brief Deinitialize the SPI bus and remove any remaining devices. + ~Spi(); + + Spi(const Spi &) = delete; + Spi &operator=(const Spi &) = delete; + Spi(Spi &&) = delete; + Spi &operator=(Spi &&) = delete; + + /// @brief Convert a `Spi::Config` into an ESP-IDF `spi_bus_config_t`. + /// @param config High-level SPI bus configuration. + /// @return Equivalent ESP-IDF bus configuration. + static spi_bus_config_t make_bus_config(const Config &config); + + /// @brief Convert a `Spi::DeviceConfig` into an ESP-IDF device config. + /// @param config High-level SPI device configuration. + /// @return Equivalent ESP-IDF device interface configuration. + static spi_device_interface_config_t make_device_config(const DeviceConfig &config); + + /// @brief Initialize the SPI bus. + /// @param ec Error code populated on failure. + void init(std::error_code &ec); + + /// @brief Deinitialize the SPI bus. + /// @param ec Error code populated on failure. + void deinit(std::error_code &ec); + + /// @brief Check whether the SPI bus is initialized. + /// @return True if the SPI bus is initialized. + bool initialized() const; + + /// @brief Get the configured SPI host. + /// @return Configured SPI host controller. + spi_host_device_t host() const; + + /// @brief Add a device to the SPI bus. + /// @param config Device configuration. + /// @param ec Error code populated on failure. + /// @return Shared pointer to the registered device, or nullptr on failure. + std::shared_ptr add_device(const DeviceConfig &config, std::error_code &ec); + +private: + void prune_expired_devices_locked(); + + friend class Device; + + Config config_; + mutable std::recursive_mutex mutex_; + bool initialized_{false}; + bool deinitializing_{false}; + std::vector> devices_; +}; + +/// @brief Attached SPI device wrapper. +class Spi::Device : public BaseComponent { +public: + /// @brief Construct an unattached device wrapper. + /// @param log_level Logger verbosity inherited from the owning SPI bus. + /// @param config Device configuration. + explicit Device(Logger::Verbosity log_level, const DeviceConfig &config); + + /// @brief Remove the device from the SPI bus on destruction. + ~Device(); + + Device(const Device &) = delete; + Device &operator=(const Device &) = delete; + Device(Device &&) = delete; + Device &operator=(Device &&) = delete; + + /// @brief Check whether the device is attached to the SPI bus. + /// @return True if the device has a valid ESP-IDF handle. + bool initialized() const; + + /// @brief Get the underlying ESP-IDF device handle. + /// @return SPI device handle, or nullptr if unattached. + spi_device_handle_t handle() const; + + /// @brief Get the configuration used to create this device. + /// @return Device configuration. + const DeviceConfig &config() const; + + /// @brief Remove this device from the SPI bus. + /// @param ec Error code populated on failure. + void remove_device(std::error_code &ec); + + /// @brief Submit a blocking SPI transaction. + /// @param transaction Transaction to execute. + /// @param ec Error code populated on failure. + /// @return True on success. + bool transmit(spi_transaction_t &transaction, std::error_code &ec); + + /// @brief Submit a polling SPI transaction. + /// @param transaction Transaction to execute. + /// @param ec Error code populated on failure. + /// @return True on success. + bool polling_transmit(spi_transaction_t &transaction, std::error_code &ec); + + /// @brief Queue an asynchronous SPI transaction. + /// @param transaction Transaction to queue. + /// @param timeout Queue timeout in FreeRTOS ticks. + /// @param ec Error code populated on failure. + /// @return True on success. + bool queue_transaction(spi_transaction_t &transaction, TickType_t timeout, std::error_code &ec); + + /// @brief Wait for a queued SPI transaction to complete. + /// @param transaction Filled with the completed transaction pointer. + /// @param timeout Wait timeout in FreeRTOS ticks. + /// @param ec Error code populated on failure. + /// @return True on success. + bool get_transaction_result(spi_transaction_t **transaction, TickType_t timeout, + std::error_code &ec); + + /// @brief Acquire exclusive access to the SPI bus for this device. + /// @param timeout Acquire timeout in FreeRTOS ticks. + /// @param ec Error code populated on failure. + /// @return RAII bus lock object. + BusLock acquire_bus(TickType_t timeout, std::error_code &ec); + + /// @brief Write a transmit buffer using the configured per-transaction overrides. + /// @param data Data to transmit. + /// @param config Transaction overrides. + /// @param ec Error code populated on failure. + /// @return True on success. + bool write(std::span data, const TransactionConfig &config, std::error_code &ec); + + /// @brief Read data using the configured per-transaction overrides. + /// @param data Receive buffer to fill. + /// @param config Transaction overrides. + /// @param ec Error code populated on failure. + /// @return True on success. + bool read(std::span data, const TransactionConfig &config, std::error_code &ec); + + /// @brief Perform a combined transmit/receive transaction. + /// @param tx_data Transmit buffer. + /// @param rx_data Receive buffer. + /// @param config Transaction overrides. + /// @param ec Error code populated on failure. + /// @return True on success. + bool transfer(std::span tx_data, std::span rx_data, + const TransactionConfig &config, std::error_code &ec); + +private: + friend class Spi; + + DeviceConfig config_; + spi_device_handle_t handle_{nullptr}; + std::shared_ptr bus_lock_state_{}; + mutable std::recursive_mutex mutex_; +}; +} // namespace espp diff --git a/components/spi/src/spi.cpp b/components/spi/src/spi.cpp new file mode 100644 index 000000000..bb0226ef4 --- /dev/null +++ b/components/spi/src/spi.cpp @@ -0,0 +1,449 @@ +#include "spi.hpp" + +#include +#include +#include +#include + +#include "run_on_core.hpp" + +namespace espp { +struct Spi::BusLockState { + std::mutex mutex; + spi_device_handle_t handle{nullptr}; + bool lock_held{false}; +}; + +Spi::BusLock::BusLock() = default; + +Spi::BusLock::BusLock(std::shared_ptr state) + : state_(std::move(state)) {} + +Spi::BusLock::BusLock(BusLock &&other) noexcept + : state_(std::move(other.state_)) {} + +Spi::BusLock &Spi::BusLock::operator=(BusLock &&other) noexcept { + if (this == &other) { + return *this; + } + release(); + state_ = std::move(other.state_); + return *this; +} + +Spi::BusLock::~BusLock() { release(); } + +void Spi::BusLock::release() { + auto state = std::move(state_); + if (state) { + std::lock_guard lock(state->mutex); + if (state->lock_held && state->handle) { + spi_device_release_bus(state->handle); + state->lock_held = false; + } + } +} + +Spi::BusLock::operator bool() const { + if (!state_) { + return false; + } + std::lock_guard lock(state_->mutex); + return state_->lock_held && state_->handle != nullptr; +} + +Spi::Spi(const Config &config) + : BaseComponent("SPI", config.log_level) + , config_(config) { + if (config.auto_init) { + std::error_code ec; + init(ec); + if (ec) { + logger_.error("auto init failed"); + } + } +} + +Spi::~Spi() { + std::error_code ec; + deinit(ec); + if (ec) { + logger_.error("deinit failed"); + } +} + +spi_bus_config_t Spi::make_bus_config(const Config &config) { + spi_bus_config_t bus_config{}; + bus_config.mosi_io_num = config.mosi_io_num; + bus_config.miso_io_num = config.miso_io_num; + bus_config.sclk_io_num = config.sclk_io_num; + bus_config.quadwp_io_num = config.quadwp_io_num; + bus_config.quadhd_io_num = config.quadhd_io_num; + bus_config.max_transfer_sz = config.max_transfer_sz; + bus_config.flags = config.bus_flags; + bus_config.intr_flags = config.intr_flags; + return bus_config; +} + +spi_device_interface_config_t Spi::make_device_config(const DeviceConfig &config) { + spi_device_interface_config_t device_config{}; + device_config.command_bits = config.command_bits; + device_config.address_bits = config.address_bits; + device_config.dummy_bits = config.dummy_bits; + device_config.mode = config.mode; + device_config.clock_speed_hz = config.clock_speed_hz; + device_config.input_delay_ns = config.input_delay_ns; + device_config.spics_io_num = config.cs_io_num; + device_config.queue_size = config.queue_size; + device_config.flags = config.flags; + device_config.cs_ena_pretrans = config.cs_ena_pretrans; + device_config.cs_ena_posttrans = config.cs_ena_posttrans; + device_config.pre_cb = config.pre_cb; + device_config.post_cb = config.post_cb; + return device_config; +} + +void Spi::init(std::error_code &ec) { + std::lock_guard lock(mutex_); + if (deinitializing_) { + logger_.warn("cannot initialize while deinitialization is in progress"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return; + } + if (initialized_) { + logger_.warn("already initialized"); + ec = std::make_error_code(std::errc::protocol_error); + return; + } + + auto bus_config = make_bus_config(config_); + auto install_fn = [this, &bus_config]() -> esp_err_t { + return spi_bus_initialize(config_.host, &bus_config, config_.dma_channel); + }; + auto err = espp::task::run_on_core(install_fn, config_.isr_core_id); + if (err != ESP_OK) { + logger_.error("could not initialize SPI bus: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return; + } + initialized_ = true; + ec.clear(); +} + +void Spi::deinit(std::error_code &ec) { + std::vector> devices; + bool removal_failed = false; + { + std::lock_guard lock(mutex_); + if (!initialized_) { + ec.clear(); + return; + } + if (deinitializing_) { + logger_.warn("deinitialization already in progress"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return; + } + deinitializing_ = true; + prune_expired_devices_locked(); + for (auto &weak_device : devices_) { + if (auto device = weak_device.lock()) { + devices.push_back(device); + } + } + } + + for (auto &device : devices) { + std::error_code device_ec; + device->remove_device(device_ec); + if (device_ec) { + logger_.error("could not remove SPI device before bus free"); + removal_failed = true; + } + } + + if (removal_failed) { + std::lock_guard lock(mutex_); + deinitializing_ = false; + ec = std::make_error_code(std::errc::io_error); + return; + } + + std::lock_guard lock(mutex_); + auto err = spi_bus_free(config_.host); + if (err != ESP_OK) { + logger_.error("could not free SPI bus: {}", esp_err_to_name(err)); + deinitializing_ = false; + ec = std::make_error_code(std::errc::io_error); + return; + } + devices_.clear(); + initialized_ = false; + deinitializing_ = false; + ec.clear(); +} + +bool Spi::initialized() const { + std::lock_guard lock(mutex_); + return initialized_; +} + +spi_host_device_t Spi::host() const { return config_.host; } + +std::shared_ptr Spi::add_device(const DeviceConfig &config, std::error_code &ec) { + std::lock_guard lock(mutex_); + if (deinitializing_) { + logger_.error("bus is deinitializing"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return nullptr; + } + if (!initialized_) { + logger_.error("bus not initialized"); + ec = std::make_error_code(std::errc::not_connected); + return nullptr; + } + prune_expired_devices_locked(); + auto device = std::make_shared(get_log_level(), config); + auto device_config = make_device_config(config); + auto err = spi_bus_add_device(this->config_.host, &device_config, &device->handle_); + if (err != ESP_OK) { + logger_.error("could not add SPI device: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return nullptr; + } + { + std::lock_guard bus_lock_state_lock(device->bus_lock_state_->mutex); + device->bus_lock_state_->handle = device->handle_; + device->bus_lock_state_->lock_held = false; + } + devices_.push_back(device); + ec.clear(); + return device; +} + +void Spi::prune_expired_devices_locked() { + std::erase_if(devices_, [](const auto &device) { return device.expired(); }); +} + +Spi::Device::Device(Logger::Verbosity log_level, const DeviceConfig &config) + : BaseComponent("SPI Device", log_level) + , config_(config) + , bus_lock_state_(std::make_shared()) {} + +Spi::Device::~Device() { + std::error_code ec; + remove_device(ec); + if (ec) { + logger_.error("device removal failed"); + } +} + +bool Spi::Device::initialized() const { + std::lock_guard lock(mutex_); + return handle_ != nullptr; +} + +spi_device_handle_t Spi::Device::handle() const { + std::lock_guard lock(mutex_); + return handle_; +} + +const Spi::DeviceConfig &Spi::Device::config() const { return config_; } + +void Spi::Device::remove_device(std::error_code &ec) { + std::lock_guard lock(mutex_); + if (!handle_) { + ec.clear(); + return; + } + { + std::lock_guard bus_lock_state_lock(bus_lock_state_->mutex); + if (bus_lock_state_->lock_held && bus_lock_state_->handle == handle_) { + spi_device_release_bus(handle_); + bus_lock_state_->lock_held = false; + } + } + auto err = spi_bus_remove_device(handle_); + if (err != ESP_OK) { + logger_.error("could not remove SPI device: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return; + } + { + std::lock_guard bus_lock_state_lock(bus_lock_state_->mutex); + bus_lock_state_->handle = nullptr; + bus_lock_state_->lock_held = false; + } + handle_ = nullptr; + ec.clear(); +} + +bool Spi::Device::transmit(spi_transaction_t &transaction, std::error_code &ec) { + std::lock_guard lock(mutex_); + if (!handle_) { + logger_.error("device not initialized"); + ec = std::make_error_code(std::errc::not_connected); + return false; + } + auto err = spi_device_transmit(handle_, &transaction); + if (err != ESP_OK) { + logger_.error("could not transmit SPI transaction: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + ec.clear(); + return true; +} + +bool Spi::Device::polling_transmit(spi_transaction_t &transaction, std::error_code &ec) { + std::lock_guard lock(mutex_); + if (!handle_) { + logger_.error("device not initialized"); + ec = std::make_error_code(std::errc::not_connected); + return false; + } + auto err = spi_device_polling_transmit(handle_, &transaction); + if (err != ESP_OK) { + logger_.error("could not polling-transmit SPI transaction: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + ec.clear(); + return true; +} + +bool Spi::Device::queue_transaction(spi_transaction_t &transaction, TickType_t timeout, + std::error_code &ec) { + std::lock_guard lock(mutex_); + if (!handle_) { + logger_.error("device not initialized"); + ec = std::make_error_code(std::errc::not_connected); + return false; + } + auto err = spi_device_queue_trans(handle_, &transaction, timeout); + if (err != ESP_OK) { + logger_.error("could not queue SPI transaction: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + ec.clear(); + return true; +} + +bool Spi::Device::get_transaction_result(spi_transaction_t **transaction, TickType_t timeout, + std::error_code &ec) { + std::lock_guard lock(mutex_); + if (!handle_) { + logger_.error("device not initialized"); + ec = std::make_error_code(std::errc::not_connected); + return false; + } + auto err = spi_device_get_trans_result(handle_, transaction, timeout); + if (err != ESP_OK) { + logger_.error("could not get SPI transaction result: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + ec.clear(); + return true; +} + +Spi::BusLock Spi::Device::acquire_bus(TickType_t timeout, std::error_code &ec) { + std::lock_guard lock(mutex_); + if (!handle_) { + logger_.error("device not initialized"); + ec = std::make_error_code(std::errc::not_connected); + return {}; + } + auto err = spi_device_acquire_bus(handle_, timeout); + if (err != ESP_OK) { + logger_.error("could not acquire SPI bus: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return {}; + } + { + std::lock_guard bus_lock_state_lock(bus_lock_state_->mutex); + bus_lock_state_->handle = handle_; + bus_lock_state_->lock_held = true; + } + ec.clear(); + return BusLock(bus_lock_state_); +} + +bool Spi::Device::write(std::span data, const TransactionConfig &config, + std::error_code &ec) { + return transfer(data, {}, config, ec); +} + +bool Spi::Device::read(std::span data, const TransactionConfig &config, + std::error_code &ec) { + return transfer({}, data, config, ec); +} + +bool Spi::Device::transfer(std::span tx_data, std::span rx_data, + const TransactionConfig &config, std::error_code &ec) { + if (tx_data.empty() && rx_data.empty()) { + ec.clear(); + return true; + } + + const size_t tx_length_bits = tx_data.size() * 8; + const size_t rx_buffer_bits = rx_data.size() * 8; + const size_t rx_length_bits = config.rx_length_bits ? config.rx_length_bits : rx_buffer_bits; + const size_t rx_length_bytes = rx_length_bits / 8; + + if (!rx_data.empty() && (rx_length_bits % 8) != 0) { + logger_.error("requested RX length must be a multiple of 8 bits"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + + if (!rx_data.empty() && rx_length_bits > rx_buffer_bits) { + logger_.error("requested RX length exceeds RX buffer capacity"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + if (!tx_data.empty() && !rx_data.empty() && rx_length_bits > tx_length_bits) { + logger_.error("requested RX length exceeds TX clock length"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + + spi_transaction_t transaction{}; + transaction.cmd = config.command; + transaction.addr = config.address; + const auto managed_buffer_flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA; + transaction.flags = config.flags & ~managed_buffer_flags; + + if (!tx_data.empty()) { + transaction.length = tx_length_bits; + if (tx_data.size() <= sizeof(transaction.tx_data)) { + std::memcpy(transaction.tx_data, tx_data.data(), tx_data.size()); + transaction.flags |= SPI_TRANS_USE_TXDATA; + } else { + transaction.tx_buffer = tx_data.data(); + } + } else if (!rx_data.empty()) { + transaction.length = rx_length_bits; + } + + if (!rx_data.empty()) { + transaction.rxlength = rx_length_bits; + if (rx_data.size() <= sizeof(transaction.rx_data)) { + transaction.flags |= SPI_TRANS_USE_RXDATA; + } else { + transaction.rx_buffer = rx_data.data(); + } + } + + if (!transmit(transaction, ec)) { + return false; + } + + if (!rx_data.empty() && rx_data.size() <= sizeof(transaction.rx_data)) { + std::memcpy(rx_data.data(), transaction.rx_data, rx_length_bytes); + } + ec.clear(); + return true; +} +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index 3593046c4..a1f9156be 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -146,6 +146,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp \ $(PROJECT_PATH)/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp \ $(PROJECT_PATH)/components/socket/example/main/socket_example.cpp \ + $(PROJECT_PATH)/components/spi/example/main/spi_example.cpp \ $(PROJECT_PATH)/components/st25dv/example/main/st25dv_example.cpp \ $(PROJECT_PATH)/components/state_machine/example/main/hfsm_example.cpp \ $(PROJECT_PATH)/components/tabulate/example/main/tabulate_example.cpp \ @@ -325,6 +326,7 @@ INPUT = \ $(PROJECT_PATH)/components/socket/include/socket.hpp \ $(PROJECT_PATH)/components/socket/include/udp_socket.hpp \ $(PROJECT_PATH)/components/socket/include/tcp_socket.hpp \ + $(PROJECT_PATH)/components/spi/include/spi.hpp \ $(PROJECT_PATH)/components/st25dv/include/st25dv.hpp \ $(PROJECT_PATH)/components/state_machine/include/deep_history_state.hpp \ $(PROJECT_PATH)/components/state_machine/include/shallow_history_state.hpp \ diff --git a/doc/en/index.rst b/doc/en/index.rst index 6a2d4fca4..f3c6416ad 100755 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -67,6 +67,7 @@ This is the documentation for esp-idf c++ components, ESPP (`espp