mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
Add quad spi features (#5925)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
2283b3b443
commit
1fef769496
6 changed files with 207 additions and 40 deletions
|
@ -29,12 +29,15 @@ from esphome.const import (
|
|||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
CONF_ALLOW_OTHER_USES,
|
||||
CONF_DATA_PINS,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority, CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
||||
spi_ns = cg.esphome_ns.namespace("spi")
|
||||
SPIComponent = spi_ns.class_("SPIComponent", cg.Component)
|
||||
QuadSPIComponent = spi_ns.class_("QuadSPIComponent", cg.Component)
|
||||
SPIDevice = spi_ns.class_("SPIDevice")
|
||||
SPIDataRate = spi_ns.enum("SPIDataRate")
|
||||
SPIMode = spi_ns.enum("SPIMode")
|
||||
|
@ -190,12 +193,9 @@ def get_hw_spi(config, available):
|
|||
def validate_spi_config(config):
|
||||
available = list(range(len(get_hw_interface_list())))
|
||||
for spi in config:
|
||||
# map pin number to schema
|
||||
spi[CONF_CLK_PIN] = pins.gpio_output_pin_schema(spi[CONF_CLK_PIN])
|
||||
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":
|
||||
|
@ -229,6 +229,8 @@ def validate_spi_config(config):
|
|||
spi, spi[CONF_INTERFACE_INDEX]
|
||||
):
|
||||
raise cv.Invalid("Invalid pin selections for hardware SPI interface")
|
||||
if CONF_DATA_PINS in spi and CONF_INTERFACE_INDEX not in spi:
|
||||
raise cv.Invalid("Quad mode requires a hardware interface")
|
||||
|
||||
return config
|
||||
|
||||
|
@ -249,14 +251,26 @@ def get_spi_interface(index):
|
|||
return "new SPIClass(HSPI)"
|
||||
|
||||
|
||||
# Do not use a pin schema for the number, as that will trigger a pin reuse error due to duplication of the
|
||||
# clock pin in the standard and quad schemas.
|
||||
clk_pin_validator = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_NUMBER): cv.Any(cv.int_, cv.string),
|
||||
cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean,
|
||||
},
|
||||
key=CONF_NUMBER,
|
||||
)
|
||||
|
||||
SPI_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SPIComponent),
|
||||
cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_CLK_PIN): clk_pin_validator,
|
||||
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_FORCE_SW): cv.invalid(
|
||||
"force_sw is deprecated - use interface: software"
|
||||
),
|
||||
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of(
|
||||
*sum(get_hw_interface_list(), ["software", "hardware", "any"]),
|
||||
lower=True,
|
||||
|
@ -267,8 +281,34 @@ SPI_SCHEMA = cv.All(
|
|||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
|
||||
)
|
||||
|
||||
SPI_QUAD_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(QuadSPIComponent),
|
||||
cv.Required(CONF_CLK_PIN): clk_pin_validator,
|
||||
cv.Required(CONF_DATA_PINS): cv.All(
|
||||
cv.ensure_list(pins.internal_gpio_output_pin_number),
|
||||
cv.Length(min=4, max=4),
|
||||
),
|
||||
cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of(
|
||||
*sum(get_hw_interface_list(), ["hardware"]),
|
||||
lower=True,
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.only_with_esp_idf,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.ensure_list(SPI_SCHEMA),
|
||||
# Order is important. SPI_SCHEMA is the default.
|
||||
cv.ensure_list(
|
||||
cv.Any(
|
||||
SPI_SCHEMA,
|
||||
SPI_QUAD_SCHEMA,
|
||||
msg="Standard SPI requires mosi_pin and/or miso_pin; quad SPI requires data_pins only."
|
||||
+ " A clock pin is always required",
|
||||
),
|
||||
),
|
||||
validate_spi_config,
|
||||
)
|
||||
|
||||
|
@ -277,43 +317,46 @@ CONFIG_SCHEMA = cv.All(
|
|||
async def to_code(configs):
|
||||
cg.add_define("USE_SPI")
|
||||
cg.add_global(spi_ns.using)
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("SPI", None)
|
||||
for spi in configs:
|
||||
var = cg.new_Pvariable(spi[CONF_ID])
|
||||
await cg.register_component(var, spi)
|
||||
|
||||
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))))
|
||||
if miso := spi.get(CONF_MISO_PIN):
|
||||
cg.add(var.set_miso(await cg.gpio_pin_expression(miso)))
|
||||
if mosi := spi.get(CONF_MOSI_PIN):
|
||||
cg.add(var.set_mosi(await cg.gpio_pin_expression(mosi)))
|
||||
if data_pins := spi.get(CONF_DATA_PINS):
|
||||
cg.add(var.set_data_pins(data_pins))
|
||||
if (index := spi.get(CONF_INTERFACE_INDEX)) is not None:
|
||||
interface = get_spi_interface(index)
|
||||
cg.add(var.set_interface(cg.RawExpression(interface)))
|
||||
cg.add(
|
||||
var.set_interface_name(
|
||||
re.sub(
|
||||
r"\W", "", get_spi_interface(index).replace("new SPIClass", "")
|
||||
)
|
||||
re.sub(r"\W", "", interface.replace("new SPIClass", ""))
|
||||
)
|
||||
)
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("SPI", None)
|
||||
|
||||
|
||||
def spi_device_schema(
|
||||
cs_pin_required=True, default_data_rate=cv.UNDEFINED, default_mode=cv.UNDEFINED
|
||||
cs_pin_required=True,
|
||||
default_data_rate=cv.UNDEFINED,
|
||||
default_mode=cv.UNDEFINED,
|
||||
quad=False,
|
||||
):
|
||||
"""Create a schema for an SPI device.
|
||||
:param cs_pin_required: If true, make the CS_PIN required in the config.
|
||||
:param default_data_rate: Optional data_rate to use as default
|
||||
:param default_mode Optional. The default SPI mode to use.
|
||||
:param quad If set, will require an SPI component configured as quad data bits.
|
||||
:return: The SPI device schema, `extend` this in your config schema.
|
||||
"""
|
||||
schema = {
|
||||
cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent),
|
||||
cv.GenerateID(CONF_SPI_ID): cv.use_id(
|
||||
QuadSPIComponent if quad else SPIComponent
|
||||
),
|
||||
cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA,
|
||||
cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum(
|
||||
SPI_MODE_OPTIONS, upper=True
|
||||
|
|
|
@ -49,7 +49,8 @@ void SPIComponent::setup() {
|
|||
}
|
||||
|
||||
if (this->using_hw_) {
|
||||
this->spi_bus_ = SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_);
|
||||
this->spi_bus_ =
|
||||
SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_, this->data_pins_);
|
||||
if (this->spi_bus_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Unable to allocate SPI interface");
|
||||
this->mark_failed();
|
||||
|
@ -68,6 +69,9 @@ void SPIComponent::dump_config() {
|
|||
LOG_PIN(" CLK Pin: ", this->clk_pin_)
|
||||
LOG_PIN(" SDI Pin: ", this->sdi_pin_)
|
||||
LOG_PIN(" SDO Pin: ", this->sdo_pin_)
|
||||
for (size_t i = 0; i != this->data_pins_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]);
|
||||
}
|
||||
if (this->spi_bus_->is_hw()) {
|
||||
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_);
|
||||
} else {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
|
@ -208,6 +209,10 @@ class SPIDelegate {
|
|||
esph_log_e("spi_device", "variable length write not implemented");
|
||||
}
|
||||
|
||||
virtual void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address,
|
||||
const uint8_t *data, size_t length, uint8_t bus_width) {
|
||||
esph_log_e("spi_device", "write_cmd_addr_data not implemented");
|
||||
}
|
||||
// write 16 bits
|
||||
virtual void write16(uint16_t data) {
|
||||
if (this->bit_order_ == BIT_ORDER_MSB_FIRST) {
|
||||
|
@ -331,6 +336,7 @@ class SPIComponent : public Component {
|
|||
void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; }
|
||||
|
||||
void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; }
|
||||
void set_data_pins(std::vector<uint8_t> pins) { this->data_pins_ = std::move(pins); }
|
||||
|
||||
void set_interface(SPIInterface interface) {
|
||||
this->interface_ = interface;
|
||||
|
@ -348,15 +354,19 @@ class SPIComponent : public Component {
|
|||
GPIOPin *clk_pin_{nullptr};
|
||||
GPIOPin *sdi_pin_{nullptr};
|
||||
GPIOPin *sdo_pin_{nullptr};
|
||||
std::vector<uint8_t> data_pins_{};
|
||||
|
||||
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);
|
||||
static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins);
|
||||
};
|
||||
|
||||
using QuadSPIComponent = SPIComponent;
|
||||
/**
|
||||
* Base class for SPIDevice, un-templated.
|
||||
*/
|
||||
|
@ -422,18 +432,49 @@ class SPIDevice : public SPIClient {
|
|||
|
||||
void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); }
|
||||
|
||||
/**
|
||||
* Write a single data item, up to 32 bits.
|
||||
* @param data The data
|
||||
* @param num_bits The number of bits to write. The lower num_bits of data will be sent.
|
||||
*/
|
||||
void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); };
|
||||
|
||||
/* Write command, address and data. Command and address will be written as single-bit SPI,
|
||||
* data phase can be multiple bit (currently only 1 or 4)
|
||||
* @param cmd_bits Number of bits to write in the command phase
|
||||
* @param cmd The command value to write
|
||||
* @param addr_bits Number of bits to write in addr phase
|
||||
* @param address Address data
|
||||
* @param data Plain data bytes
|
||||
* @param length Number of data bytes
|
||||
* @param bus_width The number of data lines to use for the data phase.
|
||||
*/
|
||||
void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data,
|
||||
size_t length, uint8_t bus_width = 1) {
|
||||
this->delegate_->write_cmd_addr_data(cmd_bits, cmd, addr_bits, address, data, length, bus_width);
|
||||
}
|
||||
|
||||
void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); }
|
||||
|
||||
/**
|
||||
* Write the array data, replace with received data.
|
||||
* @param data
|
||||
* @param length
|
||||
*/
|
||||
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.
|
||||
/** Write 16 bit 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.
|
||||
/**
|
||||
* Write an array of data as 16 bit values, byte-swapping if required. Use of this should be avoided as
|
||||
* it is horribly slow.
|
||||
* @param data
|
||||
* @param length
|
||||
*/
|
||||
void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); }
|
||||
|
||||
void enable() { this->delegate_->begin_transaction(); }
|
||||
|
|
|
@ -85,7 +85,8 @@ class SPIBusHw : public SPIBus {
|
|||
bool is_hw() override { return true; }
|
||||
};
|
||||
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) {
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return new SPIBusHw(clk, sdo, sdi, interface);
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,60 @@ class SPIDelegateHw : public SPIDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write command, address and data
|
||||
* @param cmd_bits Number of bits to write in the command phase
|
||||
* @param cmd The command value to write
|
||||
* @param addr_bits Number of bits to write in addr phase
|
||||
* @param address Address data
|
||||
* @param data Remaining data bytes
|
||||
* @param length Number of data bytes
|
||||
* @param bus_width The number of data lines to use
|
||||
*/
|
||||
void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data,
|
||||
size_t length, uint8_t bus_width) override {
|
||||
spi_transaction_ext_t desc = {};
|
||||
if (length == 0 && cmd_bits == 0 && addr_bits == 0) {
|
||||
esph_log_w(TAG, "Nothing to transfer");
|
||||
return;
|
||||
}
|
||||
desc.base.flags = SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_DUMMY;
|
||||
if (bus_width == 4) {
|
||||
desc.base.flags |= SPI_TRANS_MODE_QIO;
|
||||
} else if (bus_width == 8) {
|
||||
desc.base.flags |= SPI_TRANS_MODE_OCT;
|
||||
}
|
||||
desc.command_bits = cmd_bits;
|
||||
desc.address_bits = addr_bits;
|
||||
desc.dummy_bits = 0;
|
||||
desc.base.rxlength = 0;
|
||||
desc.base.cmd = cmd;
|
||||
desc.base.addr = address;
|
||||
do {
|
||||
size_t chunk_size = std::min(length, MAX_TRANSFER_SIZE);
|
||||
if (data != nullptr && chunk_size != 0) {
|
||||
desc.base.length = chunk_size * 8;
|
||||
desc.base.tx_buffer = data;
|
||||
length -= chunk_size;
|
||||
data += chunk_size;
|
||||
} else {
|
||||
length = 0;
|
||||
desc.base.length = 0;
|
||||
}
|
||||
esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY);
|
||||
if (err == ESP_OK) {
|
||||
err = spi_device_polling_end(this->handle_, portMAX_DELAY);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Transmit failed - err %X", err);
|
||||
return;
|
||||
}
|
||||
// if more data is to be sent, skip the command and address phases.
|
||||
desc.command_bits = 0;
|
||||
desc.address_bits = 0;
|
||||
} while (length != 0);
|
||||
}
|
||||
|
||||
void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); }
|
||||
|
||||
uint8_t transfer(uint8_t data) override {
|
||||
|
@ -142,13 +196,27 @@ class SPIDelegateHw : public SPIDelegate {
|
|||
|
||||
class SPIBusHw : public SPIBus {
|
||||
public:
|
||||
SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) {
|
||||
SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel, std::vector<uint8_t> data_pins)
|
||||
: 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.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK;
|
||||
if (data_pins.empty()) {
|
||||
buscfg.mosi_io_num = Utility::get_pin_no(sdo);
|
||||
buscfg.miso_io_num = Utility::get_pin_no(sdi);
|
||||
buscfg.quadwp_io_num = -1;
|
||||
buscfg.quadhd_io_num = -1;
|
||||
} else {
|
||||
buscfg.data0_io_num = data_pins[0];
|
||||
buscfg.data1_io_num = data_pins[1];
|
||||
buscfg.data2_io_num = data_pins[2];
|
||||
buscfg.data3_io_num = data_pins[3];
|
||||
buscfg.data4_io_num = -1;
|
||||
buscfg.data5_io_num = -1;
|
||||
buscfg.data6_io_num = -1;
|
||||
buscfg.data7_io_num = -1;
|
||||
buscfg.flags |= SPICOMMON_BUSFLAG_QUAD;
|
||||
}
|
||||
buscfg.max_transfer_sz = MAX_TRANSFER_SIZE;
|
||||
auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO);
|
||||
if (err != ESP_OK)
|
||||
|
@ -166,8 +234,9 @@ class SPIBusHw : public SPIBus {
|
|||
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);
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return new SPIBusHw(clk, sdo, sdi, interface, data_pins);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -28,6 +28,15 @@ spi:
|
|||
allow_other_uses: false
|
||||
mosi_pin: GPIO6
|
||||
interface: any
|
||||
- id: quad_spi
|
||||
clk_pin: 47
|
||||
data_pins:
|
||||
-
|
||||
number: 40
|
||||
allow_other_uses: false
|
||||
- 41
|
||||
- 42
|
||||
- 43
|
||||
|
||||
spi_device:
|
||||
id: spidev
|
||||
|
|
Loading…
Reference in a new issue