Skip to content

I2C: share a port the application already opened instead of taking it over - #269

Merged
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:i2c_shared_port
Aug 25, 2026
Merged

I2C: share a port the application already opened instead of taking it over#269
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:i2c_shared_port

Conversation

@ainyan03

Copy link
Copy Markdown
Contributor

Summary

lgfx::i2c::init() assumed the requested HP I2C port was free. When the application had already opened it — through the ESP-IDF i2c_master driver (IDF >= 5.3) or Arduino Wireinit() reset the controller under that driver, and release() later tore the driver's bus (or its pins) down. Reported in #186 as Ex_I2C failing on ESP32-S3 with IDF 5.5 next to an IDF-driver based sensor library.

With this change a port that is already open is recognised beforehand and shared instead of taken over:

  • ESP-IDF 5.3.2 and later: i2c_master_get_bus_handle() says whether the port is open (its "not initialized" error log is muted for that one call, since a free port is the normal case). A failed i2c_new_master_bus() is no longer used as the probe — on IDF 5.5 that failure path unroutes the pins of the existing bus, revokes its GPIO reservations and unbalances the RC_FAST clock reference.
  • Arduino: i2cIsInit() tells whether the sketch has begun Wire on that port.
  • On a shared port nothing is reset or acquired, and the pins are left exactly as the owning driver configured them (routing, pull-ups). Each transaction already applies the library's configuration and endTransaction() restores the driver's register block, so transfers from both sides can alternate. release() only drops the flag.
  • endTransaction() now clears the completion flags before restoring the driver's registers: the driver enables its event interrupts without clearing them first, so a stale flag fired immediately at its next transfer (a real race on dual-core chips when the bus was created on the other core).
  • Other i2c_new_master_bus() failures now make init() fail instead of reporting success with no bus; a port held by a slave driver is refused.

Limits, stated in the code: the two sides are not serialized against each other (the driver's lock is not exposed, so the application must not run them concurrently), both must use the same pins, and only the synchronous ESP-IDF API is compatible (its interrupts are off between transfers). A low power port is not shared. On ESP32-P4 / ESP32-C61 the driver must use the default I2C clock source (the library derives its timing from it). Before ESP-IDF 5.3.2 there is no non-destructive way to tell whether a port is open, so behaviour there is unchanged.

Verification

Grove port with an SSD1306 (0x3C) and a Sensirion SGP30 (0x58); each scenario repeated 8-9 times, all transfers checked against the sensor's serial number:

chip framework result
ESP32-S3 ESP-IDF 5.5.4 Ex_I2C first / IDF bus first / alternate / hand-over via release(): all pass
ESP32-S3 Arduino core 3.3.4 same with Wire: all pass
ESP32-C6 ESP-IDF 5.5.4 / Arduino 3.3.4 all pass
ESP32 ESP-IDF 5.5.4 / Arduino 2.0.17 all pass

Before the change the first transfer after switching sides failed on every chip (the reported write=FAIL read=FAIL), and release() killed the application's bus. Builds also checked on ESP32-C5 (LP port present, IDF and Arduino).

Stress on ESP32 (dual core): IDF bus created on core 0 with enable_internal_pullup = false, a task on core 1 alternating IDF transfer / Ex_I2C transfer / Ex_I2C probe of an absent address (NACK, forced STOP path) / IDF transfer, 300 rounds: 0 failures, pull-up configuration untouched, IDF bus still working after Ex_I2C.release(). Without the completion-flag clearing the IDF transfer after every NACK path fails (300/300), which is what that part of the change fixes.

The one expected failure remains: after the sketch calls Wire.end(), Ex_I2C on that port stops working too, because in an Arduino build it is the same Wire bus.

… over

init() assumed the requested HP I2C port was free. When the application had
already opened it - through the ESP-IDF i2c_master driver or Arduino Wire - the
reset in init() wiped that driver's configuration, the failing
i2c_new_master_bus() went unnoticed (and on ESP-IDF 5.5 that failure path unroutes
the pins of the existing bus), and release() later tore the driver's bus down.
The first transfer after switching sides then failed on every chip.

A port that is already open is now recognised beforehand - i2cIsInit() under
Arduino, i2c_master_get_bus_handle() on ESP-IDF 5.3.2 and later - and shared:
nothing is reset, acquired or re-routed, each transaction applies the library's
configuration and endTransaction() restores the driver's register block, with the
completion flags cleared first so the driver's own interrupt does not see a stale
one. release() on such a port only drops the flag. A port held by a slave driver
is refused, and other i2c_new_master_bus() failures now fail init().

Not covered: serialization between the two sides (the driver's lock is not
exposed, the application must not run them concurrently), differing pins, the
asynchronous ESP-IDF API, low power ports, and a non-default I2C clock source on
ESP32-P4 / ESP32-C61.
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