[uart] Fix UART0 default pin IOMUX loopback on ESP32

Clear residual IOMUX function on UART0 default pins before calling
uart_set_pin(). The ROM bootloader configures these pins via IOMUX
for console output. 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 is later reassigned as RX via GPIO matrix
(common on ESP32-C3 where users swap GPIO20/GPIO21), the old IOMUX TX
function remains active, causing transmitted data to loop back into RX.

This is a targeted fix using gpio_func_sel() which only writes the
IO MUX function register - unlike the previous gpio_reset_pin()
approach which also changed direction, pulls, and drive strength,
breaking external components that rely on default pin state (#11823).

Fixes #14973
Fixes #14612
This commit is contained in:
J. Nick Koston
2026-03-19 09:28:08 -10:00
parent 37a3c3ab3a
commit 7f6aa47434
@@ -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<gpio_num_t>(tx), PIN_FUNC_GPIO);
}
if (is_default_uart0_pin(rx)) {
gpio_func_sel(static_cast<gpio_num_t>(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;