[gpio][esp32][uart] Add option to hold gpio during (deep)sleep and reboot (3/3) (#18964)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
luar123
2026-10-10 02:37:46 +00:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent dc553aae55
commit ba7d3bd770
25 changed files with 442 additions and 43 deletions
@@ -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();
+78
View File
@@ -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<uint32_t>(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<uint32_t>(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<ISRPinArg *>(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<ISRPinArg *>(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
+2
View File
@@ -37,6 +37,8 @@ class ESP32InternalGPIOPin final : public GPIOPin {
gpio_num_t get_pin_num() const { return static_cast<gpio_num_t>(this->pin_); }
gpio_drive_cap_t get_drive_strength() const { return static_cast<gpio_drive_cap_t>(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;
+7 -1
View File
@@ -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
+26
View File
@@ -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
)
+10
View File
@@ -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
+4 -6
View File
@@ -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
@@ -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
+4 -6
View File
@@ -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
@@ -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
+4 -6
View File
@@ -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
@@ -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
@@ -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
+4 -6
View File
@@ -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
@@ -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
+4 -6
View File
@@ -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
@@ -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
@@ -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
@@ -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_();
+130 -6
View File
@@ -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",
@@ -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)
@@ -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
+16 -1
View File
@@ -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
+23 -1
View File
@@ -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
+6 -1
View File
@@ -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