From 2890afe0e5a4299c81a5f7e0873e70553c31a5cb Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:38:20 +1000 Subject: [PATCH] [esp32] Fix S31 reserved pins (#18915) Co-authored-by: Claude Sonnet 5 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/gpio_esp32_s31.py | 13 +++++++----- tests/component_tests/esp32/test_esp32.py | 20 +++++++++++++----- .../mipi_rgb/test_mipi_rgb_config.py | 21 +++++++++++++------ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_s31.py b/esphome/components/esp32/gpio_esp32_s31.py index c53c32c99c..7ccb7cdb90 100644 --- a/esphome/components/esp32/gpio_esp32_s31.py +++ b/esphome/components/esp32/gpio_esp32_s31.py @@ -5,11 +5,10 @@ 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, 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 +# 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 +_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32} _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. @@ -25,6 +24,10 @@ def esp32_s31_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)") if value in _ESP32S31_INVALID_PINS: raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.") + 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." + ) return value diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 190f2d2896..bef273badd 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -1274,8 +1274,9 @@ 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.""" + """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 + 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 @@ -1286,9 +1287,18 @@ def test_esp32_s31_gpio_validation( 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} + # Not reserved; a normal GPIO + pin = {CONF_NUMBER: 33, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + assert validate_gpio_pin(pin)[CONF_NUMBER] == 33 + + # Reserved for the SPI flash interface, but can be bypassed with + # ignore_pin_validation_error + for num in (26, 27, 28, 30, 31, 32): + with pytest.raises(cv.Invalid, match=f"GPIO{num} is reserved"): + validate_gpio_pin( + {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + ) + pin = {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: True} assert validate_gpio_pin(pin)[CONF_NUMBER] == num for num in (29, 41): diff --git a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py index 497aba4df1..ac8e111ddb 100644 --- a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py +++ b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py @@ -144,17 +144,22 @@ def test_metadata_records_rotation( @pytest.mark.parametrize( - ("variant", "board"), + ("variant", "board", "model"), [ - (VARIANT_ESP32S3, "esp32-s3-devkitc-1"), - (VARIANT_ESP32P4, "esp32-p4-evboard"), + # ESP32-8048S070 is a real Sunton board wired for ESP32-S3 (e.g. its + # default de_pin is GPIO41, which doesn't exist on S31), so it is + # only meaningful as a config on that variant. + (VARIANT_ESP32S3, "esp32-s3-devkitc-1", "ESP32-8048S070"), + # P4 and S31 use the pin-agnostic CUSTOM model so this only checks + # that the chip itself is accepted, independent of board wiring. + (VARIANT_ESP32P4, "esp32-p4-evboard", "CUSTOM"), # No dedicated board is registered for ESP32-S31 yet; an unknown board # name simply skips per-board pin validation. - (VARIANT_ESP32S31, "esp32-s31-devkitc"), + (VARIANT_ESP32S31, "esp32-s31-devkitc", "CUSTOM"), ], ) def test_configuration_succeeds_on_supported_variants( - variant: str, board: str, set_core_config: SetCoreConfigCallable + variant: str, board: str, model: str, set_core_config: SetCoreConfigCallable ) -> None: """mipi_rgb requires a chip with an RGB LCD peripheral: S3, P4 or S31.""" set_core_config( @@ -164,7 +169,11 @@ def test_configuration_succeeds_on_supported_variants( from esphome.components.mipi_rgb.display import CONFIG_SCHEMA - CONFIG_SCHEMA({"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}) + config = {"model": model, "data_pins": DATA_PINS, "pclk_pin": 21} + if model == "CUSTOM": + config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] + config[CONF_DIMENSIONS] = {CONF_WIDTH: 480, CONF_HEIGHT: 480} + CONFIG_SCHEMA(config) def test_only_on_variant_rejects_unsupported_variant(