The Problem: Why I2C Breaks in PlatformIO but Works in Arduino IDE

When building complex ESP32 projects with display frameworks like LovyanGFX (LGFX) and multiple I2C peripherals (such as the DRV2605, PCA9557, or ADS1015), you might run into a frustrating issue: everything works perfectly in the Arduino IDE, but compiling the exact same code in PlatformIO causes either the touch screen or the other I2C devices to stop responding.

This conflict typically happens because both LovyanGFX and your other device libraries are trying to initialize and control the global Wire instance independently. In ESP32 Arduino Core v3.x, changes to the underlying I2C driver mean that calling Wire.begin() multiple times with different parameters can reset or lock up the bus.

Why Does This Mismatch Happen?

  • Library Initialization Order: LovyanGFX's touch driver often initializes the I2C bus internally. If your other libraries call Wire.begin(19, 20) afterwards, it can overwrite the settings LGFX expects.
  • PlatformIO vs. Arduino Core Versions: PlatformIO might be using a slightly different version of the ESP32 Arduino Core or different compiler optimization flags, which exposes race conditions in the I2C driver initialization.

How to Fix It: Step-by-Step Solutions

1. Align Your PlatformIO Core Version

First, ensure PlatformIO is using the exact same ESP32 framework version as your working Arduino IDE setup. In your platformio.ini, pin the platform version to match the core version you used in Arduino IDE:

[env:esp32dev]
platform = espressif32 @ 6.6.0 ; This corresponds to Arduino Core 3.0.x
framework = arduino
board = esp32dev

2. Initialize the Shared Wire Bus Manually

Instead of letting LovyanGFX and your other libraries auto-initialize the I2C bus, initialize the Wire instance manually in your setup() function before anything else. Then, configure LovyanGFX to use this existing bus.

Here is how to structure your setup() code:

#include <Wire.h>
#include <LovyanGFX.hpp>
#include <Adafruit_DRV2605.h>

// Create your LGFX instance
class LGFX_Device : public lgfx::LGFX_Device {
  // Your custom LGFX panel and touch configuration here
};
LGFX_Device lcd;

Adafruit_DRV2605 haptic;

void setup() {
  Serial.begin(115200);

  // 1. Explicitly initialize the I2C bus on pins 19 and 20
  Wire.begin(19, 20, 400000); // SDA, SCL, Frequency

  // 2. Initialize LovyanGFX
  // Ensure your LGFX config is set to use the standard Wire port (0)
  lcd.init();

  // 3. Initialize other I2C devices WITHOUT calling Wire.begin() again inside them if possible
  // If the library forces Wire.begin(), make sure it uses the same pins
  if (!haptic.begin(&Wire)) {
    Serial.println("Failed to find DRV2605");
  }
}

3. Configure LovyanGFX Touch to Use the Shared I2C Port

In your LovyanGFX setup class, make sure the touch configuration explicitly targets the correct I2C port (usually 0 for the default Wire instance) and does not attempt to override the pins if they are already active:

auto cfg = _touch_instance.config();
cfg.pin_sda = 19;
cfg.pin_scl = 20;
cfg.i2c_port = 0; // Use 0 for Wire, 1 for Wire1
cfg.i2c_addr = 0x38; // Your touch controller address
cfg.freq = 400000;
_touch_instance.config(cfg);

4. Prevent Re-Initialization Conflicts

Some libraries hardcode Wire.begin() inside their begin() methods without parameters, which can reset the SDA/SCL pins back to the ESP32 defaults (usually pins 21 and 22). To prevent this:

  • Pass the &Wire reference to your library initializers (e.g., ads.begin(0x48, &Wire)).
  • If a library does not accept a custom TwoWire pointer and forces a default Wire.begin(), call Wire.setPins(19, 20); right before initializing that library so it defaults to the correct pins.