From 0400c2d3a39315686290c6d4d0f6f4e8ca458c74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 13:19:20 -1000 Subject: [PATCH 1/3] [i2c] Fix RP2040 I2C bus selection based on pin assignment instead of definition order The RP2040 I2C bus was previously assigned Wire/Wire1 based on the order I2C buses were defined in YAML, not based on which GPIO pins were used. This caused I2C1 to not work when only a single bus was configured with I2C1 pins (e.g., GPIO6/GPIO7). Now selects the correct Wire instance using the RP2040/RP2350 GPIO pin mapping formula: (pin / 2) % 2. Also adds config validation to catch SDA/SCL pin mismatches and duplicate controller assignments. Closes https://github.com/esphome/esphome/issues/14742 --- esphome/components/i2c/__init__.py | 30 ++++++++++++++++++++++ esphome/components/i2c/i2c_bus_arduino.cpp | 9 ++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index de3f2be6740..3884204ae5c 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -93,11 +93,25 @@ def _bus_declare_type(value): raise NotImplementedError +def _rp2040_i2c_controller(pin): + """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin.""" + return (pin // 2) % 2 + + def validate_config(config): if CORE.is_esp32: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) )(config) + if CORE.is_rp2040: + sda_controller = _rp2040_i2c_controller(config[CONF_SDA]) + scl_controller = _rp2040_i2c_controller(config[CONF_SCL]) + if sda_controller != scl_controller: + raise cv.Invalid( + f"SDA pin GPIO{config[CONF_SDA]} is on I2C{sda_controller} but " + f"SCL pin GPIO{config[CONF_SCL]} is on I2C{scl_controller}. " + f"Both pins must be on the same I2C controller." + ) return config @@ -146,6 +160,22 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + if CORE.is_rp2040: + if len(full_config) > 2: + raise cv.Invalid( + "The maximum number of I2C interfaces for RP2040/RP2350 is 2" + ) + if len(full_config) > 1: + controllers = [ + _rp2040_i2c_controller(conf[CONF_SDA]) for conf in full_config + ] + if len(set(controllers)) != len(controllers): + raise cv.Invalid( + "Multiple I2C buses are configured to use the same I2C controller. " + "Each bus must use pins on a different controller " + "(I2C0: SDA on GPIO 0,4,8,12,16,20,24,28; " + "I2C1: SDA on GPIO 2,6,10,14,18,22,26)." + ) if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 5120eb4c007..e339fe59fc8 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -20,12 +20,13 @@ void ArduinoI2CBus::setup() { #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) - static bool first = true; - if (first) { + // Select Wire instance based on pin assignment, not definition order. + // RP2040 I2C controller is determined by GPIO: (pin / 2) % 2 + // I2C0 SDA: GPIO 0,4,8,12,16,20,24,28 I2C1 SDA: GPIO 2,6,10,14,18,22,26 + if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; - first = false; } else { - wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) + wire_ = &Wire1; } #endif From 75a546bd96f36f91d43e1707db33cdbf39656e69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 13:21:03 -1000 Subject: [PATCH 2/3] Add datasheet references for RP2040/RP2350 I2C pin mapping --- esphome/components/i2c/__init__.py | 8 +++++++- esphome/components/i2c/i2c_bus_arduino.cpp | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 3884204ae5c..88a497ddf2c 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -94,7 +94,13 @@ def _bus_declare_type(value): def _rp2040_i2c_controller(pin): - """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin.""" + """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin. + + See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"): + https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + See RP2350 datasheet Table 7 (section 9.4, "Function Select"): + https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + """ return (pin // 2) % 2 diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index e339fe59fc8..3a511edfdbb 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -22,7 +22,8 @@ void ArduinoI2CBus::setup() { #elif defined(USE_RP2040) // Select Wire instance based on pin assignment, not definition order. // RP2040 I2C controller is determined by GPIO: (pin / 2) % 2 - // I2C0 SDA: GPIO 0,4,8,12,16,20,24,28 I2C1 SDA: GPIO 2,6,10,14,18,22,26 + // See RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + // See RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; } else { From 32e8efb7b7e6554a37fec3a2cd2111b2f3b7fb51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 13:24:27 -1000 Subject: [PATCH 3/3] Use formula-based description instead of incomplete GPIO pin lists Addresses review feedback: the hard-coded pin lists only covered RP2040 GPIOs and were incomplete for RP2350 (GPIO up to 47). Now describes the (gpio / 2) % 2 rule instead. --- esphome/components/i2c/__init__.py | 7 ++++--- esphome/components/i2c/i2c_bus_arduino.cpp | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 88a497ddf2c..1684f479ba3 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -178,9 +178,10 @@ def _final_validate(config): if len(set(controllers)) != len(controllers): raise cv.Invalid( "Multiple I2C buses are configured to use the same I2C controller. " - "Each bus must use pins on a different controller " - "(I2C0: SDA on GPIO 0,4,8,12,16,20,24,28; " - "I2C1: SDA on GPIO 2,6,10,14,18,22,26)." + "Each bus must use pins on a different controller. " + "The I2C controller is determined by (gpio / 2) % 2: " + "even pin pairs (0-1, 4-5, 8-9, ...) use I2C0, " + "odd pin pairs (2-3, 6-7, 10-11, ...) use I2C1." ) if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 3a511edfdbb..47a06abe9ec 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -21,9 +21,9 @@ void ArduinoI2CBus::setup() { wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) // Select Wire instance based on pin assignment, not definition order. - // RP2040 I2C controller is determined by GPIO: (pin / 2) % 2 - // See RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf - // See RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 + // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; } else {