diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index b71a4fa138..7cf90bc33e 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -111,7 +111,7 @@ void DeepSleepComponent::deep_sleep_() { if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); -#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP +#ifndef 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(); diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index d9c9b86185..59788e76dc 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -50,17 +50,52 @@ struct ISRPinArg { gpio_num_t pin; gpio::Flags flags; bool inverted; +#ifdef USE_GPIO_HOLD + bool hold; +#endif #if defined(USE_ESP32_VARIANT_ESP32) bool use_rtc; int rtc_pin; #endif }; +#ifdef USE_GPIO_HOLD +// Re-latch a held pad onto the values just written to its registers. +static inline void refresh_hold(const ESP32InternalGPIOPin &pin) { + pin.set_hold(false); + pin.set_hold(true); +} + +static inline void IRAM_ATTR isr_refresh_hold(const ISRPinArg *arg) { + if (!arg->hold) + return; +#if defined(USE_ESP32_VARIANT_ESP32) + if (arg->use_rtc) { + rtcio_hal_hold_disable(arg->rtc_pin); + rtcio_hal_hold_enable(arg->rtc_pin); + return; + } +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + // GPIO0-21 are held from the RTC domain; the digital hold mask is zero for them + if (arg->pin < SOC_RTCIO_PIN_COUNT) { + rtcio_hal_hold_disable(arg->pin); + rtcio_hal_hold_enable(arg->pin); + return; + } +#endif + gpio_hal_hold_dis(&GPIO_HAL, arg->pin); + gpio_hal_hold_en(&GPIO_HAL, arg->pin); +} +#endif + ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) arg->pin = this->get_pin_num(); arg->flags = gpio::FLAG_NONE; arg->inverted = this->pin_flags_.inverted; +#ifdef USE_GPIO_HOLD + arg->hold = this->get_hold(); +#endif #if defined(USE_ESP32_VARIANT_ESP32) arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num()); if (arg->use_rtc) @@ -105,7 +140,23 @@ size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const { return snprintf(buffer, len, "GPIO%" PRIu32, static_cast(this->pin_)); } +#ifdef USE_GPIO_HOLD +void ESP32InternalGPIOPin::set_hold(bool hold) const { + if (!this->get_hold()) + return; + if (hold) { + gpio_hold_en(this->get_pin_num()); + } else { + gpio_hold_dis(this->get_pin_num()); + } +} +#endif + void ESP32InternalGPIOPin::setup() { +#ifdef USE_GPIO_HOLD + // Hold before gpio_config so the pad keeps its state while the registers change + this->set_hold(true); +#endif gpio_config_t conf{}; conf.pin_bit_mask = 1ULL << static_cast(this->pin_); conf.mode = flags_to_mode(this->flags_); @@ -116,6 +167,18 @@ void ESP32InternalGPIOPin::setup() { if (this->flags_ & gpio::FLAG_OUTPUT) { gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength()); } +#ifdef USE_GPIO_HOLD + // gpio_hold_dis logs an error on input-only pads + if (GPIO_IS_VALID_OUTPUT_GPIO(this->get_pin_num())) { + if (!this->get_hold()) { + // Release a hold left behind by an earlier boot + gpio_hold_dis(this->get_pin_num()); + } else if (this->flags_ & gpio::FLAG_INPUT) { + // Inputs apply the config now so reads work; outputs wait for the first write + refresh_hold(*this); + } + } +#endif } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { @@ -130,6 +193,9 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { pull_mode = GPIO_PULLDOWN_ONLY; } gpio_set_pull_mode(this->get_pin_num(), pull_mode); +#ifdef USE_GPIO_HOLD + refresh_hold(*this); +#endif } bool ESP32InternalGPIOPin::digital_read() { @@ -137,6 +203,9 @@ bool ESP32InternalGPIOPin::digital_read() { } void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0); +#ifdef USE_GPIO_HOLD + refresh_hold(*this); +#endif } void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); } @@ -144,6 +213,8 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->ge using namespace esp32; +// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- some gpio_hal functions use MMIO at fixed addresses +// internally bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(this->arg_); return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted; @@ -152,6 +223,9 @@ bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { auto *arg = reinterpret_cast(this->arg_); gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted); +#ifdef USE_GPIO_HOLD + isr_refresh_hold(arg); +#endif } void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { @@ -203,8 +277,12 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { gpio_hal_input_disable(&GPIO_HAL, arg->pin); } } +#ifdef USE_GPIO_HOLD + isr_refresh_hold(arg); +#endif arg->flags = flags; } +// NOLINTEND(clang-analyzer-core.FixedAddressDereference) } // namespace esphome diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 954b213d7f..6ac1673127 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -37,6 +37,8 @@ class ESP32InternalGPIOPin final : public GPIOPin { 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 + /// Apply or release the pad hold. Does nothing unless the pin is configured with hold_state. + void set_hold(bool hold) const; bool is_held() const override { if (!this->get_hold()) return false; diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 321dd3d498..7753574bc8 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -5,6 +5,7 @@ from typing import Any from esphome import pins import esphome.codegen as cg +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv from esphome.const import ( CONF_ID, @@ -248,6 +249,7 @@ ESP32_PIN_SCHEMA = cv.All( cv.float_with_unit("current", "mA", optional_unit=True), cv.enum(DRIVE_STRENGTHS), ), + cv.Optional(CONF_HOLD_STATE, default=False): cv.boolean, } ), validate_gpio_pin, @@ -266,7 +268,11 @@ async def esp32_pin_to_code(config): cg.add(var.set_inverted(True)) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) - cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + flags = pins.gpio_flags_expr(config[CONF_MODE]) + if config[CONF_HOLD_STATE]: + flags = flags | cg.gpio_Flags.FLAG_HOLD + cg.add_define("USE_GPIO_HOLD") + cg.add(var.set_flags(flags)) return var diff --git a/esphome/components/esp32/gpio_common.py b/esphome/components/esp32/gpio_common.py new file mode 100644 index 0000000000..a12bebf6e9 --- /dev/null +++ b/esphome/components/esp32/gpio_common.py @@ -0,0 +1,26 @@ +"""Pin checks shared by the ESP32 variant validators.""" + +from logging import Logger +from typing import Any + +from esphome.components.const import CONF_HOLD_STATE +from esphome.const import CONF_NUMBER + + +def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None: + if num in usb_jtag_pins: + logger.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + num, + ) + + +def check_usb_jtag_hold( + conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger +) -> None: + num = conf[CONF_NUMBER] + if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins: + logger.warning( + "GPIO%d cannot hold at low level during wakeup from deep sleep.", num + ) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index fec257f90b..11c62359c1 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,6 +1,7 @@ import logging from typing import Any +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, @@ -53,6 +54,7 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: is_output = mode[CONF_OUTPUT] is_pullup = mode[CONF_PULLUP] is_pulldown = mode[CONF_PULLDOWN] + is_hold = value.get(CONF_HOLD_STATE) if is_input: # All ESP32 pins support input mode @@ -70,6 +72,14 @@ def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: raise cv.Invalid( f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] ) + if is_hold and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) is input-only and cannot be held.", + [CONF_HOLD_STATE], + ) + if is_hold and num == 20: + # Not in the digital hold mask and not an RTC pad, so the driver cannot hold it + raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE]) check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 6eb002f3f0..c4624a93a4 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + _ESP32C3_SPI_PSRAM_PINS = { 12: "SPIHD", 13: "SPIWP", @@ -28,12 +30,7 @@ def esp32_c3_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP32C3_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER) return value @@ -49,6 +46,7 @@ def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c5.py b/esphome/components/esp32/gpio_esp32_c5.py index 639ed64c9e..b3f4c319c6 100644 --- a/esphome/components/esp32/gpio_esp32_c5.py +++ b/esphome/components/esp32/gpio_esp32_c5.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h _ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3} @@ -20,6 +22,8 @@ _ESP32C5_SPI_PSRAM_PINS = { _ESP32C5_STRAPPING_PINS = {2, 7, 27, 28} +_ESP32C5_USB_JTAG_PINS = {13, 14} + _LOGGER = logging.getLogger(__name__) @@ -31,6 +35,7 @@ def esp32_c5_validate_gpio_pin(value: int) -> int: f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})" ) + check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER) return value @@ -45,6 +50,8 @@ def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index bd7bb9e220..9eba375e26 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h _ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7} @@ -32,12 +34,7 @@ def esp32_c6_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" ) - if value in _ESP32C6_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER) return value @@ -52,6 +49,7 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py index 2f3abe6a0f..3ae8f1c292 100644 --- a/esphome/components/esp32/gpio_esp32_c61.py +++ b/esphome/components/esp32/gpio_esp32_c61.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM _ESP32C61_SPI_PSRAM_PINS = { 14: "SPICS0", @@ -18,6 +20,8 @@ _ESP32C61_SPI_PSRAM_PINS = { _ESP32C61_STRAPPING_PINS = {8, 9} +_ESP32C61_USB_JTAG_PINS = {12, 13} + _LOGGER = logging.getLogger(__name__) @@ -29,6 +33,7 @@ def esp32_c61_validate_gpio_pin(value: int) -> int: f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})" ) + check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER) return value @@ -43,5 +48,7 @@ def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32-C61 pins support input mode pass + check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 9dd6537694..41520bb4b7 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_USB_JTAG_PINS = {26, 27} @@ -24,12 +26,7 @@ def esp32_h2_validate_gpio_pin(value: int) -> int: "See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html", value, ) - if value in _ESP32H2_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER) return value @@ -44,5 +41,6 @@ def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_h21.py b/esphome/components/esp32/gpio_esp32_h21.py index 5ab1b7c074..a13fe3e4cc 100644 --- a/esphome/components/esp32/gpio_esp32_h21.py +++ b/esphome/components/esp32/gpio_esp32_h21.py @@ -5,17 +5,22 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # Partial set from the ESP-IDF / esptool boot-mode docs: # https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html # The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section. _ESP32H21_STRAPPING_PINS: set[int] = {13, 14} +_ESP32H21_USB_JTAG_PINS = {17, 18} + _LOGGER = logging.getLogger(__name__) def esp32_h21_validate_gpio_pin(value: int) -> int: if value < 0 or value > 25: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)") + check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER) return value @@ -30,5 +35,7 @@ def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_h4.py b/esphome/components/esp32/gpio_esp32_h4.py index 86a4d55858..0cbd83c50c 100644 --- a/esphome/components/esp32/gpio_esp32_h4.py +++ b/esphome/components/esp32/gpio_esp32_h4.py @@ -5,17 +5,22 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # Partial set from the ESP-IDF / esptool boot-mode docs: # https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html # The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section. _ESP32H4_STRAPPING_PINS: set[int] = {13, 14} +_ESP32H4_USB_JTAG_PINS = {13, 14} + _LOGGER = logging.getLogger(__name__) def esp32_h4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 39: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") + check_usb_jtag_pin(value, _ESP32H4_USB_JTAG_PINS, _LOGGER) return value @@ -30,5 +35,7 @@ def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32H4_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 6e9227c501..69fdf5dd22 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf _ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} @@ -18,12 +20,7 @@ _LOGGER = logging.getLogger(__name__) def esp32_p4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 54: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") - if value in _ESP32P4_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32P4_USB_JTAG_PINS, _LOGGER) return value @@ -38,6 +35,7 @@ def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32P4_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index 4978f48a1c..0de621789d 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,6 +1,7 @@ import logging from typing import Any +from esphome.components.const import CONF_HOLD_STATE import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, @@ -70,6 +71,11 @@ def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: raise cv.Invalid( f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] ) + if value.get(CONF_HOLD_STATE) and num == 46: + raise cv.Invalid( + f"GPIO{num} is input-only and cannot be held.", + [CONF_HOLD_STATE], + ) check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index db8c520533..d38c0a8011 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -12,6 +12,8 @@ from esphome.const import ( from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin from esphome.types import ConfigType +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + _ESP32S3_SPI_PSRAM_PINS = { 26: "SPICS1", 27: "SPIHD", @@ -53,12 +55,7 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") - if value in _ESP32S3_USB_JTAG_PINS: - _LOGGER.warning( - "GPIO%d is used by the USB-Serial-JTAG interface." - " Using this pin as GPIO will conflict with USB-Serial-JTAG.", - value, - ) + check_usb_jtag_pin(value, _ESP32S3_USB_JTAG_PINS, _LOGGER) return value @@ -73,6 +70,7 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if is_input: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32S3_USB_JTAG_PINS, _LOGGER) check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_s31.py b/esphome/components/esp32/gpio_esp32_s31.py index 7ccb7cdb90..a6a3b39193 100644 --- a/esphome/components/esp32/gpio_esp32_s31.py +++ b/esphome/components/esp32/gpio_esp32_s31.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin + # Per the ESP32-S31 IDF DOCS and datasheet: # https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html # https://documentation.espressif.com/esp32-s31_datasheet_en.pdf @@ -16,6 +18,8 @@ _ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61} # LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table. _ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6} +_ESP32S31_USB_JTAG_PINS = {33, 34} + _LOGGER = logging.getLogger(__name__) @@ -28,6 +32,7 @@ def esp32_s31_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used." ) + check_usb_jtag_pin(value, _ESP32S31_USB_JTAG_PINS, _LOGGER) return value @@ -46,6 +51,8 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass + check_usb_jtag_hold(value, _ESP32S31_USB_JTAG_PINS, _LOGGER) + check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 41fae283b4..cccb7289fb 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -238,6 +238,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { this->mark_failed(); return; } +#ifdef USE_GPIO_HOLD + // Release held pins so the UART peripheral can drive them + this->set_pins_hold_(false); +#endif #ifdef USE_UART_WAKE_LOOP_ON_RX // Register ISR callback to wake the main loop when UART data arrives. @@ -514,7 +518,20 @@ void IDFUARTComponent::on_shutdown() { if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); } +#ifdef USE_GPIO_HOLD + // Hold the pins again so they keep their state through the reset + this->set_pins_hold_(true); +#endif } +#ifdef USE_GPIO_HOLD +void IDFUARTComponent::set_pins_hold_(bool hold) { + for (auto *pin : {this->tx_pin_, this->rx_pin_, this->flow_control_pin_}) { + if (pin != nullptr) + pin->set_hold(hold); + } +} +#endif + } // namespace esphome::uart #endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 7c93b74fbd..260117c429 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -90,6 +90,9 @@ class IDFUARTComponent final : public UARTComponent, public Component { protected: void check_logger_conflict() override; +#ifdef USE_GPIO_HOLD + void set_pins_hold_(bool hold); +#endif uint32_t line_inversion_mask_(); // Re-applies what uart_param_config() resets: inversion, RX threshold/timeout, mode. esp_err_t apply_line_settings_(); diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 04416d7f57..4b7a0a6ab8 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -10,6 +10,7 @@ from typing import Any import pytest +from esphome.components.const import CONF_HOLD_STATE from esphome.components.esp32 import ( _ESP_TLS_LINKING_COMPONENTS, DEFAULT_EXCLUDED_IDF_COMPONENTS, @@ -38,13 +39,30 @@ from esphome.components.esp32.const import ( KEY_NETWORK_SDKCONFIG, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + VARIANT_ESP32H4, + VARIANT_ESP32H21, + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32S31, ) -from esphome.components.esp32.gpio import validate_gpio_pin +from esphome.components.esp32.gpio import validate_gpio_pin, validate_supports import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, CONF_IGNORE_PIN_VALIDATION_ERROR, + CONF_INPUT, + CONF_MODE, CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, PlatformFramework, Toolchain, ) @@ -57,7 +75,7 @@ def test_esp32_config( ) -> None: set_core_config(PlatformFramework.ESP32_IDF) - from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_ESP32, VARIANT_FRIENDLY + from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_FRIENDLY # Example ESP32 configuration config = { @@ -1722,9 +1740,8 @@ def test_esp32_s31_gpio_validation( caplog: pytest.LogCaptureFixture, ) -> None: """S31: GPIO26-28/30-32 are reserved for the SPI flash interface, GPIO29 - and GPIO41 do not exist, GPIO33 is a normal pin, and GPIO36 is a + and GPIO41 do not exist, GPIO35 is a normal pin, and GPIO36 is a strapping pin.""" - from esphome.components.esp32.const import VARIANT_ESP32S31 from esphome.components.esp32.gpio import validate_supports from esphome.const import CONF_INPUT, CONF_MODE, CONF_OPEN_DRAIN, CONF_OUTPUT @@ -1735,8 +1752,8 @@ def test_esp32_s31_gpio_validation( input_mode = {CONF_INPUT: True, CONF_OUTPUT: False, CONF_OPEN_DRAIN: False} # Not reserved; a normal GPIO - pin = {CONF_NUMBER: 33, CONF_IGNORE_PIN_VALIDATION_ERROR: False} - assert validate_gpio_pin(pin)[CONF_NUMBER] == 33 + pin = {CONF_NUMBER: 35, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + assert validate_gpio_pin(pin)[CONF_NUMBER] == 35 # Reserved for the SPI flash interface, but can be bypassed with # ignore_pin_validation_error @@ -1764,6 +1781,113 @@ def test_esp32_s31_gpio_validation( assert "GPIO36 is a strapping pin" in caplog.text +_INPUT_ONLY_SETTINGS = ( + (CONF_OUTPUT, "does not support output pin mode"), + (CONF_PULLUP, "does not support pullups"), + (CONF_PULLDOWN, "does not support pulldowns"), + (CONF_HOLD_STATE, "is input-only and cannot be held"), +) + + +@pytest.mark.parametrize( + ("variant", "number", "setting", "error"), + [ + pytest.param( + variant, number, setting, error, id=f"{name}-gpio{number}-{setting}" + ) + for variant, name, numbers in ( + (VARIANT_ESP32, "esp32", range(34, 40)), + (VARIANT_ESP32S2, "s2", (46,)), + ) + for number in numbers + for setting, error in _INPUT_ONLY_SETTINGS + ] + + [ + pytest.param( + VARIANT_ESP32, + 20, + CONF_HOLD_STATE, + "GPIO20 has no hold function", + id="esp32-gpio20-hold_state", + ) + ], +) +def test_input_only_gpio_rejects_unsupported_modes( + set_core_config: SetCoreConfigCallable, + variant: str, + number: int, + setting: str, + error: str, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: variant}) + mode = { + CONF_INPUT: True, + CONF_OUTPUT: False, + CONF_OPEN_DRAIN: False, + CONF_PULLUP: False, + CONF_PULLDOWN: False, + } + pin = {CONF_NUMBER: number, CONF_MODE: mode} + if setting == CONF_HOLD_STATE: + pin[setting] = True + else: + mode[setting] = True + + with pytest.raises(cv.Invalid, match=error): + validate_supports(pin) + + +@pytest.mark.parametrize( + ("variant", "number"), + [ + pytest.param(VARIANT_ESP32C3, 18, id="c3-18"), + pytest.param(VARIANT_ESP32C3, 19, id="c3-19"), + pytest.param(VARIANT_ESP32C5, 13, id="c5-13"), + pytest.param(VARIANT_ESP32C5, 14, id="c5-14"), + pytest.param(VARIANT_ESP32C6, 12, id="c6-12"), + pytest.param(VARIANT_ESP32C6, 13, id="c6-13"), + pytest.param(VARIANT_ESP32C61, 12, id="c61-12"), + pytest.param(VARIANT_ESP32C61, 13, id="c61-13"), + pytest.param(VARIANT_ESP32H2, 26, id="h2-26"), + pytest.param(VARIANT_ESP32H2, 27, id="h2-27"), + pytest.param(VARIANT_ESP32H4, 13, id="h4-13"), + pytest.param(VARIANT_ESP32H4, 14, id="h4-14"), + pytest.param(VARIANT_ESP32H21, 17, id="h21-17"), + pytest.param(VARIANT_ESP32H21, 18, id="h21-18"), + pytest.param(VARIANT_ESP32P4, 24, id="p4-24"), + pytest.param(VARIANT_ESP32P4, 25, id="p4-25"), + pytest.param(VARIANT_ESP32S3, 19, id="s3-19"), + pytest.param(VARIANT_ESP32S3, 20, id="s3-20"), + pytest.param(VARIANT_ESP32S31, 33, id="s31-33"), + pytest.param(VARIANT_ESP32S31, 34, id="s31-34"), + ], +) +def test_usb_jtag_gpio_hold_state_warns( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, + variant: str, + number: int, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: variant}) + pin = { + CONF_NUMBER: number, + CONF_MODE: { + CONF_INPUT: True, + CONF_OUTPUT: False, + CONF_OPEN_DRAIN: False, + }, + CONF_HOLD_STATE: True, + } + + with caplog.at_level(logging.WARNING): + validate_supports(pin) + + assert ( + f"GPIO{number} cannot hold at low level during wakeup from deep sleep." + in caplog.text + ) + + _TLS_SERVER_OPTIONS = ( "CONFIG_MBEDTLS_TLS_CLIENT_ONLY", "CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", diff --git a/tests/component_tests/gpio/test_output_final_validate.py b/tests/component_tests/gpio/test_output_final_validate.py new file mode 100644 index 0000000000..5d92ad44c6 --- /dev/null +++ b/tests/component_tests/gpio/test_output_final_validate.py @@ -0,0 +1,38 @@ +"""Tests for GPIO output final validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components.const import CONF_HOLD_STATE +from esphome.components.gpio.output import FINAL_VALIDATE_SCHEMA +from esphome.config import Config +from esphome.const import CONF_ID, CONF_PIN, CONF_POWER_SUPPLY, PlatformFramework +from esphome.core import ID +from tests.component_tests.types import SetCoreConfigCallable + + +@pytest.mark.parametrize("power_supply_holds", [False, True]) +def test_output_hold_state_requires_power_supply_hold_state( + set_core_config: SetCoreConfigCallable, + power_supply_holds: bool, +) -> None: + supply_id = ID("supply", is_declaration=True, type="power_supply") + full_config = Config() + full_config["power_supply"] = [ + {CONF_ID: supply_id, CONF_PIN: {CONF_HOLD_STATE: power_supply_holds}} + ] + full_config.declare_ids.append((supply_id, ["power_supply", 0, CONF_ID])) + set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config) + + config = { + CONF_PIN: {CONF_HOLD_STATE: True}, + CONF_POWER_SUPPLY: ID("supply", is_declaration=False, type="power_supply"), + } + if power_supply_holds: + FINAL_VALIDATE_SCHEMA(config) + else: + with pytest.raises( + cv.Invalid, + match="The power supply pin must also set hold_state when an output it powers sets it", + ): + FINAL_VALIDATE_SCHEMA(config) diff --git a/tests/components/deep_sleep/test.esp32-idf.yaml b/tests/components/deep_sleep/test.esp32-idf.yaml index e45eb08349..cc9bf5af16 100644 --- a/tests/components/deep_sleep/test.esp32-idf.yaml +++ b/tests/components/deep_sleep/test.esp32-idf.yaml @@ -1,5 +1,27 @@ substitutions: wakeup_pin: GPIO4 -<<: !include common.yaml -<<: !include common-esp32-all.yaml +packages: + deep_sleep: !include common.yaml + deep_sleep_esp32: !include common-esp32-all.yaml + +power_supply: + - id: power_supply_ds_esp32 + pin: + number: GPIO5 + hold_state: true + +output: + - platform: gpio + pin: + number: GPIO22 + hold_state: true + id: gpio_output_ds_esp32 + power_supply: power_supply_ds_esp32 + +switch: + - platform: gpio + pin: + number: GPIO23 + hold_state: true + id: gpio_switch_ds_esp32 diff --git a/tests/components/gpio/test.esp32-c3-idf.yaml b/tests/components/gpio/test.esp32-c3-idf.yaml index e9071b4356..42c207d627 100644 --- a/tests/components/gpio/test.esp32-c3-idf.yaml +++ b/tests/components/gpio/test.esp32-c3-idf.yaml @@ -6,4 +6,19 @@ substitutions: switch_pin_3: GPIO6 switch_pin_4: GPIO7 -<<: !include common.yaml +packages: + gpio: !include common.yaml + +binary_sensor: + - platform: gpio + pin: + number: GPIO8 + hold_state: true + id: gpio_binary_sensor_esp32 + +output: + - platform: gpio + pin: + number: GPIO9 + hold_state: true + id: gpio_output_esp32 diff --git a/tests/components/gpio/test.esp32-idf.yaml b/tests/components/gpio/test.esp32-idf.yaml index 862aa533ea..12be2ef6fe 100644 --- a/tests/components/gpio/test.esp32-idf.yaml +++ b/tests/components/gpio/test.esp32-idf.yaml @@ -6,4 +6,26 @@ substitutions: switch_pin_3: GPIO16 switch_pin_4: GPIO17 -<<: !include common.yaml +packages: + gpio: !include common.yaml + +binary_sensor: + - platform: gpio + pin: + number: GPIO18 + hold_state: true + id: gpio_binary_sensor_esp32 + +output: + - platform: gpio + pin: + number: GPIO19 + hold_state: true + id: gpio_output_esp32 + +switch: + - platform: gpio + pin: + number: GPIO21 + hold_state: true + id: gpio_switch_esp32 diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 9550de911b..3bb87aa3cc 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -14,7 +14,12 @@ esphome: uart: - id: uart_id - tx_pin: 17 + tx_pin: + number: 17 + mode: + pullup: true + output: true + hold_state: true rx_pin: 16 flow_control_pin: 4 baud_rate: 9600