mirror of
https://github.com/esphome/esphome.git
synced 2026-08-28 00:33:31 +00:00
[spi] Use PSRAM DMA for external buffers (#18699)
This commit is contained in:
@@ -15,6 +15,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
only_on_variant,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
@@ -126,6 +127,7 @@ CONF_FORCE_SW = "force_sw"
|
||||
CONF_INTERFACE = "interface"
|
||||
CONF_INTERFACE_INDEX = "interface_index"
|
||||
CONF_RELEASE_DEVICE = "release_device"
|
||||
CONF_PSRAM_DMA = "psram_dma"
|
||||
TYPE_SINGLE = "single"
|
||||
TYPE_QUAD = "quad"
|
||||
TYPE_OCTAL = "octal"
|
||||
@@ -136,6 +138,29 @@ TYPE_CLASS = {
|
||||
TYPE_OCTAL: OctalSPIComponent,
|
||||
}
|
||||
|
||||
|
||||
def _validate_psram_dma(value: Any) -> bool:
|
||||
value = cv.boolean(value)
|
||||
if not value:
|
||||
return value
|
||||
return cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.only_with_framework("esp-idf"),
|
||||
only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S31,
|
||||
VARIANT_ESP32S3,
|
||||
],
|
||||
msg_prefix="PSRAM DMA",
|
||||
),
|
||||
cv.require_framework_version(esp_idf=cv.Version(5, 5, 3)),
|
||||
cv.requires_component("psram"),
|
||||
)(value)
|
||||
|
||||
|
||||
# RP2040 SPI pin assignments are complicated;
|
||||
# refer to GPIO function select table in https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
|
||||
|
||||
@@ -450,6 +475,7 @@ def spi_device_schema(
|
||||
SPI_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32),
|
||||
cv.Optional(CONF_PSRAM_DMA): _validate_psram_dma,
|
||||
cs_pin_option(CONF_CS_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
@@ -471,6 +497,9 @@ async def register_spi_device(
|
||||
cg.add(var.set_mode(spi_mode))
|
||||
if release_device := config.get(CONF_RELEASE_DEVICE):
|
||||
cg.add(var.set_release_device(release_device))
|
||||
if psram_dma := config.get(CONF_PSRAM_DMA):
|
||||
cg.add_define("USE_SPI_PSRAM_DMA")
|
||||
cg.add(var.set_psram_dma(psram_dma))
|
||||
|
||||
|
||||
def final_validate_device_schema(
|
||||
@@ -498,6 +527,36 @@ def final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
def _walk_config(value: Any, path: tuple[Any, ...] = ()):
|
||||
if isinstance(value, dict):
|
||||
yield value, path
|
||||
for key, child in value.items():
|
||||
yield from _walk_config(child, (*path, key))
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
yield from _walk_config(child, (*path, index))
|
||||
|
||||
|
||||
def _final_validate(config: Any) -> Any:
|
||||
buses = config if isinstance(config, list) else [config]
|
||||
software_bus_ids = {
|
||||
bus[CONF_ID] for bus in buses if CONF_INTERFACE_INDEX not in bus
|
||||
}
|
||||
if not software_bus_ids:
|
||||
return config
|
||||
for candidate, path in _walk_config(fv.full_config.get()):
|
||||
if (
|
||||
candidate.get(CONF_PSRAM_DMA)
|
||||
and candidate.get(CONF_SPI_ID) in software_bus_ids
|
||||
):
|
||||
with cv.prepend_path([cv.ROOT_CONFIG_PATH, *path, CONF_PSRAM_DMA]):
|
||||
raise cv.Invalid("psram_dma requires a hardware SPI interface")
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"spi_arduino.cpp": {
|
||||
|
||||
@@ -253,11 +253,18 @@ class SPIDelegate {
|
||||
// check if device is ready
|
||||
virtual bool is_ready();
|
||||
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
void set_psram_dma(bool enable) { this->psram_dma_ = enable; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST};
|
||||
uint32_t data_rate_{1000000};
|
||||
SPIMode mode_{MODE0};
|
||||
GPIOPin *cs_pin_{NullPin::NULL_PIN};
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
bool psram_dma_{false};
|
||||
#endif
|
||||
static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
};
|
||||
|
||||
@@ -397,6 +404,11 @@ class SPIClient {
|
||||
esph_log_d("spi_device", "mode %u, data_rate %ukHz", (unsigned) this->mode_, (unsigned) (this->data_rate_ / 1000));
|
||||
this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_,
|
||||
this->release_device_, this->write_only_);
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
this->delegate_->set_psram_dma(this->psram_dma_);
|
||||
if (this->psram_dma_)
|
||||
esph_log_config("spi_device", "PSRAM DMA: enabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
virtual void spi_teardown() {
|
||||
@@ -407,6 +419,9 @@ class SPIClient {
|
||||
bool spi_is_ready() { return this->delegate_->is_ready(); }
|
||||
void set_release_device(bool release) { this->release_device_ = release; }
|
||||
void set_write_only(bool write_only) { this->write_only_ = write_only; }
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
void set_psram_dma(bool enable) { this->psram_dma_ = enable; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST};
|
||||
@@ -416,6 +431,9 @@ class SPIClient {
|
||||
GPIOPin *cs_{nullptr};
|
||||
bool release_device_{false};
|
||||
bool write_only_{false};
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
bool psram_dma_{false};
|
||||
#endif
|
||||
SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
#include "spi.h"
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
#include <esp_memory_utils.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::spi {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
static const char *const TAG = "spi";
|
||||
static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API.
|
||||
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
static uint32_t get_psram_dma_flags(bool enabled, const void *tx_buffer) {
|
||||
if (enabled && tx_buffer != nullptr && esp_ptr_dma_ext_capable(tx_buffer))
|
||||
return SPI_TRANS_DMA_USE_PSRAM;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
class SPIDelegateHw : public SPIDelegate {
|
||||
public:
|
||||
SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin,
|
||||
@@ -65,8 +77,13 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
return;
|
||||
}
|
||||
spi_transaction_t desc = {};
|
||||
desc.flags = 0;
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
const uint32_t psram_flags = rxbuf == nullptr ? get_psram_dma_flags(this->psram_dma_, txbuf) : 0;
|
||||
#endif
|
||||
while (length != 0) {
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
desc.flags = psram_flags;
|
||||
#endif
|
||||
size_t const partial = std::min(length, MAX_TRANSFER_SIZE);
|
||||
desc.length = partial * 8;
|
||||
desc.rxlength = this->write_only_ ? 0 : partial * 8;
|
||||
@@ -81,6 +98,12 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
ESP_LOGE(TAG, "Transmit failed - err %X", err);
|
||||
break;
|
||||
}
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
if ((desc.flags & SPI_TRANS_DMA_TX_FAIL) != 0) {
|
||||
ESP_LOGE(TAG, "PSRAM DMA TX underflow");
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
length -= partial;
|
||||
if (txbuf != nullptr)
|
||||
txbuf += partial;
|
||||
@@ -133,7 +156,13 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
desc.base.rxlength = 0;
|
||||
desc.base.cmd = cmd;
|
||||
desc.base.addr = address;
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
const uint32_t transaction_flags = desc.base.flags | get_psram_dma_flags(this->psram_dma_, data);
|
||||
#endif
|
||||
do {
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
desc.base.flags = transaction_flags;
|
||||
#endif
|
||||
size_t chunk_size = std::min(length, MAX_TRANSFER_SIZE);
|
||||
if (data != nullptr && chunk_size != 0) {
|
||||
desc.base.length = chunk_size * 8;
|
||||
@@ -152,6 +181,12 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
ESP_LOGE(TAG, "Transmit failed - err %X", err);
|
||||
return;
|
||||
}
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
if ((desc.base.flags & SPI_TRANS_DMA_TX_FAIL) != 0) {
|
||||
ESP_LOGE(TAG, "PSRAM DMA TX underflow");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// if more data is to be sent, skip the command and address phases.
|
||||
desc.command_bits = 0;
|
||||
desc.address_bits = 0;
|
||||
|
||||
@@ -355,6 +355,7 @@
|
||||
#define USE_SPEAKER
|
||||
#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
#define USE_SPI
|
||||
#define USE_SPI_PSRAM_DMA
|
||||
#define USE_VOICE_ASSISTANT
|
||||
#define USE_WEBSERVER
|
||||
#define USE_WEBSERVER_AUTH
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Tests for SPI PSRAM DMA configuration validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
)
|
||||
from esphome.components.spi import (
|
||||
CONF_INTERFACE_INDEX,
|
||||
CONF_PSRAM_DMA,
|
||||
_final_validate,
|
||||
spi_device_schema,
|
||||
)
|
||||
from esphome.config import Config
|
||||
from esphome.const import CONF_ID, CONF_SPI_ID, KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from esphome.core import CORE, ID
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _schema() -> cv.Schema:
|
||||
return spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_data_rate="1MHz",
|
||||
default_mode="MODE0",
|
||||
)
|
||||
|
||||
|
||||
def _stage(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
platform_framework: PlatformFramework,
|
||||
variant: str,
|
||||
version: cv.Version,
|
||||
) -> None:
|
||||
set_core_config(
|
||||
platform_framework,
|
||||
core_data={KEY_FRAMEWORK_VERSION: version},
|
||||
platform_data={KEY_BOARD: "test-board", KEY_VARIANT: variant},
|
||||
)
|
||||
CORE.loaded_integrations.add("psram")
|
||||
|
||||
|
||||
def test_psram_dma_accepts_supported_idf_target(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
VARIANT_ESP32S3,
|
||||
cv.Version(5, 5, 3),
|
||||
)
|
||||
|
||||
config = _schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
assert config[CONF_PSRAM_DMA] is True
|
||||
|
||||
|
||||
def test_psram_dma_accepts_esp32s31(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
VARIANT_ESP32S31,
|
||||
cv.Version(6, 0, 0),
|
||||
)
|
||||
|
||||
config = _schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
assert config[CONF_PSRAM_DMA] is True
|
||||
|
||||
|
||||
def test_psram_dma_rejects_arduino(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
VARIANT_ESP32S3,
|
||||
cv.Version(5, 5, 3),
|
||||
)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="only available with framework"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
def test_psram_dma_rejects_target_without_capability(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
VARIANT_ESP32,
|
||||
cv.Version(5, 5, 3),
|
||||
)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="PSRAM DMA is only available"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
def test_psram_dma_false_is_portable(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
VARIANT_ESP32,
|
||||
cv.Version(5, 5, 2),
|
||||
)
|
||||
|
||||
config = _schema()({CONF_PSRAM_DMA: False})
|
||||
|
||||
assert config[CONF_PSRAM_DMA] is False
|
||||
|
||||
|
||||
def test_psram_dma_rejects_older_idf(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
VARIANT_ESP32S3,
|
||||
cv.Version(5, 5, 2),
|
||||
)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="requires at least framework version 5.5.3"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
def test_psram_dma_requires_psram_component(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
VARIANT_ESP32S3,
|
||||
cv.Version(5, 5, 3),
|
||||
)
|
||||
CORE.loaded_integrations.remove("psram")
|
||||
|
||||
with pytest.raises(cv.Invalid, match="requires component psram"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
def _full_spi_config(*, hardware: bool, with_device: bool = True) -> tuple[Config, ID]:
|
||||
bus_id = ID("spi_bus", is_declaration=True, type="SPIComponent")
|
||||
bus = {CONF_ID: bus_id}
|
||||
if hardware:
|
||||
bus[CONF_INTERFACE_INDEX] = 0
|
||||
full = Config()
|
||||
full["spi"] = [bus]
|
||||
if with_device:
|
||||
full["spi_device_test"] = {
|
||||
CONF_SPI_ID: ID("spi_bus"),
|
||||
CONF_PSRAM_DMA: True,
|
||||
}
|
||||
full.declare_ids.append((bus_id, ["spi", 0, CONF_ID]))
|
||||
return full, ID("spi_bus", is_declaration=False, type="SPIComponent")
|
||||
|
||||
|
||||
def test_psram_dma_accepts_hardware_spi(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
full_config, _ = _full_spi_config(hardware=True)
|
||||
set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config)
|
||||
|
||||
_final_validate(full_config["spi"])
|
||||
|
||||
|
||||
def test_psram_dma_rejects_software_spi(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
full_config, _ = _full_spi_config(hardware=False)
|
||||
set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="psram_dma requires a hardware SPI") as error:
|
||||
_final_validate(full_config["spi"])
|
||||
|
||||
assert error.value.path[-2:] == ["spi_device_test", CONF_PSRAM_DMA]
|
||||
|
||||
|
||||
def test_spi_bus_rejects_psram_dma_device_without_component_final_validation(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
full_config, bus_id = _full_spi_config(hardware=False, with_device=False)
|
||||
full_config["device_without_final_validation"] = {
|
||||
CONF_SPI_ID: bus_id,
|
||||
CONF_PSRAM_DMA: True,
|
||||
}
|
||||
set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="psram_dma requires a hardware SPI") as error:
|
||||
_final_validate(full_config["spi"])
|
||||
|
||||
assert error.value.path[-2:] == [
|
||||
"device_without_final_validation",
|
||||
CONF_PSRAM_DMA,
|
||||
]
|
||||
|
||||
|
||||
def test_psram_dma_accepts_hardware_device_with_mixed_buses(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
software_bus_id = ID("software_bus", is_declaration=True, type="SPIComponent")
|
||||
hardware_bus_id = ID("hardware_bus", is_declaration=True, type="SPIComponent")
|
||||
full_config = Config()
|
||||
full_config["spi"] = [
|
||||
{CONF_ID: software_bus_id},
|
||||
{CONF_ID: hardware_bus_id, CONF_INTERFACE_INDEX: 0},
|
||||
]
|
||||
full_config["spi_device_test"] = {
|
||||
CONF_SPI_ID: ID("hardware_bus"),
|
||||
CONF_PSRAM_DMA: True,
|
||||
}
|
||||
full_config.declare_ids.extend(
|
||||
(
|
||||
(software_bus_id, ["spi", 0, CONF_ID]),
|
||||
(hardware_bus_id, ["spi", 1, CONF_ID]),
|
||||
)
|
||||
)
|
||||
set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config)
|
||||
|
||||
_final_validate(full_config["spi"])
|
||||
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
common: !include common.yaml
|
||||
psram:
|
||||
mode: octal
|
||||
spi_device:
|
||||
- id: spi_device_psram_dma_test
|
||||
psram_dma: true
|
||||
data_rate: 1MHz
|
||||
spi_mode: 0
|
||||
Reference in New Issue
Block a user