Skip to content

ESP32 I2C: decide TwoWire::end() in one place; recover a controller left bus busy - #368

Merged
lovyan03 merged 2 commits into
m5stack:developfrom
ainyan03:esp32_i2c_recover
Oct 20, 2026
Merged

lovyan03 merged 2 commits into
m5stack:developfrom
ainyan03:esp32_i2c_recover

Conversation

@ainyan03

Copy link
Copy Markdown
Contributor

Two fixes in the ESP32 I2C layer (src/lgfx/v1/platforms/esp32/common.inl), synced from the shared lgfx source tree.

1. Decide TwoWire::end() in one place, also without core_version.h (fixes #366)

Some board packages, such as M5Stack 2.1.4 (m5stack:esp32, core 2.0.14 based), do not ship core_version.h, so ARDUINO_ESP32_GIT_VER is undefined. i2c::release() then skipped Wire.end() while moving the pins back to GPIO, and the next init() found the Arduino driver still installed: the bus stayed unusable. With Wire.h included, M5.begin() could not find the built-in IMU (thanks @niguragu for the report, the analysis and the first fix).

Whether lgfx calls TwoWire::end() (in release() and in the temporary pin switcher) is now decided in one place:

  • with core_version.h: the previous rule (every version but 2.0.0, which has no end()), unchanged for every official arduino-esp32 release
  • without it: esp_arduino_version.h, 2.0.1 or later

2. Recover a controller left bus busy before a transfer (fixes #362)

Proposed by @GOB52 in #362. A device such as the Unit UltraSonic I2C (RCWL9620) can leave the ESP32 controller waiting for a STOP during its measurement: bus_busy stays set and TIME_OUT is raised. beginTransaction() waited 128 µs and then started the address phase, which failed, and so did every following transfer. If the port is still busy after that wait, i2c_stop() now recovers the bus and the controller first, as the ESP-IDF driver does. It runs after save_reg(), so the registers shared with the platform driver are still restored at the end of the transfer.

Testing

On an M5Stack Gray (ESP32-D0WDQ6).

Case Before After
M5Stack 2.1.4 board package, Wire.h included: built-in IMU not found, 0/40 reads found, 40/40
same: M5.In_I2C.release() → begin() 10 times, then scan the IMU 0/10, SDA/SCL left as GPIO 10/10
arduino-esp32 2.0.17, Unit UltraSonic I2C on Port A, 100 kHz: write 0x01, wait 150 ms, read 3 bytes 0/20 (busy and TIME_OUT after the wait every time) the stuck state no longer fails the transfer
arduino-esp32 2.0.17: built-in IMU, release() → begin() 10 times 40/40, 10/10 40/40, 10/10

With the UltraSonic unit, some reads still fail at the second byte (connection_lost; 68 to 71 of 100 succeed, against 93 of 100 with Wire). The rate is the same when the recovery uses only the peripheral reset, so it is not caused by this change; it will be looked at separately.

The builds for the upstream CI versions pass locally (46 environments), and the fork CI passed.

…ion.h

Whether lgfx may call TwoWire::end() (released ports, the temporary pin
switcher) was decided in three places from ARDUINO_ESP32_GIT_VER, which
comes from core_version.h. Board packages that do not ship that header
left it undefined, so the port was handed back with its pins as GPIO
while the Arduino driver stayed installed; the next init found the
driver open and the bus stayed unusable.

The decision is now one macro, LGFX_TWOWIRE_HAS_END. With core_version.h
it is the previous rule (any version but 2.0.0, which has no end()).
Without it, esp_arduino_version.h decides (>= 2.0.1). The git version
stays first because 2.0.1 and 2.0.2 still report 2.0.0 there. This also
covers 2.0.1 to 2.0.4 without core_version.h, where WIRE_HAS_END is not
yet defined. For every official release the result is unchanged.

The unused i2c_context_t::setPins(), which carried the third copy of the
rule, is removed.
A device can leave the controller waiting for a STOP that never comes:
bus_busy stays set and TIME_OUT is raised while nothing is transferred.
beginTransaction() only waited 128 us for busy to clear and then started
the address phase, which failed at the first wait, and every following
transfer failed the same way. The ESP32 has no FSM reset bit, so nothing
else cleared it.

If the port is still busy after that wait, i2c_stop() now recovers the
bus and the controller first, as the ESP-IDF driver does before it
starts a command. It runs after save_reg(), so load_reg() at the end of
the transfer still restores the registers shared with the platform
driver. The C2 and C3 skip the peripheral reset in i2c_stop() and rely on
the fsm_rst that follows.
@lovyan03
lovyan03 merged commit 4407949 into m5stack:develop Oct 20, 2026
25 checks passed
@ainyan03
ainyan03 deleted the esp32_i2c_recover branch October 20, 2026 01:03
@ainyan03 ainyan03 mentioned this pull request Oct 20, 2026
lovyan03 pushed a commit that referenced this pull request Oct 20, 2026
…eft bus busy

Port of the develop change (#368) to the master layout: the TwoWire::end() guard
becomes LGFX_TWOWIRE_HAS_END (git version first, esp_arduino_version.h when
core_version.h is missing; #366) for release() and the pin search, and
beginTransaction() runs i2c_stop() when the bus is still busy after the 128 us
wait (#362). The unused member setPins() that develop removed is left as it is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants