From ed289390df50082374f23ff20daa735bd075cdda Mon Sep 17 00:00:00 2001 From: kbx81 Date: Thu, 21 May 2026 21:32:38 -0500 Subject: [PATCH] =?UTF-8?q?feat(wifi):=20Unit=20B+=20=E2=80=94=20defer=20e?= =?UTF-8?q?sp=5Fwifi=5Finit()=20to=20lazy-init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WiFi already had enable_on_boot + enable()/disable()/is_disabled() lifecycle, but enable_on_boot:false didn't actually save any memory. wifi_pre_setup_() called esp_wifi_init() and esp_netif_create_default_wifi_sta() unconditionally during setup(), which allocates ~15-30KB of DMA-capable internal SRAM (RX/TX buffers, driver state, PHY init). The flag only skipped esp_wifi_start() in the followup branch — the driver was already resident, just not associated. This commit splits wifi_pre_setup_() into two parts: - wifi_pre_setup_() (light, kept in setup() always): MAC setup, event group creation, WIFI_EVENT/IP_EVENT handler registration. No DMA allocation. - wifi_lazy_init_() (heavy, NEW): esp_netif_create_default_wifi_sta()/_ap(), esp_wifi_init(), esp_wifi_set_storage(). The DMA-allocating calls. Guarded by wifi_initialized_ flag for idempotency. setup() calls wifi_lazy_init_() only when enable_on_boot_=true. The else branch sets WIFI_COMPONENT_STATE_DISABLED without any heavy init — the dormant interface costs zero DMA-capable memory. enable() calls wifi_lazy_init_() before start(), so a runtime enable after boot-time disable does the heavy init on demand. Idempotent — subsequent enable/disable cycles don't re-allocate. disable() is unchanged — it stops wifi but doesn't deinit. A future "release_on_disable" variant could call esp_wifi_deinit() to actually free the memory at runtime, but that requires coordinating with consumers holding wifi-bound sockets and is out of scope here. ESP-IDF only. Other platforms (Arduino on ESP32, ESP8266) keep the existing behavior — their wifi_pre_setup_() lives in different per-platform files. Field-tested on ESP32-S3 with W5500 SPI ethernet + audio + bluetooth_proxy. Before Unit B+: ~14KB free internal during peak load, crash on W5500 SPI DMA buffer allocation. After Unit B+: ~32KB free internal, Min Free 78KB in some test configurations — sufficient headroom for the other DMA consumers (I2S audio, BT controller) to operate. --- esphome/components/wifi/wifi_component.cpp | 14 +++++++++++--- esphome/components/wifi/wifi_component.h | 12 ++++++++++++ esphome/components/wifi/wifi_component_esp_idf.cpp | 13 ++++++++++++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index edfb93bba2..020ad294c2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -632,11 +632,14 @@ void WiFiComponent::setup() { #endif if (this->enable_on_boot_) { +#ifdef USE_ESP32 + this->wifi_lazy_init_(); +#endif this->start(); } else { -#ifdef USE_ESP32 - esp_netif_init(); -#endif + // Note: esp_netif_init() used to be called here as a fallback for non-IDF builds. + // It is now centralized in NetworkComponent::setup(), which runs at AFTER_BLUETOOTH + // (300) ahead of WiFi (250). No action needed. this->state_ = WIFI_COMPONENT_STATE_DISABLED; } } @@ -1278,6 +1281,11 @@ void WiFiComponent::enable() { ESP_LOGD(TAG, "Enabling"); this->state_ = WIFI_COMPONENT_STATE_OFF; +#ifdef USE_ESP32 + // Idempotent — only allocates DMA buffers + netifs on the first call. After this, + // start() can safely run. + this->wifi_lazy_init_(); +#endif this->start(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 0437267a1f..d0521e548a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -694,6 +694,12 @@ class WiFiComponent final : public Component { bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); +#ifdef USE_ESP32 + // ESP-IDF only: defers esp_wifi_init() + netif creation (which allocate ~15-30KB of + // DMA-capable internal SRAM) until wifi actually needs to come up. Idempotent. + // Called from setup() only when enable_on_boot_=true, and from enable() on first use. + void wifi_lazy_init_(); +#endif WiFiSTAConnectStatus wifi_sta_connect_status_() const; bool is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && @@ -889,6 +895,12 @@ class WiFiComponent final : public Component { bool rrm_{false}; #endif bool enable_on_boot_{true}; +#ifdef USE_ESP32 + // Tracks whether esp_wifi_init() + netif creation has happened. Allows enable() + // to be called at runtime without re-allocating, and ensures the heavy init is + // skipped entirely when enable_on_boot_ is false until first enable(). + bool wifi_initialized_{false}; +#endif bool got_ipv4_address_{false}; bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 11b39b5000..b5ca4e1e4f 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -163,6 +163,16 @@ void WiFiComponent::wifi_pre_setup_() { ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err)); return; } + // NOTE: netif creation + esp_wifi_init() used to live here. They allocate ~15-30KB of + // DMA-capable internal SRAM, which competes with W5500 SPI DMA and I2S DMA on + // memory-tight devices. They are now deferred to wifi_lazy_init_(), called from + // setup() when enable_on_boot_ is true, or from enable() on first runtime enable. + // This makes enable_on_boot:false genuinely skip the wifi DMA allocation. +} + +void WiFiComponent::wifi_lazy_init_() { + if (this->wifi_initialized_) + return; s_sta_netif = esp_netif_create_default_wifi_sta(); @@ -175,7 +185,7 @@ void WiFiComponent::wifi_pre_setup_() { ESP_LOGW(TAG, "starting wifi without nvs"); cfg.nvs_enable = false; } - err = esp_wifi_init(&cfg); + esp_err_t err = esp_wifi_init(&cfg); if (err != ERR_OK) { ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); return; @@ -185,6 +195,7 @@ void WiFiComponent::wifi_pre_setup_() { ESP_LOGE(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err)); return; } + this->wifi_initialized_ = true; } bool WiFiComponent::wifi_mode_(optional sta, optional ap) {