diff --git a/CODEOWNERS b/CODEOWNERS index 7dd73417b6..c3ca5410b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -125,6 +125,7 @@ esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core +esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/rc522/* @glmnet diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 1458629acd..b97fb4ae23 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -5,6 +5,7 @@ #include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" namespace esphome { namespace display { @@ -15,7 +16,8 @@ const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); void DisplayBuffer::init_internal_(uint32_t buffer_length) { - this->buffer_ = new (std::nothrow) uint8_t[buffer_length]; // NOLINT + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_ = allocator.allocate(buffer_length); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); return; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 1135b31798..d42d4f5de3 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -19,6 +19,8 @@ from esphome.cpp_helpers import setup_entity DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["psram"] + esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") @@ -153,9 +155,7 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_ESP32_CAMERA") - cg.add_build_flag("-DBOARD_HAS_PSRAM") if CORE.using_esp_idf: cg.add_library("espressif/esp32-camera", "1.0.0") add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) - add_idf_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index a6d4a30c27..54307dce41 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -49,9 +49,6 @@ void ESP32Camera::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 Camera:"); ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_)); -#ifdef USE_ARDUINO - ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound())); -#endif // USE_ARDUINO ESP_LOGCONFIG(TAG, " Data Pins: D0:%d D1:%d D2:%d D3:%d D4:%d D5:%d D6:%d D7:%d", conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7); ESP_LOGCONFIG(TAG, " VSYNC Pin: %d", conf.pin_vsync); diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index 86b7069c55..dca764c6ed 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -12,6 +12,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c", "esp32"] +AUTO_LOAD = ["psram"] CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" @@ -179,5 +180,3 @@ async def to_code(config): display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) cg.add(var.set_display_data_7_pin(display_data_7)) - - cg.add_build_flag("-DBOARD_HAS_PSRAM") diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 8a05836db9..e62e594a49 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -42,32 +42,32 @@ void Inkplate6::setup() { this->display(); } void Inkplate6::initialize_() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint32_t buffer_size = this->get_buffer_length_(); + if (buffer_size == 0) + return; - if (this->partial_buffer_ != nullptr) { - free(this->partial_buffer_); // NOLINT - } - if (this->partial_buffer_2_ != nullptr) { - free(this->partial_buffer_2_); // NOLINT - } - if (this->buffer_ != nullptr) { - free(this->buffer_); // NOLINT - } + if (this->partial_buffer_ != nullptr) + allocator.deallocate(this->partial_buffer_, buffer_size); + if (this->partial_buffer_2_ != nullptr) + allocator.deallocate(this->partial_buffer_2_, buffer_size * 2); + if (this->buffer_ != nullptr) + allocator.deallocate(this->buffer_, buffer_size); - this->buffer_ = (uint8_t *) ps_malloc(buffer_size); + this->buffer_ = allocator.allocate(buffer_size); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); this->mark_failed(); return; } if (!this->greyscale_) { - this->partial_buffer_ = (uint8_t *) ps_malloc(buffer_size); + this->partial_buffer_ = allocator.allocate(buffer_size); if (this->partial_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate partial buffer for display!"); this->mark_failed(); return; } - this->partial_buffer_2_ = (uint8_t *) ps_malloc(buffer_size * 2); + this->partial_buffer_2_ = allocator.allocate(buffer_size * 2); if (this->partial_buffer_2_ == nullptr) { ESP_LOGE(TAG, "Could not allocate partial buffer 2 for display!"); this->mark_failed(); diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index b16f2fe7eb..1b60034bd1 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -8,6 +8,10 @@ #include "esphome/core/log.h" #include "esphome/components/network/util.h" +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace nextion { static const char *const TAG = "nextion_upload"; @@ -158,12 +162,8 @@ void Nextion::upload_tft() { if (!begin_status) { this->is_updating_ = false; ESP_LOGD(TAG, "connection failed"); -#ifdef USE_ESP32 - if (psramFound()) - free(this->transfer_buffer_); // NOLINT - else -#endif - delete this->transfer_buffer_; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); return; } else { ESP_LOGD(TAG, "Connected"); @@ -252,7 +252,7 @@ void Nextion::upload_tft() { // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 #ifdef USE_ESP32 uint32_t chunk_size = 8192; - if (psramFound()) { + if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) { chunk_size = this->content_length_; } else { if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand @@ -269,32 +269,18 @@ void Nextion::upload_tft() { #endif if (this->transfer_buffer_ == nullptr) { -#ifdef USE_ESP32 - if (psramFound()) { - ESP_LOGD(TAG, "Allocating PSRAM buffer size %d, Free PSRAM size is %u", chunk_size, ESP.getFreePsram()); - this->transfer_buffer_ = (uint8_t *) ps_malloc(chunk_size); - if (this->transfer_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate buffer size %d!", chunk_size); - this->upload_end_(); - } - } else { -#endif - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; - if (this->transfer_buffer_ == nullptr) { // Try a smaller size - ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); - chunk_size = 4096; - ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - this->transfer_buffer_ = new uint8_t[chunk_size]; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + this->transfer_buffer_ = allocator.allocate(chunk_size); + if (this->transfer_buffer_ == nullptr) { // Try a smaller size + ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); + this->transfer_buffer_ = allocator.allocate(chunk_size); - if (!this->transfer_buffer_) - this->upload_end_(); -#ifdef USE_ESP32 - } -#endif + if (!this->transfer_buffer_) + this->upload_end_(); } this->transfer_buffer_size_ = chunk_size; diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py new file mode 100644 index 0000000000..ac6d034514 --- /dev/null +++ b/esphome/components/psram/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.core import CORE +from esphome.const import ( + CONF_ID, +) + +CODEOWNERS = ["@esphome/core"] + +psram_ns = cg.esphome_ns.namespace("psram") +PsramComponent = psram_ns.class_("PsramComponent", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(PsramComponent)}), cv.only_on_esp32 +) + + +async def to_code(config): + if CORE.using_arduino: + cg.add_build_flag("-DBOARD_HAS_PSRAM") + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp new file mode 100644 index 0000000000..8325709632 --- /dev/null +++ b/esphome/components/psram/psram.cpp @@ -0,0 +1,32 @@ +#include "psram.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +#include +#include + +namespace esphome { +namespace psram { + +static const char *const TAG = "psram"; + +void PsramComponent::dump_config() { + // Technically this can be false if the PSRAM is full, but heap_caps_get_total_size() isn't always available, and it's + // very unlikely for the PSRAM to be full. + bool available = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0; + + ESP_LOGCONFIG(TAG, "PSRAM:"); + ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) + if (available) { + ESP_LOGCONFIG(TAG, " Size: %d MB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024 / 1024); + } +#endif +} + +} // namespace psram +} // namespace esphome + +#endif diff --git a/esphome/components/psram/psram.h b/esphome/components/psram/psram.h new file mode 100644 index 0000000000..8c891feee9 --- /dev/null +++ b/esphome/components/psram/psram.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" + +namespace esphome { +namespace psram { + +class PsramComponent : public Component { + void dump_config() override; +}; + +} // namespace psram +} // namespace esphome + +#endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 90f35ee4ca..c9bda18394 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -9,8 +9,8 @@ #include #include -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "esp32-hal-psram.h" +#ifdef USE_ESP32 +#include #endif #include "esphome/core/optional.h" @@ -261,21 +261,6 @@ template class Parented { uint32_t fnv1_hash(const std::string &str); -template T *new_buffer(size_t length) { - T *buffer; -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - if (psramFound()) { - buffer = (T *) ps_malloc(length); - } else { - buffer = new T[length]; // NOLINT(cppcoreguidelines-owning-memory) - } -#else - buffer = new T[length]; // NOLINT(cppcoreguidelines-owning-memory) -#endif - - return buffer; -} - // --------------------------------------------------------------------------------------------------------------------- /// @name STL backports @@ -486,6 +471,51 @@ template T remap(U value, U min, U max, T min_out, T max ///@} +/// @name Memory management +///@{ + +/** An STL allocator that uses SPI RAM. + * + * By setting flags, it can be configured to don't try main memory if SPI RAM is full or unavailable, and to return + * `nulllptr` instead of aborting when no memory is available. + */ +template class ExternalRAMAllocator { + public: + using value_type = T; + + enum Flags { + NONE = 0, + REFUSE_INTERNAL = 1 << 0, ///< Refuse falling back to internal memory when external RAM is full or unavailable. + ALLOW_FAILURE = 1 << 1, ///< Don't abort when memory allocation fails. + }; + + ExternalRAMAllocator() = default; + ExternalRAMAllocator(Flags flags) : flags_{flags} {} + template constexpr ExternalRAMAllocator(const ExternalRAMAllocator &other) : flags_{other.flags} {} + + T *allocate(size_t n) { + size_t size = n * sizeof(T); + T *ptr = nullptr; +#ifdef USE_ESP32 + ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)); +#endif + if (ptr == nullptr && (this->flags_ & Flags::REFUSE_INTERNAL) == 0) + ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + if (ptr == nullptr && (this->flags_ & Flags::ALLOW_FAILURE) == 0) + abort(); + return ptr; + } + + void deallocate(T *p, size_t n) { + free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + } + + private: + Flags flags_{Flags::NONE}; +}; + +/// @} + /// @name Deprecated functions ///@{