From d59c006ff9067c8f7b6a439548f5ee0ef0c85e43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 20:56:51 -1000 Subject: [PATCH 1/2] [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) --- .../uart/uart_component_esp_idf.cpp | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 47ddf1a38d..8168e49805 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -7,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" #include "driver/gpio.h" +#include "esp_private/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/application.h" @@ -21,6 +23,20 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual IOMUX state from the ROM bootloader that +/// must be cleared before UART reconfiguration. +/// +/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix, +/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only +/// calls gpio_input_enable() which does NOT clear the IOMUX function select. +/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later +/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active, +/// causing TX data to loop back into RX on the same pin. +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Clear residual IOMUX function on UART0 default pins left by the ROM bootloader. + // See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug. + if (is_default_uart0_pin(tx)) { + gpio_func_sel(static_cast(tx), PIN_FUNC_GPIO); + } + if (is_default_uart0_pin(rx)) { + gpio_func_sel(static_cast(rx), PIN_FUNC_GPIO); + } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 4d7e06da0a47984cecc65c3c9e70121ff866b0a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 23:07:17 -1000 Subject: [PATCH 2/2] [esp32] Disable PicolibC Newlib compatibility shim on IDF 6.0+ ESP-IDF 6.0 switched from Newlib to PicolibC. The Newlib compatibility shim (CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY) provides thread-local stdin/stdout/stderr and getreent() for code compiled against Newlib. ESPHome doesn't link against Newlib-built libraries that use stdio, so the shim is unnecessary overhead. The cost is small (a few dozen bytes of TLS per FreeRTOS task) but there's no reason to carry it. Users linking precompiled Newlib binaries can re-enable via: sdkconfig_options: CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" --- esphome/components/esp32/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 64e5f44081..31abe6cdf1 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1920,6 +1920,15 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + # Disable PicolibC Newlib compatibility shim on IDF 6.0+ + # IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local + # stdin/stdout/stderr and getreent() for code compiled against Newlib. + # ESPHome doesn't link against Newlib-built libraries that use stdio. + # If a component needs it (e.g. precompiled Newlib binaries), re-enable via + # sdkconfig_options: CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False) + # Disable regi2c control functions in IRAM # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: