[esp32] Fix ESP32-S31 GPIO validation (#18904)

This commit is contained in:
Jonathan Swoboda
2026-08-31 07:19:20 -04:00
committed by GitHub
parent 404da26a37
commit 575a540c9f
2 changed files with 52 additions and 9 deletions
+15 -9
View File
@@ -5,11 +5,15 @@ 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
# Per the ESP32-S31 datasheet (page 96):
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source.
_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61}
# Per the ESP32-S31 datasheet, the SPI flash and PSRAM interfaces use
# dedicated package pins (SPICS/SPIQ/SPIWP/SPIHD/SPICLK/SPID) outside the
# GPIO matrix, so no GPIOs are reserved for them. GPIO29 and GPIO41 do not
# exist on this chip (SOC_GPIO_VALID_GPIO_MASK excludes them).
# https://documentation.espressif.com/esp32-s31_datasheet_en.html
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
_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}
@@ -19,10 +23,8 @@ _LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
)
if value in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
return value
@@ -33,6 +35,10 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
# Checked here as well so ignore_pin_validation_error cannot bypass it;
# these pins are not bonded and can never work
if num in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
if is_input:
# All ESP32 pins support input mode
pass
+37
View File
@@ -1268,3 +1268,40 @@ def test_parse_pio_platform_version(value: str, expected: str) -> None:
from esphome.components.esp32 import _parse_pio_platform_version
assert _parse_pio_platform_version(value) == expected
def test_esp32_s31_gpio_validation(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""S31: flash uses dedicated pins so GPIO27-33 are normal pins, GPIO29 and
GPIO41 do not exist, 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
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32S31}
)
input_mode = {CONF_INPUT: True, CONF_OUTPUT: False, CONF_OPEN_DRAIN: False}
# Previously reserved for the flash interface, which uses dedicated pins
for num in (27, 28, 31, 32, 33):
pin = {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
assert validate_gpio_pin(pin)[CONF_NUMBER] == num
for num in (29, 41):
with pytest.raises(cv.Invalid, match=f"GPIO{num} does not exist"):
validate_gpio_pin(
{CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
)
# Also rejected in validate_supports so ignore_pin_validation_error
# cannot bypass it
with pytest.raises(cv.Invalid, match=f"GPIO{num} does not exist"):
validate_supports({CONF_NUMBER: num, CONF_MODE: input_mode})
pin = {CONF_NUMBER: 36, CONF_MODE: input_mode}
with caplog.at_level("WARNING"):
validate_supports(pin)
assert "GPIO36 is a strapping PIN" in caplog.text