diff --git a/CODEOWNERS b/CODEOWNERS index 498cfcac01..ab4c5011f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -270,6 +270,7 @@ esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/speaker/* @jesserockz esphome/components/spi/* @esphome/core +esphome/components/spi_device/* @clydebarrow esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index a2ef956200..79e7a5b034 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,6 +1,17 @@ +import re + import esphome.codegen as cg import esphome.config_validation as cv import esphome.final_validate as fv +from esphome.components.esp32.const import ( + KEY_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, +) from esphome import pins from esphome.const import ( CONF_CLK_PIN, @@ -9,6 +20,11 @@ from esphome.const import ( CONF_MOSI_PIN, CONF_SPI_ID, CONF_CS_PIN, + CONF_NUMBER, + CONF_INVERTED, + KEY_CORE, + KEY_TARGET_PLATFORM, + KEY_VARIANT, ) from esphome.core import coroutine_with_priority, CORE @@ -34,10 +50,147 @@ SPI_DATA_RATE_OPTIONS = { } SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS)) -MULTI_CONF = True CONF_FORCE_SW = "force_sw" +CONF_INTERFACE = "interface" +CONF_INTERFACE_INDEX = "interface_index" -CONFIG_SCHEMA = cv.All( + +def get_target_platform(): + return ( + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] + if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE] + else "" + ) + + +def get_target_variant(): + return ( + CORE.data[KEY_ESP32][KEY_VARIANT] if KEY_VARIANT in CORE.data[KEY_ESP32] else "" + ) + + +# Get a list of available hardware interfaces based on target and variant. +# The returned value is a list of lists of names +def get_hw_interface_list(): + target_platform = get_target_platform() + if target_platform == "esp8266": + return [["spi", "hspi"]] + if target_platform == "esp32": + if get_target_variant() in [ + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + ]: + return [["spi", "spi2"]] + return [["spi", "spi2"], ["spi3"]] + if target_platform == "rp2040": + return [["spi"]] + return [] + + +# Given an SPI name, return the index of it in the available list +def get_spi_index(name): + for i, ilist in enumerate(get_hw_interface_list()): + if name in ilist: + return i + # Should never get to here. + raise cv.Invalid(f"{name} is not an available SPI") + + +# Check that pins are suitable for HW spi +# TODO verify that the pins are internal +def validate_hw_pins(spi): + clk_pin = spi[CONF_CLK_PIN] + if clk_pin[CONF_INVERTED]: + return False + clk_pin_no = clk_pin[CONF_NUMBER] + sdo_pin_no = -1 + sdi_pin_no = -1 + if CONF_MOSI_PIN in spi: + sdo_pin = spi[CONF_MOSI_PIN] + if sdo_pin[CONF_INVERTED]: + return False + sdo_pin_no = sdo_pin[CONF_NUMBER] + if CONF_MISO_PIN in spi: + sdi_pin = spi[CONF_MISO_PIN] + if sdi_pin[CONF_INVERTED]: + return False + sdi_pin_no = sdi_pin[CONF_NUMBER] + + target_platform = get_target_platform() + if target_platform == "esp8266": + if clk_pin_no == 6: + return sdo_pin_no in (-1, 8) and sdi_pin_no in (-1, 7) + if clk_pin_no == 14: + return sdo_pin_no in (-1, 13) and sdi_pin_no in (-1, 12) + return False + + if target_platform == "esp32": + return clk_pin_no >= 0 + + return False + + +def validate_spi_config(config): + available = list(range(len(get_hw_interface_list()))) + for spi in config: + interface = spi[CONF_INTERFACE] + if spi[CONF_FORCE_SW]: + if interface == "any": + spi[CONF_INTERFACE] = interface = "software" + elif interface != "software": + raise cv.Invalid("force_sw is deprecated - use interface: software") + if interface == "software": + pass + elif interface == "any": + if not validate_hw_pins(spi): + spi[CONF_INTERFACE] = "software" + elif interface == "hardware": + if len(available) == 0: + raise cv.Invalid("No hardware interface available") + index = spi[CONF_INTERFACE_INDEX] = available[0] + available.remove(index) + else: + # Must be a specific name + index = spi[CONF_INTERFACE_INDEX] = get_spi_index(interface) + if index not in available: + raise cv.Invalid( + f"interface '{interface}' not available here (may be already assigned)" + ) + available.remove(index) + + # Second time around: + # Any specific names and any 'hardware' requests will have already been filled, + # so just need to assign remaining hardware to 'any' requests. + for spi in config: + if spi[CONF_INTERFACE] == "any" and len(available) != 0: + index = available[0] + spi[CONF_INTERFACE_INDEX] = index + available.remove(index) + if CONF_INTERFACE_INDEX in spi and not validate_hw_pins(spi): + raise cv.Invalid("Invalid pin selections for hardware SPI interface") + + return config + + +# Given an SPI index, convert to a string that represents the C++ object for it. +def get_spi_interface(index): + if CORE.using_esp_idf: + return ["SPI2_HOST", "SPI3_HOST"][index] + # Arduino code follows + platform = get_target_platform() + if platform == "rp2040": + return "&spi1" + if index == 0: + return "&SPI" + # Following code can't apply to C2, H2 or 8266 since they have only one SPI + if get_target_variant() in (VARIANT_ESP32S3, VARIANT_ESP32S2): + return "new SPIClass(FSPI)" + return "return new SPIClass(HSPI)" + + +SPI_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(SPIComponent), @@ -45,28 +198,47 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_FORCE_SW, default=False): cv.boolean, + cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( + *sum(get_hw_interface_list(), ["software", "hardware", "any"]), + lower=True, + ), } ), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN), cv.only_on(["esp32", "esp8266", "rp2040"]), ) +CONFIG_SCHEMA = cv.All( + cv.ensure_list(SPI_SCHEMA), + validate_spi_config, +) + @coroutine_with_priority(1.0) -async def to_code(config): +async def to_code(configs): cg.add_global(spi_ns.using) - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) + for spi in configs: + var = cg.new_Pvariable(spi[CONF_ID]) + await cg.register_component(var, spi) - clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) - cg.add(var.set_clk(clk)) - cg.add(var.set_force_sw(config[CONF_FORCE_SW])) - if CONF_MISO_PIN in config: - miso = await cg.gpio_pin_expression(config[CONF_MISO_PIN]) - cg.add(var.set_miso(miso)) - if CONF_MOSI_PIN in config: - mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) - cg.add(var.set_mosi(mosi)) + clk = await cg.gpio_pin_expression(spi[CONF_CLK_PIN]) + cg.add(var.set_clk(clk)) + if CONF_MISO_PIN in spi: + miso = await cg.gpio_pin_expression(spi[CONF_MISO_PIN]) + cg.add(var.set_miso(miso)) + if CONF_MOSI_PIN in spi: + mosi = await cg.gpio_pin_expression(spi[CONF_MOSI_PIN]) + cg.add(var.set_mosi(mosi)) + if CONF_INTERFACE_INDEX in spi: + index = spi[CONF_INTERFACE_INDEX] + cg.add(var.set_interface(cg.RawExpression(get_spi_interface(index)))) + cg.add( + var.set_interface_name( + re.sub( + r"\W", "", get_spi_interface(index).replace("new SPIClass", "") + ) + ) + ) if CORE.using_arduino: cg.add_library("SPI", None) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 33630897f6..935399500f 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -1,268 +1,116 @@ #include "spi.h" #include "esphome/core/log.h" -#include "esphome/core/helpers.h" #include "esphome/core/application.h" namespace esphome { namespace spi { -static const char *const TAG = "spi"; +const char *const TAG = "spi"; -void IRAM_ATTR HOT SPIComponent::disable() { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - this->hw_spi_->endTransaction(); - } -#endif // USE_SPI_ARDUINO_BACKEND - if (this->active_cs_) { - this->active_cs_->digital_write(true); - this->active_cs_ = nullptr; +SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + new SPIDelegateDummy(); +// https://bugs.llvm.org/show_bug.cgi?id=48040 + +bool SPIDelegate::is_ready() { return true; } + +GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, + GPIOPin *cs_pin) { + if (this->devices_.count(device) != 0) { + ESP_LOGE(TAG, "SPI device already registered"); + return this->devices_[device]; } + SPIDelegate *delegate = this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin); // NOLINT + this->devices_[device] = delegate; + return delegate; } + +void SPIComponent::unregister_device(SPIClient *device) { + if (this->devices_.count(device) == 0) { + esph_log_e(TAG, "SPI device not registered"); + return; + } + delete this->devices_[device]; // NOLINT + this->devices_.erase(device); +} + void SPIComponent::setup() { - ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); - this->clk_->setup(); - this->clk_->digital_write(true); + ESP_LOGD(TAG, "Setting up SPI bus..."); -#ifdef USE_SPI_ARDUINO_BACKEND - bool use_hw_spi = !this->force_sw_; - const bool has_miso = this->miso_ != nullptr; - const bool has_mosi = this->mosi_ != nullptr; - int8_t clk_pin = -1, miso_pin = -1, mosi_pin = -1; - - if (!this->clk_->is_internal()) - use_hw_spi = false; - if (has_miso && !miso_->is_internal()) - use_hw_spi = false; - if (has_mosi && !mosi_->is_internal()) - use_hw_spi = false; - if (use_hw_spi) { - auto *clk_internal = (InternalGPIOPin *) clk_; - auto *miso_internal = (InternalGPIOPin *) miso_; - auto *mosi_internal = (InternalGPIOPin *) mosi_; - - if (clk_internal->is_inverted()) - use_hw_spi = false; - if (has_miso && miso_internal->is_inverted()) - use_hw_spi = false; - if (has_mosi && mosi_internal->is_inverted()) - use_hw_spi = false; - - if (use_hw_spi) { - clk_pin = clk_internal->get_pin(); - miso_pin = has_miso ? miso_internal->get_pin() : -1; - mosi_pin = has_mosi ? mosi_internal->get_pin() : -1; - } - } -#ifdef USE_ESP8266 - if (!(clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) && - !(clk_pin == 14 && (!has_miso || miso_pin == 12) && (!has_mosi || mosi_pin == 13))) - use_hw_spi = false; - - if (use_hw_spi) { - this->hw_spi_ = &SPI; - this->hw_spi_->pins(clk_pin, miso_pin, mosi_pin, 0); - this->hw_spi_->begin(); + if (this->sdo_pin_ == nullptr) + this->sdo_pin_ = NullPin::NULL_PIN; + if (this->sdi_pin_ == nullptr) + this->sdi_pin_ = NullPin::NULL_PIN; + if (this->clk_pin_ == nullptr) { + ESP_LOGE(TAG, "No clock pin for SPI"); + this->mark_failed(); return; } -#endif // USE_ESP8266 -#ifdef USE_ESP32 - static uint8_t spi_bus_num = 0; - if (spi_bus_num >= 2) { - use_hw_spi = false; - } - if (use_hw_spi) { - if (spi_bus_num == 0) { - this->hw_spi_ = &SPI; - } else { -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C6) - this->hw_spi_ = new SPIClass(FSPI); // NOLINT(cppcoreguidelines-owning-memory) -#else - this->hw_spi_ = new SPIClass(HSPI); // NOLINT(cppcoreguidelines-owning-memory) -#endif // USE_ESP32_VARIANT + if (this->using_hw_) { + this->spi_bus_ = SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); + if (this->spi_bus_ == nullptr) { + ESP_LOGE(TAG, "Unable to allocate SPI interface"); + this->mark_failed(); } - spi_bus_num++; - this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin); - return; - } -#endif // USE_ESP32 -#ifdef USE_RP2040 - static uint8_t spi_bus_num = 0; - if (spi_bus_num >= 2) { - use_hw_spi = false; - } - if (use_hw_spi) { - SPIClassRP2040 *spi; - if (spi_bus_num == 0) { - spi = &SPI; - } else { - spi = &SPI1; - } - spi_bus_num++; - - if (miso_pin != -1) - spi->setRX(miso_pin); - if (mosi_pin != -1) - spi->setTX(mosi_pin); - spi->setSCK(clk_pin); - this->hw_spi_ = spi; - this->hw_spi_->begin(); - return; - } -#endif // USE_RP2040 -#endif // USE_SPI_ARDUINO_BACKEND - - if (this->miso_ != nullptr) { - this->miso_->setup(); - } - if (this->mosi_ != nullptr) { - this->mosi_->setup(); - this->mosi_->digital_write(false); + } else { + this->spi_bus_ = new SPIBus(this->clk_pin_, this->sdo_pin_, this->sdi_pin_); // NOLINT + this->clk_pin_->setup(); + this->clk_pin_->digital_write(true); + this->sdo_pin_->setup(); + this->sdi_pin_->setup(); } } + void SPIComponent::dump_config() { ESP_LOGCONFIG(TAG, "SPI bus:"); - LOG_PIN(" CLK Pin: ", this->clk_); - LOG_PIN(" MISO Pin: ", this->miso_); - LOG_PIN(" MOSI Pin: ", this->mosi_); -#ifdef USE_SPI_ARDUINO_BACKEND - ESP_LOGCONFIG(TAG, " Using HW SPI: %s", YESNO(this->hw_spi_ != nullptr)); -#endif // USE_SPI_ARDUINO_BACKEND -} -float SPIComponent::get_setup_priority() const { return setup_priority::BUS; } - -void SPIComponent::cycle_clock_(bool value) { - uint32_t start = arch_get_cpu_cycle_count(); - while (start - arch_get_cpu_cycle_count() < this->wait_cycle_) - ; - this->clk_->digital_write(value); - start += this->wait_cycle_; - while (start - arch_get_cpu_cycle_count() < this->wait_cycle_) - ; + LOG_PIN(" CLK Pin: ", this->clk_pin_) + LOG_PIN(" SDI Pin: ", this->sdi_pin_) + LOG_PIN(" SDO Pin: ", this->sdo_pin_) + if (this->spi_bus_->is_hw()) { + ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_); + } else { + ESP_LOGCONFIG(TAG, " Using software SPI"); + } } -// NOLINTNEXTLINE -#ifndef CLANG_TIDY -#pragma GCC optimize("unroll-loops") -// NOLINTNEXTLINE -#pragma GCC optimize("O2") -#endif // CLANG_TIDY +void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } -template -uint8_t HOT SPIComponent::transfer_(uint8_t data) { +uint8_t SPIDelegateBitBash::transfer(uint8_t data) { // Clock starts out at idle level - this->clk_->digital_write(CLOCK_POLARITY); + this->clk_pin_->digital_write(clock_polarity_); uint8_t out_data = 0; for (uint8_t i = 0; i < 8; i++) { uint8_t shift; - if (BIT_ORDER == BIT_ORDER_MSB_FIRST) { + if (bit_order_ == BIT_ORDER_MSB_FIRST) { shift = 7 - i; } else { shift = i; } - if (CLOCK_PHASE == CLOCK_PHASE_LEADING) { + if (clock_phase_ == CLOCK_PHASE_LEADING) { // sampling on leading edge - if (WRITE) { - this->mosi_->digital_write(data & (1 << shift)); - } - - // SAMPLE! - this->cycle_clock_(!CLOCK_POLARITY); - - if (READ) { - out_data |= uint8_t(this->miso_->digital_read()) << shift; - } - - this->cycle_clock_(CLOCK_POLARITY); + this->sdo_pin_->digital_write(data & (1 << shift)); + this->cycle_clock_(); + out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift; + this->clk_pin_->digital_write(!this->clock_polarity_); + this->cycle_clock_(); + this->clk_pin_->digital_write(this->clock_polarity_); } else { // sampling on trailing edge - this->cycle_clock_(!CLOCK_POLARITY); - - if (WRITE) { - this->mosi_->digital_write(data & (1 << shift)); - } - - // SAMPLE! - this->cycle_clock_(CLOCK_POLARITY); - - if (READ) { - out_data |= uint8_t(this->miso_->digital_read()) << shift; - } + this->cycle_clock_(); + this->clk_pin_->digital_write(!this->clock_polarity_); + this->sdo_pin_->digital_write(data & (1 << shift)); + this->cycle_clock_(); + out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift; + this->clk_pin_->digital_write(this->clock_polarity_); } } - App.feed_wdt(); - return out_data; } -// Generate with (py3): -// -// from itertools import product -// bit_orders = ['BIT_ORDER_LSB_FIRST', 'BIT_ORDER_MSB_FIRST'] -// clock_pols = ['CLOCK_POLARITY_LOW', 'CLOCK_POLARITY_HIGH'] -// clock_phases = ['CLOCK_PHASE_LEADING', 'CLOCK_PHASE_TRAILING'] -// reads = [False, True] -// writes = [False, True] -// cpp_bool = {False: 'false', True: 'true'} -// for b, cpol, cph, r, w in product(bit_orders, clock_pols, clock_phases, reads, writes): -// if not r and not w: -// continue -// print(f"template uint8_t SPIComponent::transfer_<{b}, {cpol}, {cph}, {cpp_bool[r]}, {cpp_bool[w]}>(uint8_t -// data);") - -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); -template uint8_t SPIComponent::transfer_( - uint8_t data); - } // namespace spi } // namespace esphome diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 159d117533..2761c2d604 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -2,16 +2,34 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" #include +#include #ifdef USE_ARDUINO -#define USE_SPI_ARDUINO_BACKEND -#endif -#ifdef USE_SPI_ARDUINO_BACKEND #include + +#ifdef USE_RP2040 +using SPIInterface = SPIClassRP2040 *; +#else +using SPIInterface = SPIClass *; #endif +#endif + +#ifdef USE_ESP_IDF + +#include "driver/spi_master.h" + +using SPIInterface = spi_host_device_t; + +#endif // USE_ESP_IDF + +/** + * Implementation of SPI Controller mode. + */ namespace esphome { namespace spi { @@ -48,10 +66,19 @@ enum SPIClockPhase { /// The data is sampled on a trailing clock edge. (CPHA=1) CLOCK_PHASE_TRAILING, }; -/** The SPI clock signal data rate. This defines for what duration the clock signal is HIGH/LOW. - * So effectively the rate of bytes can be calculated using + +/** + * Modes mapping to clock phase and polarity. * - * effective_byte_rate = spi_data_rate / 16 + */ + +enum SPIMode { + MODE0 = 0, + MODE1 = 1, + MODE2 = 2, + MODE3 = 3, +}; +/** The SPI clock signal frequency, which determines the transfer bit rate/second. * * Implementations can use the pre-defined constants here, or use an integer in the template definition * to manually use a specific data rate. @@ -71,270 +98,340 @@ enum SPIDataRate : uint32_t { DATA_RATE_80MHZ = 80000000, }; -class SPIComponent : public Component { +/** + * A pin to replace those that don't exist. + */ +class NullPin : public GPIOPin { + friend class SPIComponent; + + friend class SPIDelegate; + + friend class Utility; + public: - void set_clk(GPIOPin *clk) { clk_ = clk; } - void set_miso(GPIOPin *miso) { miso_ = miso; } - void set_mosi(GPIOPin *mosi) { mosi_ = mosi; } - void set_force_sw(bool force_sw) { force_sw_ = force_sw; } + void setup() override {} - void setup() override; + void pin_mode(gpio::Flags flags) override {} - void dump_config() override; + bool digital_read() override { return false; } - template uint8_t read_byte() { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - return this->hw_spi_->transfer(0x00); - } -#endif // USE_SPI_ARDUINO_BACKEND - return this->transfer_(0x00); - } + void digital_write(bool value) override {} - template - void read_array(uint8_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - this->hw_spi_->transfer(data, length); - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - for (size_t i = 0; i < length; i++) { - data[i] = this->read_byte(); - } - } - - template - void write_byte(uint8_t data) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { -#ifdef USE_RP2040 - this->hw_spi_->transfer(data); -#else - this->hw_spi_->write(data); -#endif - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - this->transfer_(data); - } - - template - void write_byte16(const uint16_t data) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { -#ifdef USE_RP2040 - this->hw_spi_->transfer16(data); -#else - this->hw_spi_->write16(data); -#endif - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - - this->write_byte(data >> 8); - this->write_byte(data); - } - - template - void write_array16(const uint16_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - for (size_t i = 0; i < length; i++) { -#ifdef USE_RP2040 - this->hw_spi_->transfer16(data[i]); -#else - this->hw_spi_->write16(data[i]); -#endif - } - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - for (size_t i = 0; i < length; i++) { - this->write_byte16(data[i]); - } - } - - template - void write_array(const uint8_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - auto *data_c = const_cast(data); -#ifdef USE_RP2040 - this->hw_spi_->transfer(data_c, length); -#else - this->hw_spi_->writeBytes(data_c, length); -#endif - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - for (size_t i = 0; i < length; i++) { - this->write_byte(data[i]); - } - } - - template - uint8_t transfer_byte(uint8_t data) { - if (this->miso_ != nullptr) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - return this->hw_spi_->transfer(data); - } else { -#endif // USE_SPI_ARDUINO_BACKEND - return this->transfer_(data); -#ifdef USE_SPI_ARDUINO_BACKEND - } -#endif // USE_SPI_ARDUINO_BACKEND - } - this->write_byte(data); - return 0; - } - - template - void transfer_array(uint8_t *data, size_t length) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - if (this->miso_ != nullptr) { - this->hw_spi_->transfer(data, length); - } else { -#ifdef USE_RP2040 - this->hw_spi_->transfer(data, length); -#else - this->hw_spi_->writeBytes(data, length); -#endif - } - return; - } -#endif // USE_SPI_ARDUINO_BACKEND - - if (this->miso_ != nullptr) { - for (size_t i = 0; i < length; i++) { - data[i] = this->transfer_byte(data[i]); - } - } else { - this->write_array(data, length); - } - } - - template - void enable(GPIOPin *cs) { -#ifdef USE_SPI_ARDUINO_BACKEND - if (this->hw_spi_ != nullptr) { - uint8_t data_mode = SPI_MODE0; - if (!CLOCK_POLARITY && CLOCK_PHASE) { - data_mode = SPI_MODE1; - } else if (CLOCK_POLARITY && !CLOCK_PHASE) { - data_mode = SPI_MODE2; - } else if (CLOCK_POLARITY && CLOCK_PHASE) { - data_mode = SPI_MODE3; - } -#ifdef USE_RP2040 - SPISettings settings(DATA_RATE, static_cast(BIT_ORDER), data_mode); -#else - SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); -#endif - this->hw_spi_->beginTransaction(settings); - } else { -#endif // USE_SPI_ARDUINO_BACKEND - this->clk_->digital_write(CLOCK_POLARITY); - uint32_t cpu_freq_hz = arch_get_cpu_freq_hz(); - this->wait_cycle_ = uint32_t(cpu_freq_hz) / DATA_RATE / 2ULL; -#ifdef USE_SPI_ARDUINO_BACKEND - } -#endif // USE_SPI_ARDUINO_BACKEND - - if (cs != nullptr) { - this->active_cs_ = cs; - this->active_cs_->digital_write(false); - } - } - - void disable(); - - float get_setup_priority() const override; + std::string dump_summary() const override { return std::string(); } protected: - inline void cycle_clock_(bool value); - - template - uint8_t transfer_(uint8_t data); - - GPIOPin *clk_; - GPIOPin *miso_{nullptr}; - GPIOPin *mosi_{nullptr}; - GPIOPin *active_cs_{nullptr}; - bool force_sw_{false}; -#ifdef USE_SPI_ARDUINO_BACKEND - SPIClass *hw_spi_{nullptr}; -#endif // USE_SPI_ARDUINO_BACKEND - uint32_t wait_cycle_; + static GPIOPin *const NULL_PIN; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + // https://bugs.llvm.org/show_bug.cgi?id=48040 }; -template -class SPIDevice { +class Utility { public: - SPIDevice() = default; - SPIDevice(SPIComponent *parent, GPIOPin *cs) : parent_(parent), cs_(cs) {} + static int get_pin_no(GPIOPin *pin) { + if (pin == nullptr || !pin->is_internal()) + return -1; + if (((InternalGPIOPin *) pin)->is_inverted()) + return -1; + return ((InternalGPIOPin *) pin)->get_pin(); + } - void set_spi_parent(SPIComponent *parent) { parent_ = parent; } - void set_cs_pin(GPIOPin *cs) { cs_ = cs; } + static SPIMode get_mode(SPIClockPolarity polarity, SPIClockPhase phase) { + if (polarity == CLOCK_POLARITY_HIGH) { + return phase == CLOCK_PHASE_LEADING ? MODE2 : MODE3; + } + return phase == CLOCK_PHASE_LEADING ? MODE0 : MODE1; + } - void spi_setup() { - if (this->cs_) { - this->cs_->setup(); - this->cs_->digital_write(true); + static SPIClockPhase get_phase(SPIMode mode) { + switch (mode) { + case MODE0: + case MODE2: + return CLOCK_PHASE_LEADING; + default: + return CLOCK_PHASE_TRAILING; } } - void enable() { this->parent_->template enable(this->cs_); } + static SPIClockPolarity get_polarity(SPIMode mode) { + switch (mode) { + case MODE0: + case MODE1: + return CLOCK_POLARITY_LOW; + default: + return CLOCK_POLARITY_HIGH; + } + } +}; - void disable() { this->parent_->disable(); } +class SPIDelegateDummy; - uint8_t read_byte() { return this->parent_->template read_byte(); } +// represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is +// a thin wrapper over SPIClass. +class SPIDelegate { + friend class SPIClient; - void read_array(uint8_t *data, size_t length) { - return this->parent_->template read_array(data, length); + public: + SPIDelegate() = default; + + SPIDelegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) + : bit_order_(bit_order), data_rate_(data_rate), mode_(mode), cs_pin_(cs_pin) { + if (this->cs_pin_ == nullptr) + this->cs_pin_ = NullPin::NULL_PIN; + this->cs_pin_->setup(); + this->cs_pin_->digital_write(true); } - template std::array read_array() { - std::array data; - this->read_array(data.data(), N); - return data; + virtual ~SPIDelegate(){}; + + // enable CS if configured. + virtual void begin_transaction() { this->cs_pin_->digital_write(false); } + + // end the transaction + virtual void end_transaction() { this->cs_pin_->digital_write(true); } + + // transfer one byte, return the byte that was read. + virtual uint8_t transfer(uint8_t data) = 0; + + // transfer a buffer, replace the contents with read data + virtual void transfer(uint8_t *ptr, size_t length) { this->transfer(ptr, ptr, length); } + + virtual void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) { + for (size_t i = 0; i != length; i++) + rxbuf[i] = this->transfer(txbuf[i]); } - void write_byte(uint8_t data) { - return this->parent_->template write_byte(data); + // write 16 bits + virtual void write16(uint16_t data) { + if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { + uint16_t buffer; + buffer = (data >> 8) | (data << 8); + this->write_array(reinterpret_cast(&buffer), 2); + } else { + this->write_array(reinterpret_cast(&data), 2); + } } - void write_byte16(uint16_t data) { - return this->parent_->template write_byte16(data); + virtual void write_array16(const uint16_t *data, size_t length) { + for (size_t i = 0; i != length; i++) { + this->write16(data[i]); + } } - void write_array16(const uint16_t *data, size_t length) { - this->parent_->template write_array16(data, length); + // write the contents of a buffer, ignore read data (buffer is unchanged.) + virtual void write_array(const uint8_t *ptr, size_t length) { + for (size_t i = 0; i != length; i++) + this->transfer(ptr[i]); } - void write_array(const uint8_t *data, size_t length) { - this->parent_->template write_array(data, length); + // read into a buffer, write nulls + virtual void read_array(uint8_t *ptr, size_t length) { + for (size_t i = 0; i != length; i++) + ptr[i] = this->transfer(0); } + // check if device is ready + virtual bool is_ready(); + + protected: + SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; + uint32_t data_rate_{1000000}; + SPIMode mode_{MODE0}; + GPIOPin *cs_pin_{NullPin::NULL_PIN}; + static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +}; + +/** + * A dummy SPIDelegate that complains if it's used. + */ + +class SPIDelegateDummy : public SPIDelegate { + public: + SPIDelegateDummy() = default; + + uint8_t transfer(uint8_t data) override { return 0; } + + void begin_transaction() override; +}; + +/** + * An implementation of SPI that relies only on software toggling of pins. + * + */ +class SPIDelegateBitBash : public SPIDelegate { + public: + SPIDelegateBitBash(uint32_t clock, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, GPIOPin *clk_pin, + GPIOPin *sdo_pin, GPIOPin *sdi_pin) + : SPIDelegate(clock, bit_order, mode, cs_pin), clk_pin_(clk_pin), sdo_pin_(sdo_pin), sdi_pin_(sdi_pin) { + // this calculation is pretty meaningless except at very low bit rates. + this->wait_cycle_ = uint32_t(arch_get_cpu_freq_hz()) / this->data_rate_ / 2ULL; + this->clock_polarity_ = Utility::get_polarity(this->mode_); + this->clock_phase_ = Utility::get_phase(this->mode_); + } + + uint8_t transfer(uint8_t data) override; + + protected: + GPIOPin *clk_pin_; + GPIOPin *sdo_pin_; + GPIOPin *sdi_pin_; + uint32_t last_transition_{0}; + uint32_t wait_cycle_; + SPIClockPolarity clock_polarity_; + SPIClockPhase clock_phase_; + + void HOT cycle_clock_() { + while (this->last_transition_ - arch_get_cpu_cycle_count() < this->wait_cycle_) + continue; + this->last_transition_ += this->wait_cycle_; + } +}; + +class SPIBus { + public: + SPIBus() = default; + + SPIBus(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) : clk_pin_(clk), sdo_pin_(sdo), sdi_pin_(sdi) {} + + virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) { + return new SPIDelegateBitBash(data_rate, bit_order, mode, cs_pin, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); + } + + virtual bool is_hw() { return false; } + + protected: + GPIOPin *clk_pin_{}; + GPIOPin *sdo_pin_{}; + GPIOPin *sdi_pin_{}; +}; + +class SPIClient; + +class SPIComponent : public Component { + public: + SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, + GPIOPin *cs_pin); + void unregister_device(SPIClient *device); + + void set_clk(GPIOPin *clk) { this->clk_pin_ = clk; } + + void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; } + + void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; } + + void set_interface(SPIInterface interface) { + this->interface_ = interface; + this->using_hw_ = true; + } + + void set_interface_name(const char *name) { this->interface_name_ = name; } + + float get_setup_priority() const override { return setup_priority::BUS; } + + void setup() override; + void dump_config() override; + + protected: + GPIOPin *clk_pin_{nullptr}; + GPIOPin *sdi_pin_{nullptr}; + GPIOPin *sdo_pin_{nullptr}; + SPIInterface interface_{}; + bool using_hw_{false}; + const char *interface_name_{nullptr}; + SPIBus *spi_bus_{}; + std::map devices_; + + static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi); +}; + +/** + * Base class for SPIDevice, un-templated. + */ +class SPIClient { + public: + SPIClient(SPIBitOrder bit_order, SPIMode mode, uint32_t data_rate) + : bit_order_(bit_order), mode_(mode), data_rate_(data_rate) {} + + virtual void spi_setup() { + this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_); + } + + virtual void spi_teardown() { + this->parent_->unregister_device(this); + this->delegate_ = SPIDelegate::NULL_DELEGATE; + } + + bool spi_is_ready() { return this->delegate_->is_ready(); } + + protected: + SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; + SPIMode mode_{MODE0}; + uint32_t data_rate_{1000000}; + SPIComponent *parent_{nullptr}; + GPIOPin *cs_{nullptr}; + SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; +}; + +/** + * The SPIDevice is what components using the SPI will create. + * + * @tparam BIT_ORDER + * @tparam CLOCK_POLARITY + * @tparam CLOCK_PHASE + * @tparam DATA_RATE + */ +template +class SPIDevice : public SPIClient { + public: + SPIDevice() : SPIClient(BIT_ORDER, Utility::get_mode(CLOCK_POLARITY, CLOCK_PHASE), DATA_RATE) {} + + SPIDevice(SPIComponent *parent, GPIOPin *cs_pin) { + this->set_spi_parent(parent); + this->set_cs_pin(cs_pin); + } + + void spi_setup() override { SPIClient::spi_setup(); } + + void spi_teardown() override { SPIClient::spi_teardown(); } + + void set_spi_parent(SPIComponent *parent) { this->parent_ = parent; } + + void set_cs_pin(GPIOPin *cs) { this->cs_ = cs; } + + void set_data_rate(uint32_t data_rate) { this->data_rate_ = data_rate; } + + void set_bit_order(SPIBitOrder order) { + this->bit_order_ = order; + esph_log_d("spi.h", "bit order set to %d", order); + } + + void set_mode(SPIMode mode) { this->mode_ = mode; } + + uint8_t read_byte() { return this->delegate_->transfer(0); } + + void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + + void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); } + + void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); } + + uint8_t transfer_byte(uint8_t data) { return this->delegate_->transfer(data); } + + // the driver will byte-swap if required. + void write_byte16(uint16_t data) { this->delegate_->write16(data); } + + // avoid use of this if possible. It's inefficient and ugly. + void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); } + + void enable() { this->delegate_->begin_transaction(); } + + void disable() { this->delegate_->end_transaction(); } + + void write_array(const uint8_t *data, size_t length) { this->delegate_->write_array(data, length); } + template void write_array(const std::array &data) { this->write_array(data.data(), N); } void write_array(const std::vector &data) { this->write_array(data.data(), data.size()); } - uint8_t transfer_byte(uint8_t data) { - return this->parent_->template transfer_byte(data); - } - - void transfer_array(uint8_t *data, size_t length) { - this->parent_->template transfer_array(data, length); - } - template void transfer_array(std::array &data) { this->transfer_array(data.data(), N); } - - protected: - SPIComponent *parent_{nullptr}; - GPIOPin *cs_{nullptr}; }; } // namespace spi diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp new file mode 100644 index 0000000000..40ed9e6062 --- /dev/null +++ b/esphome/components/spi/spi_arduino.cpp @@ -0,0 +1,89 @@ +#include "spi.h" +#include + +namespace esphome { +namespace spi { + +#ifdef USE_ARDUINO + +static const char *const TAG = "spi-esp-arduino"; +class SPIDelegateHw : public SPIDelegate { + public: + SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel) {} + + void begin_transaction() override { +#ifdef USE_RP2040 + SPISettings const settings(this->data_rate_, static_cast(this->bit_order_), this->mode_); +#else + SPISettings const settings(this->data_rate_, this->bit_order_, this->mode_); +#endif + this->channel_->beginTransaction(settings); + SPIDelegate::begin_transaction(); + } + + void transfer(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } + + void end_transaction() override { + this->channel_->endTransaction(); + SPIDelegate::end_transaction(); + } + + uint8_t transfer(uint8_t data) override { return this->channel_->transfer(data); } + + void write16(uint16_t data) override { this->channel_->transfer16(data); } + +#ifdef USE_RP2040 + void write_array(const uint8_t *ptr, size_t length) override { + // avoid overwriting the supplied buffer + uint8_t *rxbuf = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory) + memcpy(rxbuf, ptr, length); + this->channel_->transfer((void *) rxbuf, length); + delete[] rxbuf; // NOLINT(cppcoreguidelines-owning-memory) + } +#else + void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); } +#endif + + void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } + + protected: + SPIInterface channel_{}; +}; + +class SPIBusHw : public SPIBus { + public: + SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) { +#ifdef USE_ESP8266 + channel->pins(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1); + channel->begin(); +#endif // USE_ESP8266 +#ifdef USE_ESP32 + channel->begin(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1); +#endif +#ifdef USE_RP2040 + if (Utility::get_pin_no(sdi) != -1) + channel->setRX(Utility::get_pin_no(sdi)); + if (Utility::get_pin_no(sdo) != -1) + channel->setTX(Utility::get_pin_no(sdo)); + channel->setSCK(Utility::get_pin_no(clk)); + channel->begin(); +#endif + } + + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin); + } + + protected: + SPIInterface channel_{}; + bool is_hw() override { return true; } +}; + +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) { + return new SPIBusHw(clk, sdo, sdi, interface); +} + +#endif // USE_ARDUINO +} // namespace spi +} // namespace esphome diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp new file mode 100644 index 0000000000..f9e4bfcca6 --- /dev/null +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -0,0 +1,163 @@ +#include "spi.h" +#include + +namespace esphome { +namespace spi { + +#ifdef USE_ESP_IDF +static const char *const TAG = "spi-esp-idf"; +static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. + +class SPIDelegateHw : public SPIDelegate { + public: + SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool write_only) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel), write_only_(write_only) { + spi_device_interface_config_t config = {}; + config.mode = static_cast(mode); + config.clock_speed_hz = static_cast(data_rate); + config.spics_io_num = -1; + config.flags = 0; + config.queue_size = 1; + config.pre_cb = nullptr; + config.post_cb = nullptr; + if (bit_order == BIT_ORDER_LSB_FIRST) + config.flags |= SPI_DEVICE_BIT_LSBFIRST; + if (write_only) + config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; + esp_err_t const err = spi_bus_add_device(channel, &config, &this->handle_); + if (err != ESP_OK) + ESP_LOGE(TAG, "Add device failed - err %X", err); + } + + bool is_ready() override { return this->handle_ != nullptr; } + + void begin_transaction() override { + if (this->is_ready()) { + if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) + ESP_LOGE(TAG, "Failed to acquire SPI bus"); + SPIDelegate::begin_transaction(); + } else { + ESP_LOGW(TAG, "spi_setup called before initialisation"); + } + } + + void end_transaction() override { + if (this->is_ready()) { + SPIDelegate::end_transaction(); + spi_device_release_bus(this->handle_); + } + } + + ~SPIDelegateHw() override { + esp_err_t const err = spi_bus_remove_device(this->handle_); + if (err != ESP_OK) + ESP_LOGE(TAG, "Remove device failed - err %X", err); + } + + // do a transfer. either txbuf or rxbuf (but not both) may be null. + // transfers above the maximum size will be split. + // TODO - make use of the queue for interrupt transfers to provide a (short) pipeline of blocks + // when splitting is required. + void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) override { + if (rxbuf != nullptr && this->write_only_) { + ESP_LOGE(TAG, "Attempted read from write-only channel"); + return; + } + spi_transaction_t desc = {}; + desc.flags = 0; + while (length != 0) { + size_t const partial = std::min(length, MAX_TRANSFER_SIZE); + desc.length = partial * 8; + desc.rxlength = this->write_only_ ? 0 : partial * 8; + desc.tx_buffer = txbuf; + desc.rx_buffer = rxbuf; + esp_err_t const err = spi_device_transmit(this->handle_, &desc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + break; + } + length -= partial; + if (txbuf != nullptr) + txbuf += partial; + if (rxbuf != nullptr) + rxbuf += partial; + } + } + + void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); } + + uint8_t transfer(uint8_t data) override { + uint8_t rxbuf; + this->transfer(&data, &rxbuf, 1); + return rxbuf; + } + + void write16(uint16_t data) override { + if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { + uint16_t txbuf = SPI_SWAP_DATA_TX(data, 16); + this->transfer((uint8_t *) &txbuf, nullptr, 2); + } else { + this->transfer((uint8_t *) &data, nullptr, 2); + } + } + + void write_array(const uint8_t *ptr, size_t length) override { this->transfer(ptr, nullptr, length); } + + void write_array16(const uint16_t *data, size_t length) override { + if (this->bit_order_ == BIT_ORDER_LSB_FIRST) { + this->write_array((uint8_t *) data, length * 2); + } else { + uint16_t buffer[MAX_TRANSFER_SIZE / 2]; + while (length != 0) { + size_t const partial = std::min(length, MAX_TRANSFER_SIZE / 2); + for (size_t i = 0; i != partial; i++) { + buffer[i] = SPI_SWAP_DATA_TX(*data++, 16); + } + this->write_array((const uint8_t *) buffer, partial * 2); + length -= partial; + } + } + } + + void read_array(uint8_t *ptr, size_t length) override { this->transfer(nullptr, ptr, length); } + + protected: + SPIInterface channel_{}; + spi_device_handle_t handle_{}; + bool write_only_{false}; +}; + +class SPIBusHw : public SPIBus { + public: + SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = Utility::get_pin_no(sdo); + buscfg.miso_io_num = Utility::get_pin_no(sdi); + buscfg.sclk_io_num = Utility::get_pin_no(clk); + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + buscfg.max_transfer_sz = MAX_TRANSFER_SIZE; + auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO); + if (err != ESP_OK) + ESP_LOGE(TAG, "Bus init failed - err %X", err); + } + + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, + Utility::get_pin_no(this->sdi_pin_) == -1); + } + + protected: + SPIInterface channel_{}; + + bool is_hw() override { return true; } +}; + +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) { + return new SPIBusHw(clk, sdo, sdi, interface); +} + +#endif +} // namespace spi +} // namespace esphome diff --git a/esphome/components/spi_device/__init__.py b/esphome/components/spi_device/__init__.py new file mode 100644 index 0000000000..428b5bfbda --- /dev/null +++ b/esphome/components/spi_device/__init__.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID, CONF_DATA_RATE, CONF_MODE + +DEPENDENCIES = ["spi"] +CODEOWNERS = ["@clydebarrow"] + +MULTI_CONF = True +spi_device_ns = cg.esphome_ns.namespace("spi_device") + +spi_device = spi_device_ns.class_("SPIDeviceComponent", cg.Component, spi.SPIDevice) + +Mode = spi.spi_ns.enum("SPIMode") +MODES = { + "0": Mode.MODE0, + "1": Mode.MODE1, + "2": Mode.MODE2, + "3": Mode.MODE3, + "MODE0": Mode.MODE0, + "MODE1": Mode.MODE1, + "MODE2": Mode.MODE2, + "MODE3": Mode.MODE3, +} + +BitOrder = spi.spi_ns.enum("SPIBitOrder") +ORDERS = { + "msb_first": BitOrder.BIT_ORDER_MSB_FIRST, + "lsb_first": BitOrder.BIT_ORDER_LSB_FIRST, +} +CONF_BIT_ORDER = "bit_order" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(spi_device), + cv.Optional(CONF_DATA_RATE, default="1MHz"): spi.SPI_DATA_RATE_SCHEMA, + cv.Optional(CONF_BIT_ORDER, default="msb_first"): cv.enum(ORDERS, lower=True), + cv.Optional(CONF_MODE, default="0"): cv.enum(MODES, upper=True), + } +).extend(spi.spi_device_schema(False)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_data_rate(config[CONF_DATA_RATE])) + cg.add(var.set_mode(config[CONF_MODE])) + cg.add(var.set_bit_order(config[CONF_BIT_ORDER])) + await spi.register_spi_device(var, config) diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp new file mode 100644 index 0000000000..4e0b72ae60 --- /dev/null +++ b/esphome/components/spi_device/spi_device.cpp @@ -0,0 +1,30 @@ +#include "spi_device.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace spi_device { + +static const char *const TAG = "spi_device"; + +void SPIDeviceComponent::setup() { + ESP_LOGD(TAG, "Setting up SPIDevice..."); + this->spi_setup(); + ESP_LOGCONFIG(TAG, "SPIDevice started!"); +} + +void SPIDeviceComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SPIDevice"); + LOG_PIN(" CS pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Mode: %d", this->mode_); + if (this->data_rate_ < 1000000) { + ESP_LOGCONFIG(TAG, " Data rate: %dkHz", this->data_rate_ / 1000); + } else { + ESP_LOGCONFIG(TAG, " Data rate: %dMHz", this->data_rate_ / 1000000); + } +} + +float SPIDeviceComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace spi_device +} // namespace esphome diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h new file mode 100644 index 0000000000..d8aef440a7 --- /dev/null +++ b/esphome/components/spi_device/spi_device.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace spi_device { + +class SPIDeviceComponent : public Component, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: +}; + +} // namespace spi_device +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 1175bb207c..341e613785 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -32,6 +32,7 @@ spi: clk_pin: GPIO21 mosi_pin: GPIO22 miso_pin: GPIO23 + interface: hardware uart: - id: uart115200 diff --git a/tests/test8.yaml b/tests/test8.yaml index 28c6e78b87..498da94483 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -31,8 +31,17 @@ light: restore_mode: ALWAYS_OFF spi: + id: spi_id_1 clk_pin: GPIO7 mosi_pin: GPIO6 + interface: any + +spi_device: + id: spidev + data_rate: 2MHz + spi_id: spi_id_1 + mode: 3 + bit_order: lsb_first display: - platform: ili9xxx