From a4a09043ec1c1b5dd1f35c785125d751d56f2b43 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:32:24 +0200 Subject: [PATCH] [deep_sleep][gpio][core][power_supply] Prepare for gpio hold (2/3) (#18966) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/const/__init__.py | 1 + .../deep_sleep/deep_sleep_esp32.cpp | 17 ++++++++----- esphome/components/esp32/gpio.h | 8 ++++++ esphome/components/gpio/output/__init__.py | 25 ++++++++++++++++++- .../gpio/output/gpio_binary_output.h | 7 ++++-- .../components/gpio/switch/gpio_switch.cpp | 5 ++-- .../components/power_supply/power_supply.cpp | 14 ++++++++++- .../components/power_supply/power_supply.h | 1 + esphome/core/defines.h | 1 + esphome/core/gpio.h | 9 +++++++ 10 files changed, 76 insertions(+), 12 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 6eeab52e29..b54d797f09 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -27,6 +27,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection" CONF_ENABLED = "enabled" CONF_GYROSCOPE_ODR = "gyroscope_odr" CONF_GYROSCOPE_RANGE = "gyroscope_range" +CONF_HOLD_STATE = "hold_state" CONF_HOST = "host" CONF_HUMIDITY_SOURCE = "humidity_source" CONF_IAQ = "iaq" diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index eef392ad5c..b71a4fa138 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -111,7 +111,17 @@ void DeepSleepComponent::deep_sleep_() { if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); - // Single pin wakeup (ext0) - ESP32, S2, S3 only +#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP +#ifdef USE_GPIO_HOLD + // Variants without per-pin deep sleep hold need the global enable + gpio_deep_sleep_hold_en(); +#else + if (this->wakeup_pin_ != nullptr) + gpio_deep_sleep_hold_en(); +#endif +#endif + + // Single pin wakeup (ext0) - ESP32, S2, S3 only #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ !defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) @@ -124,11 +134,6 @@ void DeepSleepComponent::deep_sleep_() { } gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); gpio_hold_en(gpio_pin); -#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP - // Some ESP32 variants support holding a single GPIO during deep sleep without this function - // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep - gpio_deep_sleep_hold_en(); -#endif bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index aeff5af51c..9c661b4d9e 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -31,6 +31,14 @@ class ESP32InternalGPIOPin final : public InternalGPIOPin { bool is_inverted() const override { return this->pin_flags_.inverted; } gpio_num_t get_pin_num() const { return static_cast(this->pin_); } gpio_drive_cap_t get_drive_strength() const { return static_cast(this->pin_flags_.drive_strength); } +#ifdef USE_GPIO_HOLD + bool is_held() const override { + if (!this->get_hold()) + return false; + esp_reset_reason_t reason = esp_reset_reason(); + return reason == ESP_RST_DEEPSLEEP || reason == ESP_RST_SW; + } +#endif protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; diff --git a/esphome/components/gpio/output/__init__.py b/esphome/components/gpio/output/__init__.py index ab242c643f..645dd61f80 100644 --- a/esphome/components/gpio/output/__init__.py +++ b/esphome/components/gpio/output/__init__.py @@ -1,8 +1,10 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_PIN +from esphome.const import CONF_ID, CONF_PIN, CONF_POWER_SUPPLY +import esphome.final_validate as fv from esphome.types import ConfigType from .. import gpio_ns @@ -17,6 +19,27 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend( ).extend(cv.COMPONENT_SCHEMA) +def _require_hold(ps_config: ConfigType) -> ConfigType: + if not ps_config[CONF_PIN].get(CONF_HOLD_STATE): + raise cv.Invalid( + f"The power supply pin must also set {CONF_HOLD_STATE} " + f"when an output it powers sets it", + [CONF_PIN], + ) + return ps_config + + +def _final_validate(config: ConfigType) -> None: + if not config[CONF_PIN].get(CONF_HOLD_STATE): + return + if (ps_id := config.get(CONF_POWER_SUPPLY)) is None: + return + fv.id_declaration_match_schema(_require_hold)(ps_id) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await output.register_output(var, config) diff --git a/esphome/components/gpio/output/gpio_binary_output.h b/esphome/components/gpio/output/gpio_binary_output.h index 496afd131b..06e3647482 100644 --- a/esphome/components/gpio/output/gpio_binary_output.h +++ b/esphome/components/gpio/output/gpio_binary_output.h @@ -11,9 +11,12 @@ class GPIOBinaryOutput final : public output::BinaryOutput, public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void setup() override { - this->turn_off(); + const bool held = this->pin_->is_held(); + if (!held) + this->turn_off(); this->pin_->setup(); - this->turn_off(); + if (!held) + this->turn_off(); } void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 4daea8d877..450a97eb19 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -12,8 +12,9 @@ float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; void GPIOSwitch::setup() { bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); - // write state before setup - this->control(initial_state); + // write state before setup unless the pin kept it across the reset + if (!this->pin_->is_held()) + this->control(initial_state); this->pin_->setup(); // write after setup again for other IOs this->control(initial_state); diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index 3abffc88e1..8fa1b92c25 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -9,7 +9,12 @@ static constexpr uint32_t POWER_OFF_TIMEOUT_ID = 0; void PowerSupply::setup() { this->pin_->setup(); - this->pin_->digital_write(false); + if (this->pin_->is_held()) { + // Rail stayed on across the reset; drop it unless something asks for it + this->schedule_off_if_idle_(); + } else { + this->pin_->digital_write(false); + } if (this->enable_on_boot_) this->request_high_power(); } @@ -47,6 +52,10 @@ void PowerSupply::unrequest_high_power() { return; } this->active_requests_--; + this->schedule_off_if_idle_(); +} + +void PowerSupply::schedule_off_if_idle_() { if (this->active_requests_ == 0) { this->set_timeout(POWER_OFF_TIMEOUT_ID, this->keep_on_time_, [this]() { ESP_LOGV(TAG, "Disabling"); @@ -54,7 +63,10 @@ void PowerSupply::unrequest_high_power() { }); } } + void PowerSupply::on_powerdown() { + if (this->pin_->get_hold()) + return; this->active_requests_ = 0; this->pin_->digital_write(false); } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index eaf77af32e..dc6b368ea8 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -39,6 +39,7 @@ class PowerSupply final : public Component { uint32_t keep_on_time_; int16_t active_requests_{0}; // use signed integer to make catching negative requests easier. bool enable_on_boot_{false}; + void schedule_off_if_idle_(); }; class PowerSupplyRequester { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 12f61467d6..d4932ae749 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -474,6 +474,7 @@ #define USE_USB_UART_CP210X #define USE_USB_UART_FT23XX #define USE_USB_UART_PL2303 +#define USE_GPIO_HOLD // USB identity on serial proxy ports needs the usb_host stack #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4) diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index 43db3b7c0c..7daf1917a6 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -2,6 +2,7 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -27,6 +28,7 @@ enum Flags : uint8_t { FLAG_OPEN_DRAIN = 0x04, FLAG_PULLUP = 0x08, FLAG_PULLDOWN = 0x10, + FLAG_HOLD = 0x20, }; class FlagsHelper { @@ -79,6 +81,13 @@ class GPIOPin { virtual size_t dump_summary(char *buffer, size_t len) const; virtual bool is_internal() { return false; } +#ifdef USE_GPIO_HOLD + bool get_hold() const { return this->get_flags() & gpio::FLAG_HOLD; } + virtual bool is_held() const { return false; } +#else + bool get_hold() const { return false; } + bool is_held() const { return false; } +#endif }; /// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions)