Skip to content

Fix bus and device lifecycle, legacy build, argument validation (review of 2.1.3) - #23

Merged
UncleRus merged 2 commits into
mainfrom
fix/i2cdev-review
Nov 11, 2026
Merged

UncleRus merged 2 commits into
mainfrom
fix/i2cdev-review

Conversation

@UncleRus

@UncleRus UncleRus commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Fixes every problem listed in #22: the legacy build, the bus and device lifecycle of the new driver, argument validation, and the header documentation that did not match the implementation.

Legacy build

  • driver is added to the component requirements unconditionally. The requirement list is evaluated on the first CMake pass, before sdkconfig exists, so a list derived from CONFIG_I2CDEV_USE_LEGACY_DRIVER never contains it: the legacy build failed with fatal error: driver/i2c.h. The source selector is unchanged and still follows the config.
  • i2cdev_get_shared_handle() gets a stub in i2cdev_legacy.c (ESP_ERR_NOT_SUPPORTED), otherwise the legacy build does not link: the function is declared in i2cdev.h for both implementations.

Bus lifecycle

  • i2cdev_init() drops the static bool initialized latch. The state is derived from the port mutexes, so init() / done() / init() works again, and a port with a live bus is not reset.
  • A failed teardown keeps the state instead of dropping it: bus_handle, installed, dev->dev_handle and the device list survive an error from i2c_del_master_bus() / i2c_master_bus_rm_device(), so the call can be repeated. i2cdev_done() now returns the first error and keeps partially released ports for the next attempt.
  • i2cdev_done() never deletes a port mutex it failed to take.
  • The bus lives from the first operation until i2cdev_done(). It is no longer deleted when the last i2cdev device is removed, because the handle can be shared with external components through i2cdev_get_shared_handle().
  • The port device array is replaced by a per-port list: calloc() per registration, free() on removal, the whole list is released in init() and done(). The 8-device limit is gone; CONFIG_I2CDEV_MAX_DEVICES_PER_PORT is now documented as legacy-only.

Device lifecycle

  • Devices are registered in i2c_setup_device() under the port mutex, right after i2c_master_bus_add_device(). Registration failure fails the operation and removes the device from the bus, so a device can no longer be on the bus without being tracked.
  • dev->dev_handle is checked under the port mutex, which closes the race where two tasks added the same device twice (the IDF driver does not detect duplicates).
  • The retry loop no longer recreates the device handle on ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE. That path ran without the port mutex, leaked the device slot, and the trigger condition was wrong.
  • i2c_dev_create_mutex() creates the mutex only (and normalizes addr_bit_len); i2c_dev_delete_mutex() performs the bus-side cleanup in both locking modes, so CONFIG_I2CDEV_NOLOCK now disables the per-device mutex only.

Validation

  • Port numbers below zero are rejected before they index internal arrays.
  • sda_pin == scl_pin is rejected with ESP_ERR_INVALID_ARG (a zero-initialized descriptor used GPIO0 for both lines).
  • sda_pullup_en / scl_pullup_en are applied per line: the bus config has a single flag, so the mixed case enables the pullup on one GPIO after the bus is created.
  • i2c_dev_check_present() returns ESP_ERR_INVALID_STATE for an uninitialized port.
  • i2c_dev_read_reg() / i2c_dev_write_reg() reject NULL buffers and zero sizes, matching the legacy implementation.
  • i2c_dev_read() selects the transaction from the whole (out_data, out_size) pair again: a half-filled pair is treated as absent.
  • #include <stdlib.h> added for the allocator functions.

Header documentation

  • The docstrings were checked against the implementation: return codes of every function, retry policy, lazy bus and handle creation, side effects of check_present() / probe(), descriptor lifetime, macro requirements (I2C_DEV_GIVE_MUTEX() after a successful I2C_DEV_CHECK(), TAG for I2C_DEV_CHECK_LOGE()), and the cleanup order for a shared bus (external device -> i2c_dev_delete_mutex() -> i2cdev_done()).
  • Dead fallbacks for CONFIG_FREERTOS_HZ and CONFIG_LOG_MAXIMUM_LEVEL removed, dead field clk_flags marked as ignored, I2CDEV_MAX_STRETCH_TIME documented as legacy-only, ASCII sections instead of box drawing (broken in generated docs).

Behaviour changes

  • A failed i2c_dev_delete_mutex() / free_desc() can now return a driver error instead of ESP_OK.
  • The bus is not released when the last device is removed; i2cdev_done() releases it.
  • CONFIG_I2CDEV_NOLOCK=1 no longer disables device removal and bus teardown.
  • Re-registration limit removed; a port accepts any number of devices until memory runs out.

Testing

  • ESP-IDF v6.1 (v6.1-671-g9bf0c0eb952), target esp32, no warnings (-Werror): new driver, CONFIG_I2CDEV_NOLOCK=1, legacy configuration, and the example.
  • All 71 esp-idf-lib components that use i2cdev build against this branch.
  • On the board (BMP180 at 0x77 and HD44780 via PCF8574 at 0x27 on one bus): concurrent access from two tasks without errors, 16 descriptors on one port (previously the 8-device limit), negative port, same-pin and argument checks, teardown, i2cdev_done() / i2cdev_init() / repeated use, shared bus handle and external component lifecycle, invalid read_reg arguments.

Legacy build:
- add `driver` to component requirements unconditionally: the requirement
  list is evaluated on the first CMake pass, before sdkconfig exists, so a
  list derived from CONFIG_I2CDEV_USE_LEGACY_DRIVER never contains it
- add i2cdev_get_shared_handle() stub for the legacy driver so the API
  declared in i2cdev.h links

i2cdev.c:
- i2cdev_init() no longer keeps a static "initialized" latch: state is
  derived from the port mutexes, so init/done/init works again
- keep the bus and the device handle when a teardown step fails instead of
  dropping them: i2c_del_master_bus() refusing to run left the port occupied
  until reboot; i2cdev_done() now retries partially released ports
- i2cdev_done() does not delete a port mutex it failed to take
- drop per-device handle recreation in the retry loop: it ran without the
  port mutex, without releasing the device slot and leaked the handle
- register devices under the port mutex in i2c_setup_device() and fail the
  operation if registration is impossible
- replace the fixed device array with a per-port list, drop the per-port
  device limit, release the list in init/done
- the bus now lives from the first operation until i2cdev_done(): deleting
  it on the last i2cdev device broke external users of the shared bus
- take the port mutex before checking dev->dev_handle (two tasks could add
  the same device twice, the IDF driver does not detect duplicates)
- reject negative port numbers before they index internal arrays
- reject descriptors whose SDA and SCL pins are the same GPIO
- apply per-line pullups individually: the bus config has a single flag
- return ESP_ERR_INVALID_ARG from read_reg/write_reg for empty buffers
- return ESP_ERR_INVALID_STATE from i2c_dev_check_present() when the port is
  not initialized
- keep CONFIG_I2CDEV_NOLOCK limited to per-device mutexes; bus lifecycle
  works in both modes
- replace direct pointer comparisons with NULL by the short form
@UncleRus
UncleRus merged commit 947e973 into main Nov 11, 2026
55 checks passed
@UncleRus
UncleRus deleted the fix/i2cdev-review branch November 11, 2026 23:04
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.

1 participant