Skip to content

BMI270: use Wire instead of the legacy I2C driver (fixes driver_ng CONFLICT abort on Arduino core 3.2+) - #8

Merged
weekroom merged 3 commits into
m5stack:mainfrom
devilSatou:claude/bmi270-wire-i2c-driver-conflict
Oct 27, 2026
Merged

weekroom merged 3 commits into
m5stack:mainfrom
devilSatou:claude/bmi270-wire-i2c-driver-conflict

Conversation

@devilSatou

@devilSatou devilSatou commented Oct 14, 2026 •

Copy link
Copy Markdown

Summary

Fixes #4.

On Arduino core 3.2+ (ESP-IDF 5.4+), adding this library to a project aborts before setup() runs. This was observed on an M5Stack CoreS3 with pioarduino platform-espressif32 (stable):

E (1057) i2c: CONFLICT! driver_ng is not allowed to be used with this old driver
abort() was called at PC 0x4204ef83 on core 0

addr2line:

0x4204ef83: check_i2c_driver_conflict at components/driver/i2c/i2c.c:1738
0x4202ab9e: do_global_ctors at components/esp_system/startup.c:104

The "included because of" section of firmware.map:

libesp_driver_i2c.a(i2c_master.c.obj)
    ← libFrameworkArduino.a(esp32-hal-i2c-ng.c.o)  (i2c_new_master_bus)   ← Arduino core's Wire
libdriver.a(i2c.c.obj)
    ← M5Module-GNSS/BMI270.cpp.o  (i2c_master_write_to_device)            ← this library

Root cause

  • BMI270.cpp calls i2c_master_write_to_device() and i2c_master_write_read_device() directly. Despite their names, both belong to the legacy I2C driver (components/driver/i2c/i2c.c). Referencing them links i2c.c.obj, and that object carries a constructor, check_i2c_driver_conflict(), which aborts if the new i2c_master driver is linked too.
  • Since Arduino core 3.2.0, Wire is built on the new driver (cores/esp32/esp32-hal-i2c-ng.c → i2c_new_master_bus(), IDF ≥ 5.4), so both drivers end up in every binary. Core 3.1.x and earlier still used the legacy driver for Wire, which is why ESP3.2 conflict #4 started with 3.2.

Changes

src/utilities/ (the Bosch BMI270/BMM150 Sensor API) is untouched.

src/BMI270.h / src/BMI270.cpp

  • bmi2_i2c_read and bmi2_i2c_write now use TwoWire:
    • read: beginTransmission → write(reg) → endTransmission(false) (repeated start) → requestFrom(addr, len) (checked against len) → read()
    • write: beginTransmission → write(reg) → write(data, len) → endTransmission(true)
    • They still return 0 on success and -1 on failure. On failure, ESP_LOGE now logs the address, the register and Wire's return code.
  • The file-scope static uint8_t _data_buffer[32], static esp_err_t _i2c_ret and struct dev_info* _dev_info were mutable state shared by all instances and tasks. They are now locals. The 32-byte limit (bmi2.read_write_len = 30) is kept.
  • dev_info::i2c_port and _i2c_port (i2c_port_t) are replaced by wire and _wire (TwoWire*).
  • New: int init(TwoWire& wire, uint8_t bmiAddr).
  • int init(i2c_port_t, uint8_t) is kept, deprecated, for source compatibility. I2C_NUM_0 maps to Wire, and I2C_NUM_1 maps to Wire1 on chips with a second HP I2C controller.
    • On IDF 5.x, i2c_port_t comes from the header-only hal/i2c_types.h.
    • On IDF 4.x (core 2.x), it comes from driver/i2c.h, which is where I2C_NUM_x are defined there. IDF 4.x has no new driver to conflict with.
    • Including a header links nothing; only calling the legacy functions does.
  • Behavior change: I2C transactions now use Wire's timeout (50 ms by default) instead of portMAX_DELAY.
  • The library still does not call Wire.begin(). Pins and clock remain the sketch's responsibility.

Example, docs and metadata

  • examples/getSensorData: bmi270.init(I2C_NUM_0, ...) → bmi270.init(Wire, ...)
  • README: documents the change and the new init().
  • Version → 1.0.3

This branch is stacked on #7; its first commit is the version bump from #7. If #7 is merged first, only the Wire port remains here. Merging this PR alone brings in both.

Testing

I couldn't build with the ESP32 toolchain in my environment, so this branch has not had a hardware run or a firmware.map check yet. What I did instead:

  • Compile check: compiled src/BMI270.cpp, plus a sketch that calls both init() overloads, with host g++ (-Wall -Wextra). The stubs reproduce the relevant real declarations:

    • the TwoWire I/O overload sets and the Wire/Wire1 declaration conditions of arduino-esp32 2.0.17, 3.0.7, 3.1.3 and 3.3.12
    • the i2c_port_t / I2C_NUM_x definitions of ESP-IDF 4.4.7, 5.1, 5.3 and 5.5
    • the SOC I2C counts of ESP32-S3, ESP32-C3 and ESP32-C6

    All 10 configurations compile, including core 3.3 with NO_GLOBAL_WIRE. There are no new warnings; the three remaining -Wunused-parameter warnings were already there.

  • Symbol check: nm on the resulting BMI270.o finds no references to i2c_master_write_to_device or i2c_master_write_read_device. The object built from current main references both, and those references are what make the linker pull in libdriver.a(i2c.c.obj).

