Refactor SPI code; Add ESP-IDF hardware support (#5311)

* Checkpoint

* Checkpoint

* Checkpoint

* Revert hal change

* Checkpoint

* Checkpoint

* Checkpoint

* Checkpoint

* ESP-IDF working

* clang-format

* use bus_list

* Add spi_device; fix 16 bit transfer.

* Enable multi_conf;
Fix LSB 16 bit transactions

* Formatting fixes

* Clang-format, codeowners

* Add test

* Formatting

* clang tidy

* clang-format

* clang-tidy

* clang-format

* Checkpoint

* Checkpoint

* Checkpoint

* Revert hal change

* Checkpoint

* Checkpoint

* Checkpoint

* Checkpoint

* ESP-IDF working

* clang-format

* use bus_list

* Add spi_device; fix 16 bit transfer.

* Enable multi_conf;
Fix LSB 16 bit transactions

* Formatting fixes

* Clang-format, codeowners

* Add test

* Formatting

* clang tidy

* clang-format

* clang-tidy

* clang-format

* Clang-tidy

* Clang-format

* clang-tidy

* clang-tidy

* Fix ESP8266

* RP2040

* RP2040

* Avoid use of spi1 as id

* Refactor SPI code.
Add support for ESP-IDF hardware SPI

* Force SW only for RP2040

* Break up large transfers

* Add interface: option for spi.
validate pins in python.

* Can't use match/case with Python 3.9.
Check for inverted pins.

* Work around target_platform issue with

* Remove debug code

* Optimize write_array16

* Show errors in hex

* Only one spi on ESP32Cx variants

* Ensure bus is claimed before asserting /CS.

* Check on init/deinit

* Allow maximum rate write only SPI on GPIO MUXed pins.

* Clang-format

* Clang-tidy

* Fix issue with reads.

* Finger trouble...

* Make comment about missing SPI on Cx variants

* Pacify CI clang-format. Did not complain locally??

* Restore 8266 to its former SPI glory

* Fix per clang-format

* Move validation and choice of SPI into Python code.

* Add test for interface: config

* Fix issues found on self-review.

---------

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Clyde Stubbs 2023-09-08 17:27:19 +10:00 committed by GitHub
parent ce171f5c00
commit 5c26f95a4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 954 additions and 473 deletions

View File

@ -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

View File

@ -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)

View File

@ -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<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE>
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_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
} // namespace spi
} // namespace esphome

View File

@ -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 <vector>
#include <map>
#ifdef USE_ARDUINO
#define USE_SPI_ARDUINO_BACKEND
#endif
#ifdef USE_SPI_ARDUINO_BACKEND
#include <SPI.h>
#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<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> 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_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, false>(0x00);
}
void digital_write(bool value) override {}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>();
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, false, true>(data);
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data >> 8);
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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<uint8_t *>(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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data);
#ifdef USE_SPI_ARDUINO_BACKEND
}
#endif // USE_SPI_ARDUINO_BACKEND
}
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
return 0;
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
}
} else {
this->write_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, uint32_t DATA_RATE>
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<BitOrder>(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<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE>
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<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, SPIDataRate DATA_RATE>
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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, DATA_RATE>(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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(); }
// 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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(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<size_t N> std::array<uint8_t, N> read_array() {
std::array<uint8_t, N> 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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(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<const uint8_t *>(&buffer), 2);
} else {
this->write_array(reinterpret_cast<const uint8_t *>(&data), 2);
}
}
void write_byte16(uint16_t data) {
return this->parent_->template write_byte16<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(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<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(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<SPIClient *, SPIDelegate *> 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<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, SPIDataRate DATA_RATE>
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<size_t N> void write_array(const std::array<uint8_t, N> &data) { this->write_array(data.data(), N); }
void write_array(const std::vector<uint8_t> &data) { this->write_array(data.data(), data.size()); }
uint8_t transfer_byte(uint8_t data) {
return this->parent_->template transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
}
void transfer_array(uint8_t *data, size_t length) {
this->parent_->template transfer_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
}
template<size_t N> void transfer_array(std::array<uint8_t, N> &data) { this->transfer_array(data.data(), N); }
protected:
SPIComponent *parent_{nullptr};
GPIOPin *cs_{nullptr};
};
} // namespace spi

View File

@ -0,0 +1,89 @@
#include "spi.h"
#include <vector>
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<BitOrder>(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

View File

@ -0,0 +1,163 @@
#include "spi.h"
#include <vector>
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<uint8_t>(mode);
config.clock_speed_hz = static_cast<int>(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

View File

@ -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)

View File

@ -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

View File

@ -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<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
};
} // namespace spi_device
} // namespace esphome

View File

@ -32,6 +32,7 @@ spi:
clk_pin: GPIO21
mosi_pin: GPIO22
miso_pin: GPIO23
interface: hardware
uart:
- id: uart115200

View File

@ -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