From ba1bbaf67db7272760d81add9736b8c965cb4d2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jan 2026 15:09:43 -1000 Subject: [PATCH] [esp32] Move heap functions to flash, saving ~6KB This is the culmination of months of work to reduce heap churn throughout the ESPHome codebase. By systematically eliminating unnecessary dynamic allocations (StaticVector, FixedVector, const char* instead of std::string, pre-allocated buffers, etc.), heap functions are now called so infrequently that they can safely be moved from IRAM to flash. Enable CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH by default, which moves malloc/free/realloc from IRAM to flash. This is safe because: - Heap functions should never be called from ISRs - CONFIG_SPI_MASTER_ISR_IN_IRAM is not enabled - Audio/video use pre-allocated ring buffers, not dynamic allocation Measured results: +6,124 bytes of heap freed. Add heap_in_iram advanced option as an escape hatch for users who need heap functions in IRAM for specific use cases. --- esphome/components/esp32/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index da550e58dc..aa7d215c06 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -644,6 +644,7 @@ CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_FREERTOS_IN_IRAM = "freertos_in_iram" CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" +CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking @@ -745,6 +746,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_FREERTOS_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean, + cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 @@ -1090,6 +1092,12 @@ async def to_code(config): # Place in flash to save IRAM (default) add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) + # Place heap functions into flash to save IRAM (~4-6KB savings) + # Safe as long as heap functions are not called from ISRs (which they shouldn't be) + # Users can set heap_in_iram: true as an escape hatch if needed + if not conf[CONF_ADVANCED][CONF_HEAP_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH", True) + # Setup watchdog add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)