To confirm end to end, build examples/getSensorData for ESP32-S3 on core 3.2+ and run grep 'libdriver.a(i2c.c.obj)' firmware.map. It should find nothing.

🤖 Generated with Claude Code

claude added 3 commits August 29, 2026 13:10
The invalid int -> i2c_port_t conversion in BMI270.h (dev_info::i2c_port)
was already fixed on main by commit 1aaf0d6, but the library version was
never bumped, so PlatformIO's Library Registry still serves the old 1.0.1
release without the fix, and users pulling M5Module-GNSS as a dependency
still hit the -fpermissive warning. Bump to 1.0.2 so a new release ships
the fix.
On Arduino core 3.x / pioarduino platform-espressif32 (ESP-IDF 5.x), BMI270
called the legacy driver/i2c.h API (i2c_master_write_to_device /
i2c_master_write_read_device) directly. When Arduino's own Wire also links
the new i2c_master driver, ESP-IDF's old i2c.c driver aborts at startup via
a global constructor:

    E (1057) i2c: CONFLICT! driver_ng is not allowed to be used with this
    old driver
    abort() was called at PC 0x4204ef83 on core 0

firmware.map confirmed libdriver.a(i2c.c.obj) was pulled in solely by
BMI270.cpp.o, while libesp_driver_i2c.a(i2c_master.c.obj) was pulled in by
the Arduino core's own Wire (esp32-hal-i2c-ng.c.o), which cannot be
avoided.

Port BMI270's I2C access to Arduino's TwoWire (Wire) so it no longer
touches driver/i2c.h at all:

- dev_info::i2c_port (i2c_port_t) -> dev_info::wire (TwoWire*)
- BMI270::_i2c_port (i2c_port_t) -> BMI270::_wire (TwoWire*)
- bmi2_i2c_read/bmi2_i2c_write reimplemented on top of
  beginTransmission/write/endTransmission/requestFrom/read instead of
  i2c_master_write_read_device/i2c_master_write_to_device
- drop the file-scope static _data_buffer/_i2c_ret/_dev_info (shared
  mutable state unsafe across instances/tasks) in favor of locals in each
  callback
- add int init(TwoWire&, uint8_t); keep int init(i2c_port_t, uint8_t) as a
  deprecated back-compat overload (I2C_NUM_0 -> Wire, I2C_NUM_1 -> Wire1),
  now only needing the header-only <hal/i2c_types.h> for the i2c_port_t
  type instead of <driver/i2c.h>, so it does not reintroduce the
  i2c.c.obj link
- update examples/getSensorData to use the new Wire-based init()
- document the fix and new init() usage in README
- bump library version to 1.0.3

Not independently re-verified against a real firmware.map in this
environment (no ESP32/PlatformIO toolchain available here); please
confirm libdriver.a(i2c.c.obj) is absent from the link in your build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRT6bgzHXTPERGjx5tEHQj
The compatibility overload added with the Wire port referenced I2C_NUM_1
and Wire1 unconditionally, and the header used the global Wire as a
default member initializer. Checked against the real arduino-esp32 and
ESP-IDF headers, that breaks the build in several configurations:

- Arduino core 2.x (ESP-IDF 4.4): I2C_NUM_0/I2C_NUM_1 are macros in
  driver/i2c.h, not in hal/i2c_types.h, so the library itself failed to
  compile and existing init(I2C_NUM_0, ...) sketches lost the constant.
- ESP32-C3/C6 and other chips with one HP I2C controller: Wire1 (and
  I2C_NUM_1 on IDF 5.5) is not declared.
- Arduino core 3.3 with NO_GLOBAL_WIRE: Wire is not declared at all.

Include driver/i2c.h only on IDF 4.x, where no driver_ng exists to
conflict with; keep the header-only hal/i2c_types.h on IDF 5.x. Map
port 1 to Wire1 only on chips with a second HP I2C controller (which
implies Wire1 in every core release), guard the global instances, and
log an error otherwise. Default the TwoWire pointers to nullptr.

README: the conflict starts with Arduino core 3.2.0, where Wire moved to
the i2c_master driver (esp32-hal-i2c-ng.c, IDF >= 5.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRT6bgzHXTPERGjx5tEHQj
This was referenced Oct 15, 2026
@weekroom
weekroom merged commit 5c58234 into m5stack:main Oct 27, 2026
2 checks passed
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.

ESP3.2 conflict

3 participants