Add quad spi features (#5925)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs 2024-01-19 13:42:17 +11:00 committed by GitHub
parent 2283b3b443
commit 1fef769496
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 207 additions and 40 deletions

View File

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

View File

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

View File

@ -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(); }

View File

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

View File

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

View File

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