diff --git a/components/i2cdev/.eil.yml b/components/i2cdev/.eil.yml index add591c7..ad57b3e8 100644 --- a/components/i2cdev/.eil.yml +++ b/components/i2cdev/.eil.yml @@ -1,7 +1,6 @@ ---- name: i2cdev description: ESP-IDF I2C master thread-safe utilities -version: 1.5.0 +version: 2.0.0 groups: - common code_owners: @@ -26,4 +25,4 @@ targets: license: MIT copyrights: - name: UncleRus - year: 2018 + year: 2018 \ No newline at end of file diff --git a/components/i2cdev/CMakeLists.txt b/components/i2cdev/CMakeLists.txt index 585cfb20..8c50d5b3 100644 --- a/components/i2cdev/CMakeLists.txt +++ b/components/i2cdev/CMakeLists.txt @@ -1,11 +1,37 @@ -if(${IDF_TARGET} STREQUAL esp8266) - set(req esp8266 freertos esp_idf_lib_helpers) +# ESP-IDF CMake component for i2cdev library +set(req driver freertos esp_idf_lib_helpers) + +# ESP-IDF version detection for automatic driver selection +# Check for manual override via Kconfig +if(CONFIG_I2CDEV_USE_LEGACY_DRIVER) + set(USE_LEGACY_DRIVER TRUE) + message(STATUS "i2cdev: Manual override - using legacy driver (CONFIG_I2CDEV_USE_LEGACY_DRIVER=y)") +elseif(NOT DEFINED IDF_VERSION_MAJOR) + # In case older ESP-IDF versions that don't define IDF_VERSION_MAJOR + set(USE_LEGACY_DRIVER TRUE) + message(STATUS "i2cdev: IDF_VERSION_MAJOR not defined, using legacy driver") +elseif(IDF_VERSION_MAJOR LESS 5) + set(USE_LEGACY_DRIVER TRUE) + message(STATUS "i2cdev: ESP-IDF v${IDF_VERSION_MAJOR}.x detected, using legacy driver") +elseif(IDF_VERSION_MAJOR EQUAL 5 AND IDF_VERSION_MINOR LESS 3) + set(USE_LEGACY_DRIVER TRUE) + message(STATUS "i2cdev: ESP-IDF v${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR} detected, using legacy driver") else() - set(req driver freertos esp_idf_lib_helpers) + set(USE_LEGACY_DRIVER FALSE) + message(STATUS "i2cdev: ESP-IDF v${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR} detected, using new i2c_master driver") endif() -idf_component_register( - SRCS i2cdev.c - INCLUDE_DIRS . - REQUIRES ${req} -) +# Conditionally set the source file based on version detection or Kconfig override +if(USE_LEGACY_DRIVER) + set(SRCS "i2cdev_legacy.c") + message(STATUS "i2cdev: Compiling with legacy I2C driver (i2cdev_legacy.c)") +else() + set(SRCS "i2cdev.c") + message(STATUS "i2cdev: Compiling with new I2C master driver (i2cdev.c)") +endif() + +# Register the component +idf_component_register(SRCS ${SRCS} + INCLUDE_DIRS "." + REQUIRES ${req}) + diff --git a/components/i2cdev/Kconfig b/components/i2cdev/Kconfig index aaff31d3..74aa9670 100644 --- a/components/i2cdev/Kconfig +++ b/components/i2cdev/Kconfig @@ -1,4 +1,52 @@ -menu "I2C" +menu "I2C Device Library" + +config I2CDEV_USE_LEGACY_DRIVER + bool "Use Legacy I2C Driver API" + default n + help + Select this option to use the older ESP-IDF I2C driver API (driver/i2c.h) + instead of the newer driver API (driver/i2c_master.h). + + This is automatically determined by the build system based on your ESP-IDF version. + For ESP-IDF versions prior to v5.3, the legacy driver will be used automatically. + You can manually override this setting if needed. + +config I2CDEV_AUTO_ENABLE_PULLUPS + bool "Automatically enable internal I2C pullups when not configured" + default n + depends on !IDF_TARGET_ESP8266 + help + When enabled, internal pullup resistors are automatically enabled + when both sda_pullup_en and scl_pullup_en are false (default state). + + Useful for development and prototyping. Disable for production + systems with external pullups to avoid interference. + + Considerations: + - May increase power consumption slightly + - Could interfere with carefully tuned external pullups + - Not recommended for battery-powered applications + + Note: This option only affects the modern i2cdev driver (ESP32 family). + Legacy driver behavior is unchanged for compatibility. + +config I2CDEV_DEFAULT_SDA_PIN + int "Default I2C SDA pin" + default 21 + help + Default SDA pin for I2C devices. + +config I2CDEV_DEFAULT_SCL_PIN + int "Default I2C SCL pin" + default 22 + help + Default SCL pin for I2C devices. + +config I2CDEV_MAX_DEVICES_PER_PORT + int "Maximum number of devices per I2C port" + default 8 + help + Maximum number of devices that can be registered on a single I2C port. config I2CDEV_TIMEOUT int "I2C transaction timeout, milliseconds" @@ -13,5 +61,5 @@ config I2CDEV_NOLOCK drivers will become non-thread safe. Use this option if you need to access your I2C devices from interrupt handlers. - + endmenu \ No newline at end of file diff --git a/components/i2cdev/component.mk b/components/i2cdev/component.mk index 877b269f..2f812ff7 100644 --- a/components/i2cdev/component.mk +++ b/components/i2cdev/component.mk @@ -2,6 +2,28 @@ COMPONENT_ADD_INCLUDEDIRS = . ifdef CONFIG_IDF_TARGET_ESP8266 COMPONENT_DEPENDS = esp8266 freertos esp_idf_lib_helpers +# ESP8266 RTOS SDK auto-detects all .c files, so use COMPONENT_OBJS to override +# This prevents both i2cdev.c and i2cdev_legacy.c from being compiled +COMPONENT_OBJS := i2cdev_legacy.o +COMPONENT_SRCDIRS := . else COMPONENT_DEPENDS = driver freertos esp_idf_lib_helpers +# For ESP32 family, check for manual override first +ifdef CONFIG_I2CDEV_USE_LEGACY_DRIVER +COMPONENT_SRCS = i2cdev_legacy.c +else +# Check if version variables are available, fallback to legacy if not +ifdef IDF_VERSION_MAJOR +ifeq ($(shell test $(IDF_VERSION_MAJOR) -lt 5 && echo 1),1) +COMPONENT_SRCS = i2cdev_legacy.c +else ifeq ($(shell test $(IDF_VERSION_MAJOR) -eq 5 -a $(IDF_VERSION_MINOR) -lt 3 && echo 1),1) +COMPONENT_SRCS = i2cdev_legacy.c +else +COMPONENT_SRCS = i2cdev.c +endif +else +# Version variables not available - fallback to legacy driver for safety +COMPONENT_SRCS = i2cdev_legacy.c +endif +endif endif diff --git a/components/i2cdev/i2cdev.c b/components/i2cdev/i2cdev.c index 7f1bba28..6fe774ab 100644 --- a/components/i2cdev/i2cdev.c +++ b/components/i2cdev/i2cdev.c @@ -26,117 +26,243 @@ * * ESP-IDF I2C master thread-safe functions for communication with I2C slave * - * Copyright (c) 2018 Ruslan V. Uss + * Copyright (C) 2018 Ruslan V. Uss + * Updated 2025 by quinkq to use newer ESP-IDF I2C master driver API * * MIT Licensed as described in the file LICENSE */ -#include -#include + +#include "i2cdev.h" +#include +#include #include #include -#include -#include "i2cdev.h" +#include +#include static const char *TAG = "i2cdev"; -typedef struct { - SemaphoreHandle_t lock; - i2c_config_t config; - bool installed; -} i2c_port_state_t; - -static i2c_port_state_t states[I2C_NUM_MAX]; - -#if CONFIG_I2CDEV_NOLOCK -#define SEMAPHORE_TAKE(port) -#else -#define SEMAPHORE_TAKE(port) do { \ - if (!xSemaphoreTake(states[port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT))) \ - { \ - ESP_LOGE(TAG, "Could not take port mutex %d", port); \ - return ESP_ERR_TIMEOUT; \ - } \ - } while (0) +// Fallback definition for platforms without 10-bit address support +#ifndef I2C_ADDR_BIT_LEN_10 +#define I2C_ADDR_BIT_LEN_10 1 #endif -#if CONFIG_I2CDEV_NOLOCK -#define SEMAPHORE_GIVE(port) -#else -#define SEMAPHORE_GIVE(port) do { \ - if (!xSemaphoreGive(states[port].lock)) \ - { \ - ESP_LOGE(TAG, "Could not give port mutex %d", port); \ - return ESP_FAIL; \ - } \ - } while (0) -#endif +#define I2C_DEFAULT_FREQ_HZ 400000 +#define I2C_MAX_RETRIES 3 +#define I2C_RETRY_BASE_DELAY_MS 20 +#define I2CDEV_MAX_STACK_ALLOC_SIZE 32 // Stack allocation threshold to avoid heap fragmentation for small buffers -esp_err_t i2cdev_init() +typedef struct { - memset(states, 0, sizeof(states)); + SemaphoreHandle_t lock; // Mutex for exclusive access to this port's state + i2c_master_bus_handle_t bus_handle; // Handle to the initialized I2C master bus + bool installed; // Flag indicating if the bus for this port has been installed + uint32_t ref_count; // Number of devices currently active on this bus port + int sda_pin_current; // Actual SDA pin the bus was initialized with + int scl_pin_current; // Actual SCL pin the bus was initialized with +} i2c_port_state_t; -#if !CONFIG_I2CDEV_NOLOCK - for (int i = 0; i < I2C_NUM_MAX; i++) +static i2c_port_state_t i2c_ports[I2C_NUM_MAX] = { 0 }; +static i2c_dev_t *active_devices[I2C_NUM_MAX][CONFIG_I2CDEV_MAX_DEVICES_PER_PORT] = { { NULL } }; + +// Helper to register a device +static esp_err_t register_device(i2c_dev_t *dev) +{ + if (!dev) + return ESP_ERR_INVALID_ARG; + int port = dev->port; + if (port >= I2C_NUM_MAX) + return ESP_ERR_INVALID_ARG; + + // Note: Port mutex should be held by caller + for (int i = 0; i < CONFIG_I2CDEV_MAX_DEVICES_PER_PORT; i++) { - states[i].lock = xSemaphoreCreateMutex(); - if (!states[i].lock) + if (active_devices[port][i] == NULL) { - ESP_LOGE(TAG, "Could not create port mutex %d", i); - return ESP_FAIL; + active_devices[port][i] = dev; + ESP_LOGV(TAG, "[0x%02x at %d] Registered device in slot %d", dev->addr, port, i); + return ESP_OK; } } -#endif + ESP_LOGE(TAG, "[0x%02x at %d] No free slots to register device - limit reached", dev->addr, port); + return ESP_ERR_NO_MEM; +} - return ESP_OK; +// Helper to deregister a device +static void deregister_device(i2c_dev_t *dev) +{ + if (!dev) + return; + int port = dev->port; + if (port >= I2C_NUM_MAX) + return; + for (int i = 0; i < CONFIG_I2CDEV_MAX_DEVICES_PER_PORT; i++) + { + if (active_devices[port][i] == dev) + { + active_devices[port][i] = NULL; + ESP_LOGV(TAG, "[0x%02x at %d] Deregistered device from slot %d", dev->addr, port, i); + return; + } + } } -esp_err_t i2cdev_done() +esp_err_t i2cdev_init(void) { + ESP_LOGV(TAG, "Initializing I2C subsystem..."); + memset(active_devices, 0, sizeof(active_devices)); for (int i = 0; i < I2C_NUM_MAX; i++) { - if (!states[i].lock) continue; - - if (states[i].installed) + if (!i2c_ports[i].lock) { - SEMAPHORE_TAKE(i); - i2c_driver_delete(i); - states[i].installed = false; - SEMAPHORE_GIVE(i); + i2c_ports[i].lock = xSemaphoreCreateMutex(); + if (!i2c_ports[i].lock) + { + ESP_LOGE(TAG, "Could not create port mutex %d", i); + return ESP_ERR_NO_MEM; + } + ESP_LOGV(TAG, "Created port mutex %d", i); } -#if !CONFIG_I2CDEV_NOLOCK - vSemaphoreDelete(states[i].lock); -#endif - states[i].lock = NULL; + i2c_ports[i].installed = false; + i2c_ports[i].ref_count = 0; + i2c_ports[i].bus_handle = NULL; + i2c_ports[i].sda_pin_current = -1; + i2c_ports[i].scl_pin_current = -1; } + ESP_LOGV(TAG, "I2C subsystem initialized."); return ESP_OK; } esp_err_t i2c_dev_create_mutex(i2c_dev_t *dev) { #if !CONFIG_I2CDEV_NOLOCK - if (!dev) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; - ESP_LOGV(TAG, "[0x%02x at %d] creating mutex", dev->addr, dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] Creating device mutex...", dev->addr, dev->port); + if (dev->mutex) + { + ESP_LOGW(TAG, "[0x%02x at %d] device mutex already exists (Handle: %p)", dev->addr, dev->port, dev->mutex); + return ESP_OK; // Already created + } dev->mutex = xSemaphoreCreateMutex(); if (!dev->mutex) { ESP_LOGE(TAG, "[0x%02x at %d] Could not create device mutex", dev->addr, dev->port); - return ESP_FAIL; + return ESP_ERR_NO_MEM; // Use ESP_ERR_NO_MEM for memory allocation failures } -#endif + ESP_LOGV(TAG, "[0x%02x at %d] Device mutex created (Handle: %p)", dev->addr, dev->port, dev->mutex); + + // Register the device for cleanup tracking (under port mutex for consistency) + if (dev->port < I2C_NUM_MAX && i2c_ports[dev->port].lock) + { + if (xSemaphoreTake(i2c_ports[dev->port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) == pdTRUE) + { + esp_err_t reg_res = register_device(dev); + if (reg_res != ESP_OK) + { + ESP_LOGW(TAG, "[0x%02x at %d] Failed to register device: %s - device will work but cleanup tracking disabled", dev->addr, dev->port, esp_err_to_name(reg_res)); + // Continue - device can still function without registration tracking + } + else + { + ESP_LOGV(TAG, "[0x%02x at %d] Device registered successfully for cleanup tracking", dev->addr, dev->port); + } + xSemaphoreGive(i2c_ports[dev->port].lock); + } + else + { + ESP_LOGW(TAG, "[0x%02x at %d] Could not take port mutex for device registration", dev->addr, dev->port); + // Continue - device can still function without registration tracking + } + } + + // Set default address bit length if not explicitly set + if (dev->addr_bit_len != I2C_ADDR_BIT_LEN_7 && dev->addr_bit_len != I2C_ADDR_BIT_LEN_10) + { + ESP_LOGV(TAG, "[0x%02x at %d] Setting default 7-bit address format", dev->addr, dev->port); + dev->addr_bit_len = I2C_ADDR_BIT_LEN_7; + } +#else + ESP_LOGV(TAG, "[0x%02x at %d] Mutex creation skipped (CONFIG_I2CDEV_NOLOCK=1)", dev->addr, dev->port); +#endif return ESP_OK; } esp_err_t i2c_dev_delete_mutex(i2c_dev_t *dev) { #if !CONFIG_I2CDEV_NOLOCK - if (!dev) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Deleting device mutex and cleaning up resources", dev->addr, dev->port); + + // Remove device from bus if handle exists + if (dev->dev_handle) + { + ESP_LOGV(TAG, "[0x%02x at %d] Removing device handle %p from bus", dev->addr, dev->port, dev->dev_handle); + esp_err_t rm_res = i2c_master_bus_rm_device((i2c_master_dev_handle_t)dev->dev_handle); + if (rm_res != ESP_OK) + { + ESP_LOGW(TAG, "[0x%02x at %d] Failed to remove device handle: %s", dev->addr, dev->port, esp_err_to_name(rm_res)); + // Continue with cleanup despite error + } + dev->dev_handle = NULL; + } - ESP_LOGV(TAG, "[0x%02x at %d] deleting mutex", dev->addr, dev->port); + // Deregister the device + deregister_device(dev); - vSemaphoreDelete(dev->mutex); + // Update port reference count if port is valid + if (dev->port < I2C_NUM_MAX) + { + if (xSemaphoreTake(i2c_ports[dev->port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) == pdTRUE) + { + if (i2c_ports[dev->port].installed && i2c_ports[dev->port].ref_count > 0) + { + i2c_ports[dev->port].ref_count--; + ESP_LOGV(TAG, "[Port %d] Decremented ref_count to %" PRIu32, dev->port, i2c_ports[dev->port].ref_count); + + // If last device on this port, delete the bus + if (i2c_ports[dev->port].ref_count == 0) + { + ESP_LOGI(TAG, "[Port %d] Last device removed, cleaning up THIS port's bus", dev->port); + // Just clean up this port's bus + if (i2c_ports[dev->port].bus_handle) + { + ESP_LOGI(TAG, "[Port %d] Deleting bus handle %p", dev->port, i2c_ports[dev->port].bus_handle); + esp_err_t del_bus_res = i2c_del_master_bus(i2c_ports[dev->port].bus_handle); + if (del_bus_res != ESP_OK) + { + ESP_LOGE(TAG, "[Port %d] Failed to delete master bus: %s", dev->port, esp_err_to_name(del_bus_res)); + } + i2c_ports[dev->port].bus_handle = NULL; + } + i2c_ports[dev->port].installed = false; + i2c_ports[dev->port].sda_pin_current = -1; + i2c_ports[dev->port].scl_pin_current = -1; + } + } + xSemaphoreGive(i2c_ports[dev->port].lock); + } + else + { + ESP_LOGW(TAG, "[0x%02x at %d] Could not take port mutex for ref_count update", dev->addr, dev->port); + } + } + + // Delete the mutex itself last + if (dev->mutex) + { + vSemaphoreDelete(dev->mutex); + dev->mutex = NULL; + } + else + { + ESP_LOGV(TAG, "[0x%02x at %d] Device mutex was NULL, nothing to delete", dev->addr, dev->port); + } #endif return ESP_OK; } @@ -144,15 +270,26 @@ esp_err_t i2c_dev_delete_mutex(i2c_dev_t *dev) esp_err_t i2c_dev_take_mutex(i2c_dev_t *dev) { #if !CONFIG_I2CDEV_NOLOCK - if (!dev) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; - ESP_LOGV(TAG, "[0x%02x at %d] taking mutex", dev->addr, dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] Attempting to take device mutex (Handle: %p)...", dev->addr, dev->port, dev->mutex); + if (!dev->mutex) + { + ESP_LOGE(TAG, "[0x%02x at %d] Attempt to take NULL device mutex!", dev->addr, dev->port); + return ESP_ERR_INVALID_STATE; // Mutex doesn't exist + } - if (!xSemaphoreTake(dev->mutex, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT))) + TickType_t timeout_ticks = pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT); + ESP_LOGV(TAG, "[0x%02x at %d] Taking device mutex with timeout %d ms (%lu ticks)", dev->addr, dev->port, CONFIG_I2CDEV_TIMEOUT, (unsigned long)timeout_ticks); + if (!xSemaphoreTake(dev->mutex, timeout_ticks)) { - ESP_LOGE(TAG, "[0x%02x at %d] Could not take device mutex", dev->addr, dev->port); + ESP_LOGE(TAG, "[0x%02x at %d] Could not take device mutex (Timeout after %d ms)", dev->addr, dev->port, CONFIG_I2CDEV_TIMEOUT); return ESP_ERR_TIMEOUT; } + ESP_LOGV(TAG, "[0x%02x at %d] Device mutex taken successfully.", dev->addr, dev->port); +#else + ESP_LOGV(TAG, "[0x%02x at %d] Mutex take skipped (CONFIG_I2CDEV_NOLOCK=1)", dev->addr, dev->port); #endif return ESP_OK; } @@ -160,181 +297,598 @@ esp_err_t i2c_dev_take_mutex(i2c_dev_t *dev) esp_err_t i2c_dev_give_mutex(i2c_dev_t *dev) { #if !CONFIG_I2CDEV_NOLOCK - if (!dev) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; - ESP_LOGV(TAG, "[0x%02x at %d] giving mutex", dev->addr, dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] Giving device mutex (Handle: %p)...", dev->addr, dev->port, dev->mutex); + if (!dev->mutex) + { + ESP_LOGE(TAG, "[0x%02x at %d] Attempt to give NULL device mutex!", dev->addr, dev->port); + return ESP_ERR_INVALID_STATE; + } if (!xSemaphoreGive(dev->mutex)) { - ESP_LOGE(TAG, "[0x%02x at %d] Could not give device mutex", dev->addr, dev->port); + // This case should ideally not happen if the mutex was taken correctly + ESP_LOGE(TAG, "[0x%02x at %d] Could not give device mutex (Was it taken?) (Handle: %p)", dev->addr, dev->port, dev->mutex); return ESP_FAIL; } + ESP_LOGV(TAG, "[0x%02x at %d] Device mutex given successfully.", dev->addr, dev->port); +#else + ESP_LOGV(TAG, "[0x%02x at %d] Mutex give skipped (CONFIG_I2CDEV_NOLOCK=1)", dev->addr, dev->port); #endif return ESP_OK; } -inline static bool cfg_equal(const i2c_config_t *a, const i2c_config_t *b) +// i2c_setup_port: Initializes the I2C master bus for a given port if not already done. +// It uses pin configurations from dev->cfg.sda_io_num and dev->cfg.scl_io_num. +// The pins for a port are fixed after the first device initializes it. +static esp_err_t i2c_setup_port(i2c_dev_t *dev) // dev is non-const to update dev->sda_pin, dev->scl_pin { - return a->scl_io_num == b->scl_io_num - && a->sda_io_num == b->sda_io_num -#if HELPER_TARGET_IS_ESP32 - && a->master.clk_speed == b->master.clk_speed -#elif HELPER_TARGET_IS_ESP8266 - && ((a->clk_stretch_tick && a->clk_stretch_tick == b->clk_stretch_tick) - || (!a->clk_stretch_tick && b->clk_stretch_tick == I2CDEV_MAX_STRETCH_TIME) - ) // see line 232 -#endif - && a->scl_pullup_en == b->scl_pullup_en - && a->sda_pullup_en == b->sda_pullup_en; -} + if (!dev) + return ESP_ERR_INVALID_ARG; + if (dev->port >= I2C_NUM_MAX) + { + ESP_LOGE(TAG, "Invalid I2C port number: %d", dev->port); + return ESP_ERR_INVALID_ARG; + } -static esp_err_t i2c_setup_port(const i2c_dev_t *dev) -{ - if (dev->port >= I2C_NUM_MAX) return ESP_ERR_INVALID_ARG; + esp_err_t res = ESP_OK; + i2c_port_state_t *port_state = &i2c_ports[dev->port]; - esp_err_t res; - if (!cfg_equal(&dev->cfg, &states[dev->port].config) || !states[dev->port].installed) + ESP_LOGV(TAG, "[Port %d] Setup request for device 0x%02x", dev->port, dev->addr); + if (xSemaphoreTake(port_state->lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) != pdTRUE) + { + ESP_LOGE(TAG, "[Port %d] Could not take port mutex for setup", dev->port); + return ESP_ERR_TIMEOUT; + } + + if (!port_state->installed) { - ESP_LOGD(TAG, "Reconfiguring I2C driver on port %d", dev->port); - i2c_config_t temp; - memcpy(&temp, &dev->cfg, sizeof(i2c_config_t)); - temp.mode = I2C_MODE_MASTER; + // Pin Selection Logic: Use device-specified pins, fallback to Kconfig defaults if -1 + gpio_num_t sda_pin = (dev->cfg.sda_io_num == (gpio_num_t)-1) ? (gpio_num_t)CONFIG_I2CDEV_DEFAULT_SDA_PIN : dev->cfg.sda_io_num; + gpio_num_t scl_pin = (dev->cfg.scl_io_num == (gpio_num_t)-1) ? (gpio_num_t)CONFIG_I2CDEV_DEFAULT_SCL_PIN : dev->cfg.scl_io_num; - // Driver reinstallation - if (states[dev->port].installed) + // Validate pins (basic check, gpio_is_valid_gpio could be used for more robust check) + if (sda_pin < 0 || scl_pin < 0) { - i2c_driver_delete(dev->port); - states[dev->port].installed = false; + ESP_LOGE(TAG, "[Port %d] Invalid SCL/SDA pins: SDA=%d, SCL=%d. Check driver or Kconfig defaults.", dev->port, sda_pin, scl_pin); + xSemaphoreGive(port_state->lock); + return ESP_ERR_INVALID_ARG; + } + + /* + * OPTIONAL I2C PULLUP AUTO-CONFIGURATION + * + * By default: Uses whatever sda_pullup_en/scl_pullup_en you set (usually false) + * + * When CONFIG_I2CDEV_AUTO_ENABLE_PULLUPS=y: If both pullup flags are false, + * automatically change them to true to enable internal pullups (~45kΩ). + * + * Manual pullup configuration: + * - Set sda_pullup_en=true, scl_pullup_en=true for internal pullups + * - Set sda_pullup_en=false, scl_pullup_en=false for external pullups + */ + + // Read user's pullup configuration (default false if not set) + bool sda_pullup = dev->cfg.sda_pullup_en; + bool scl_pullup = dev->cfg.scl_pullup_en; + +#if CONFIG_I2CDEV_AUTO_ENABLE_PULLUPS + // CONFIG_I2CDEV_AUTO_ENABLE_PULLUPS=y: If user didn't configure pullups, enable them automatically + if (!sda_pullup && !scl_pullup) + { + sda_pullup = true; + scl_pullup = true; + ESP_LOGI(TAG, "[Port %d] Auto-enabling internal pullups (CONFIG_I2CDEV_AUTO_ENABLE_PULLUPS=y)", dev->port); } -#if HELPER_TARGET_IS_ESP32 -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) - // See https://github.com/espressif/esp-idf/issues/10163 - if ((res = i2c_driver_install(dev->port, temp.mode, 0, 0, 0)) != ESP_OK) - return res; - if ((res = i2c_param_config(dev->port, &temp)) != ESP_OK) - return res; -#else - if ((res = i2c_param_config(dev->port, &temp)) != ESP_OK) - return res; - if ((res = i2c_driver_install(dev->port, temp.mode, 0, 0, 0)) != ESP_OK) - return res; -#endif -#endif -#if HELPER_TARGET_IS_ESP8266 - // Clock Stretch time, depending on CPU frequency - temp.clk_stretch_tick = dev->timeout_ticks ? dev->timeout_ticks : I2CDEV_MAX_STRETCH_TIME; - if ((res = i2c_driver_install(dev->port, temp.mode)) != ESP_OK) - return res; - if ((res = i2c_param_config(dev->port, &temp)) != ESP_OK) - return res; #endif - states[dev->port].installed = true; - memcpy(&states[dev->port].config, &temp, sizeof(i2c_config_t)); - ESP_LOGD(TAG, "I2C driver successfully reconfigured on port %d", dev->port); + ESP_LOGI(TAG, + "[Port %d] First initialization. Configuring bus with SDA=%d, SCL=%d (Pullups " + "SCL:%d SDA:%d)", + dev->port, sda_pin, scl_pin, scl_pullup, sda_pullup); + + i2c_master_bus_config_t bus_config = { + .i2c_port = dev->port, + .sda_io_num = sda_pin, + .scl_io_num = scl_pin, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = (sda_pullup || scl_pullup), + // Bus speed is not set here. It's per-device or a global target for the bus can be set + // if desired, but i2c_master supports per-device speeds. + }; + + res = i2c_new_master_bus(&bus_config, &port_state->bus_handle); + if (res == ESP_OK) + { + port_state->installed = true; + port_state->ref_count = 0; // Will be incremented when a device is successfully added + port_state->sda_pin_current = sda_pin; + port_state->scl_pin_current = scl_pin; + dev->sda_pin = sda_pin; // Update dev struct with actual pins used + dev->scl_pin = scl_pin; + ESP_LOGI(TAG, "[Port %d] Successfully installed I2C master bus (Handle: %p).", dev->port, port_state->bus_handle); + } + else + { + ESP_LOGE(TAG, "[Port %d] Failed to create master bus: %d (%s)", dev->port, res, esp_err_to_name(res)); + port_state->installed = false; + port_state->bus_handle = NULL; + port_state->sda_pin_current = -1; + port_state->scl_pin_current = -1; + } } -#if HELPER_TARGET_IS_ESP32 - int t; - if ((res = i2c_get_timeout(dev->port, &t)) != ESP_OK) - return res; - // Timeout cannot be 0 - uint32_t ticks = dev->timeout_ticks ? dev->timeout_ticks : I2CDEV_MAX_STRETCH_TIME; - if ((ticks != t) && (res = i2c_set_timeout(dev->port, ticks)) != ESP_OK) - return res; - ESP_LOGD(TAG, "Timeout: ticks = %" PRIu32 " (%" PRIu32 " usec) on port %d", dev->timeout_ticks, dev->timeout_ticks / 80, dev->port); -#endif + else + { + ESP_LOGV(TAG, "[Port %d] Port already installed (SDA=%d, SCL=%d, Handle: %p).", dev->port, port_state->sda_pin_current, port_state->scl_pin_current, port_state->bus_handle); + // Pin Consistency Check: For subsequent devices, ensure pins match already-configured bus + gpio_num_t sda_desired = (dev->cfg.sda_io_num == (gpio_num_t)-1) ? (gpio_num_t)port_state->sda_pin_current : dev->cfg.sda_io_num; + gpio_num_t scl_desired = (dev->cfg.scl_io_num == (gpio_num_t)-1) ? (gpio_num_t)port_state->scl_pin_current : dev->cfg.scl_io_num; - return ESP_OK; + if (sda_desired != port_state->sda_pin_current || scl_desired != port_state->scl_pin_current) + { + ESP_LOGE(TAG, + "[Port %d] Pin mismatch for device 0x%02x! Bus on SDA=%d,SCL=%d. Device wants " + "SDA=%d,SCL=%d", + dev->port, dev->addr, port_state->sda_pin_current, port_state->scl_pin_current, sda_desired, scl_desired); + res = ESP_ERR_INVALID_STATE; // Cannot change pins for an installed bus + } + else + { + dev->sda_pin = port_state->sda_pin_current; // Update dev struct with actual pins used + dev->scl_pin = port_state->scl_pin_current; + } + // ref_count is managed by i2c_setup_device when adding/removing device handles + } + + xSemaphoreGive(port_state->lock); + ESP_LOGV(TAG, "[Port %d] Port setup finished with res %d.", dev->port, res); + return res; } -esp_err_t i2c_dev_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type) +// i2c_setup_device: Ensures port is set up and adds the device to the bus if not already added. +// It also registers the device in active_devices for cleanup purposes. +static esp_err_t i2c_setup_device(i2c_dev_t *dev) // dev is non-const - modifies dev->dev_handle, dev->addr_bit_len { - if (!dev) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; - SEMAPHORE_TAKE(dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] Setting up device context...", dev->addr, dev->port); esp_err_t res = i2c_setup_port(dev); - if (res == ESP_OK) + if (res != ESP_OK) { - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, dev->addr << 1 | (operation_type == I2C_DEV_READ ? 1 : 0), true); - i2c_master_stop(cmd); + ESP_LOGE(TAG, "[0x%02x at %d] Port setup failed during device setup: %d (%s)", dev->addr, dev->port, res, esp_err_to_name(res)); + return res; + } - res = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); + // If addr_bit_len is not set (e.g. 0, which is invalid for i2c_addr_bit_len_t enum), default to + // 7-bit. Modified to conditionally check for I2C_ADDR_BIT_LEN_10 based on hardware support + if (dev->addr_bit_len != I2C_ADDR_BIT_LEN_7 +#if SOC_I2C_SUPPORT_10BIT_ADDR + && dev->addr_bit_len != I2C_ADDR_BIT_LEN_10 +#endif + ) + { + ESP_LOGD(TAG, "[0x%02x at %d] addr_bit_len not explicitly set, defaulting to 7-bit.", dev->addr, dev->port); + dev->addr_bit_len = I2C_ADDR_BIT_LEN_7; + } - i2c_cmd_link_delete(cmd); + // Only warn about address size if the device is actually using 10-bit addressing + if (dev->addr_bit_len == I2C_ADDR_BIT_LEN_7 && dev->addr > 0x7F) + { + ESP_LOGW(TAG, + "[0x%02x at %d] Device address > 0x7F but addr_bit_len is 7-bit. Ensure address " + "is correct.", + dev->addr, dev->port); + } + +#if !defined(SOC_I2C_SUPPORT_10BIT_ADDR) || !SOC_I2C_SUPPORT_10BIT_ADDR + // On platforms without 10-bit support, force 7-bit addressing regardless of user setting + if (dev->addr_bit_len == I2C_ADDR_BIT_LEN_10) + { + ESP_LOGW(TAG, "[0x%02x at %d] 10-bit addressing not supported on this platform, forcing 7-bit mode", dev->addr, dev->port); + dev->addr_bit_len = I2C_ADDR_BIT_LEN_7; } +#endif - SEMAPHORE_GIVE(dev->port); + if (dev->dev_handle == NULL) + { + i2c_port_state_t *port_state = &i2c_ports[dev->port]; + if (xSemaphoreTake(port_state->lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) != pdTRUE) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not take port mutex for device add", dev->addr, dev->port); + return ESP_ERR_TIMEOUT; + } + + if (!port_state->installed || !port_state->bus_handle) + { + ESP_LOGE(TAG, "[0x%02x at %d] Cannot add device, bus for port %d not ready!", dev->addr, dev->port, dev->port); + xSemaphoreGive(port_state->lock); + return ESP_ERR_INVALID_STATE; + } + + ESP_LOGV(TAG, "[0x%02x at %d] Adding device to bus (Bus Handle: %p)...", dev->addr, dev->port, port_state->bus_handle); + + uint32_t effective_dev_speed = dev->cfg.master.clk_speed; + if (effective_dev_speed == 0) + { + ESP_LOGW(TAG, + "[0x%02x at %d] Device speed (dev->cfg.master.clk_speed) is 0, using default: " + "%" PRIu32 " Hz", + dev->addr, dev->port, (uint32_t)I2C_DEFAULT_FREQ_HZ); + effective_dev_speed = I2C_DEFAULT_FREQ_HZ; + } + + i2c_device_config_t dev_config = { + // Use the possibly modified addr_bit_len that respects hardware capabilities + .dev_addr_length = dev->addr_bit_len, + .device_address = dev->addr, + .scl_speed_hz = effective_dev_speed, + .flags.disable_ack_check = false, + }; + + res = i2c_master_bus_add_device(port_state->bus_handle, &dev_config, (i2c_master_dev_handle_t *)&dev->dev_handle); + if (res == ESP_OK) + { + ESP_LOGI(TAG, "[0x%02x at %d] Device added successfully (Device Handle: %p, Speed: %" PRIu32 " Hz).", dev->addr, dev->port, dev->dev_handle, effective_dev_speed); + + // Increment the port reference count for each device successfully added + port_state->ref_count++; + ESP_LOGV(TAG, "[Port %d] Incremented ref_count to %" PRIu32, dev->port, port_state->ref_count); + } + else + { + ESP_LOGE(TAG, "[0x%02x at %d] Failed to add device to bus: %d (%s)", dev->addr, dev->port, res, esp_err_to_name(res)); + dev->dev_handle = NULL; + } + xSemaphoreGive(port_state->lock); + } + else + { + ESP_LOGV(TAG, "[0x%02x at %d] Device handle %p already exists. Skipping add.", dev->addr, dev->port, dev->dev_handle); + res = ESP_OK; + } + ESP_LOGV(TAG, "[0x%02x at %d] Device context setup finished with res %d.", dev->addr, dev->port, res); return res; } -esp_err_t i2c_dev_read(const i2c_dev_t *dev, const void *out_data, size_t out_size, void *in_data, size_t in_size) +// Helper function with retry mechanism for I2C operations +static esp_err_t i2c_do_operation_with_retry(i2c_dev_t *dev, esp_err_t (*i2c_func)(i2c_master_dev_handle_t, const void *, size_t, void *, size_t, int), const void *write_buffer, size_t write_size, + void *read_buffer, size_t read_size) { - if (!dev || !in_data || !in_size) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; + esp_err_t res = ESP_FAIL; + int retry = 0; + int timeout_ms = CONFIG_I2CDEV_TIMEOUT; - SEMAPHORE_TAKE(dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] Performing I2C operation (timeout %d ms)...", dev->addr, dev->port, timeout_ms); - esp_err_t res = i2c_setup_port(dev); - if (res == ESP_OK) + while (retry <= I2C_MAX_RETRIES) { - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - if (out_data && out_size) + // Ensure device is set up before each attempt, in case handle became stale or bus was reset + // This is more robust if issues like bus errors or device resets occur. + res = i2c_setup_device(dev); + if (res != ESP_OK) { - i2c_master_start(cmd); - i2c_master_write_byte(cmd, dev->addr << 1, true); - i2c_master_write(cmd, (void *)out_data, out_size, true); + ESP_LOGE(TAG, "[0x%02x at %d] Device setup failed (Try %d): %d (%s). Retrying setup...", dev->addr, dev->port, retry, res, esp_err_to_name(res)); + // No point continuing this attempt if setup fails, but the loop will retry setup. + vTaskDelay(pdMS_TO_TICKS(I2C_RETRY_BASE_DELAY_MS * (1 << (retry)))); + retry++; + continue; + } + if (!dev->dev_handle) + { + ESP_LOGE(TAG, + "[0x%02x at %d] Device handle is NULL after setup (Try %d)! Cannot perform " + "operation.", + dev->addr, dev->port, retry); + // This indicates a persistent problem with adding the device to the bus. + // No point retrying the i2c_func if handle is null. + res = ESP_ERR_INVALID_STATE; + vTaskDelay(pdMS_TO_TICKS(I2C_RETRY_BASE_DELAY_MS * (1 << (retry)))); + retry++; + continue; } - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (dev->addr << 1) | 1, true); - i2c_master_read(cmd, in_data, in_size, I2C_MASTER_LAST_NACK); - i2c_master_stop(cmd); - res = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); - if (res != ESP_OK) - ESP_LOGE(TAG, "Could not read from device [0x%02x at %d]: %d (%s)", dev->addr, dev->port, res, esp_err_to_name(res)); + ESP_LOGV(TAG, "[0x%02x at %d] Attempting I2C op (Try %d, Handle %p)", dev->addr, dev->port, retry, dev->dev_handle); + res = i2c_func(dev->dev_handle, write_buffer, write_size, read_buffer, read_size, timeout_ms); + + if (res == ESP_OK) + { + ESP_LOGV(TAG, "[0x%02x at %d] I2C operation successful (Try %d).", dev->addr, dev->port, retry); + return ESP_OK; + } + + ESP_LOGW(TAG, "[0x%02x at %d] I2C op failed (Try %d, Handle %p): %d (%s).", dev->addr, dev->port, retry, dev->dev_handle, res, esp_err_to_name(res)); + + // Only remove handle on errors that indicate handle corruption or permanent invalidity + // Don't remove on temporary errors like ESP_ERR_TIMEOUT, ESP_FAIL (NACK), etc. + bool should_remove_handle = false; + switch (res) + { + case ESP_ERR_INVALID_ARG: + // Handle was likely removed by another task or is corrupted + should_remove_handle = true; + ESP_LOGW(TAG, "[0x%02x at %d] Invalid argument error - handle may be corrupted", dev->addr, dev->port); + break; + case ESP_ERR_INVALID_STATE: + // I2C driver is in invalid state, handle likely needs recreation + should_remove_handle = true; + ESP_LOGW(TAG, "[0x%02x at %d] Invalid state error - handle may need recreation", dev->addr, dev->port); + break; + default: + // For other errors (timeout, NACK, bus busy, etc.), keep the handle + // These are usually temporary and don't require handle recreation + should_remove_handle = false; + ESP_LOGV(TAG, "[0x%02x at %d] Temporary error - keeping handle for retry", dev->addr, dev->port); + break; + } - i2c_cmd_link_delete(cmd); + if (should_remove_handle && dev->dev_handle) + { + ESP_LOGW(TAG, "[0x%02x at %d] Removing potentially corrupted device handle %p after permanent error", dev->addr, dev->port, dev->dev_handle); + // Try to remove the handle from the bus before nullifying + esp_err_t rm_res = i2c_master_bus_rm_device(dev->dev_handle); + if (rm_res != ESP_OK) + { + ESP_LOGW(TAG, "[0x%02x at %d] Failed to remove corrupted handle (expected): %s", dev->addr, dev->port, esp_err_to_name(rm_res)); + // This is expected if the handle was already invalid - continue cleanup + } + dev->dev_handle = NULL; + } + + retry++; + if (retry <= I2C_MAX_RETRIES) + { + vTaskDelay(pdMS_TO_TICKS(I2C_RETRY_BASE_DELAY_MS * (1 << retry))); // Exponential backoff + ESP_LOGW(TAG, "[0x%02x at %d] Retrying operation...", dev->addr, dev->port); + } } - SEMAPHORE_GIVE(dev->port); + ESP_LOGE(TAG, "[0x%02x at %d] I2C operation failed after %d retries. Last error: %d (%s)", dev->addr, dev->port, I2C_MAX_RETRIES + 1, res, esp_err_to_name(res)); return res; } +// Wrapper functions for the I2C master API to use with the retry mechanism +// i2c_do_operation_with_retry() needs a unified function signature for all I2C operations +static esp_err_t i2c_master_transmit_wrapper(i2c_master_dev_handle_t handle, const void *write_buffer, size_t write_size, void *read_buffer, size_t read_size, int timeout_ms) +{ + return i2c_master_transmit(handle, write_buffer, write_size, timeout_ms); +} + +static esp_err_t i2c_master_receive_wrapper(i2c_master_dev_handle_t handle, const void *write_buffer, size_t write_size, void *read_buffer, size_t read_size, int timeout_ms) +{ + return i2c_master_receive(handle, read_buffer, read_size, timeout_ms); +} + +static esp_err_t i2c_master_transmit_receive_wrapper(i2c_master_dev_handle_t handle, const void *write_buffer, size_t write_size, void *read_buffer, size_t read_size, int timeout_ms) +{ + return i2c_master_transmit_receive(handle, write_buffer, write_size, read_buffer, read_size, timeout_ms); +} + +esp_err_t i2c_dev_read(const i2c_dev_t *dev, const void *out_data, size_t out_size, void *in_data, size_t in_size) +{ + if (!dev || !in_data || !in_size) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] i2c_dev_read called (out_size: %u, in_size: %u)", dev->addr, dev->port, out_size, in_size); + + esp_err_t result = i2c_do_operation_with_retry((i2c_dev_t *)dev, // Cast to non-const for i2c_setup_device internal modifications + out_data && out_size ? i2c_master_transmit_receive_wrapper : i2c_master_receive_wrapper, out_data, out_size, in_data, in_size); + + ESP_LOGV(TAG, "[0x%02x at %d] i2c_dev_read result: %s (%d)", dev->addr, dev->port, esp_err_to_name(result), result); + return result; +} + esp_err_t i2c_dev_write(const i2c_dev_t *dev, const void *out_reg, size_t out_reg_size, const void *out_data, size_t out_size) { - if (!dev || !out_data || !out_size) return ESP_ERR_INVALID_ARG; + if (!dev) + return ESP_ERR_INVALID_ARG; + if ((!out_reg || !out_reg_size) && (!out_data || !out_size)) + return ESP_ERR_INVALID_ARG; - SEMAPHORE_TAKE(dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] i2c_dev_write called (reg_size: %u, data_size: %u)", dev->addr, dev->port, out_reg_size, out_size); - esp_err_t res = i2c_setup_port(dev); - if (res == ESP_OK) - { - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, dev->addr << 1, true); - if (out_reg && out_reg_size) - i2c_master_write(cmd, (void *)out_reg, out_reg_size, true); - i2c_master_write(cmd, (void *)out_data, out_size, true); - i2c_master_stop(cmd); - res = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); - if (res != ESP_OK) - ESP_LOGE(TAG, "Could not write to device [0x%02x at %d]: %d (%s)", dev->addr, dev->port, res, esp_err_to_name(res)); - i2c_cmd_link_delete(cmd); + esp_err_t res; + if (out_reg && out_reg_size && out_data && out_size) + { + size_t total_write_size = out_reg_size + out_size; + + // Check for overflow before proceeding + if (total_write_size < out_reg_size || total_write_size < out_size) + { + ESP_LOGE(TAG, "[0x%02x at %d] Write size overflow: reg_size=%u + data_size=%u", dev->addr, dev->port, out_reg_size, out_size); + return ESP_ERR_INVALID_ARG; + } + + // Use stack for small buffers to avoid heap fragmentation + if (total_write_size <= I2CDEV_MAX_STACK_ALLOC_SIZE) + { // Use stack allocation for small buffers + uint8_t stack_buf[I2CDEV_MAX_STACK_ALLOC_SIZE]; + memcpy(stack_buf, out_reg, out_reg_size); + memcpy(stack_buf + out_reg_size, out_data, out_size); + res = i2c_do_operation_with_retry((i2c_dev_t *)dev, i2c_master_transmit_wrapper, stack_buf, total_write_size, NULL, 0); + } + else + { + uint8_t *heap_buf = malloc(total_write_size); + if (!heap_buf) + { + ESP_LOGE(TAG, "[0x%02x at %d] Failed to allocate %u bytes for write", dev->addr, dev->port, total_write_size); + return ESP_ERR_NO_MEM; + } + memcpy(heap_buf, out_reg, out_reg_size); + memcpy(heap_buf + out_reg_size, out_data, out_size); + res = i2c_do_operation_with_retry((i2c_dev_t *)dev, i2c_master_transmit_wrapper, heap_buf, total_write_size, NULL, 0); + free(heap_buf); // Free buffer regardless of operation result + } + } + else if (out_reg && out_reg_size) + { + res = i2c_do_operation_with_retry((i2c_dev_t *)dev, i2c_master_transmit_wrapper, out_reg, out_reg_size, NULL, 0); + } + else if (out_data && out_size) + { + res = i2c_do_operation_with_retry((i2c_dev_t *)dev, i2c_master_transmit_wrapper, out_data, out_size, NULL, 0); + } + else + { + return ESP_ERR_INVALID_ARG; // Shouldn't reach here given the earlier check } - SEMAPHORE_GIVE(dev->port); + ESP_LOGV(TAG, "[0x%02x at %d] i2c_dev_write result: %s (%d)", dev->addr, dev->port, esp_err_to_name(res), res); return res; } -esp_err_t i2c_dev_read_reg(const i2c_dev_t *dev, uint8_t reg, void *in_data, size_t in_size) +esp_err_t i2c_dev_read_reg(const i2c_dev_t *dev, uint8_t reg, void *data, size_t size) { - return i2c_dev_read(dev, ®, 1, in_data, in_size); + ESP_LOGV(TAG, "[0x%02x at %d] i2c_dev_read_reg called (reg: 0x%02x, size: %u)", dev->addr, dev->port, reg, size); + return i2c_dev_read(dev, ®, 1, data, size); } -esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, const void *out_data, size_t out_size) +esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, const void *data, size_t size) { - return i2c_dev_write(dev, ®, 1, out_data, out_size); + ESP_LOGV(TAG, "[0x%02x at %d] i2c_dev_write_reg called (reg: 0x%02x, size: %u)", dev->addr, dev->port, reg, size); + return i2c_dev_write(dev, ®, 1, data, size); +} + +esp_err_t i2c_dev_check_present(const i2c_dev_t *dev_const) +{ + if (!dev_const) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Probing device presence...", dev_const->addr, dev_const->port); + + // Cast to non-const for i2c_setup_port (which may modify internal state) + i2c_dev_t *dev = (i2c_dev_t *)dev_const; + + // Ensure the I2C port is set up before probing + esp_err_t setup_res = i2c_setup_port(dev); + if (setup_res != ESP_OK) + { + ESP_LOGE(TAG, "[0x%02x at %d] Failed to setup port for probe: %s", dev_const->addr, dev_const->port, esp_err_to_name(setup_res)); + return setup_res; + } + + // Now probe using the initialized bus + if (dev_const->port < I2C_NUM_MAX && i2c_ports[dev_const->port].lock) + { + if (xSemaphoreTake(i2c_ports[dev_const->port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) == pdTRUE) + { + if (i2c_ports[dev_const->port].installed && i2c_ports[dev_const->port].bus_handle) + { + // Use ESP-IDF's built-in probe function - completely non-intrusive + esp_err_t probe_res = i2c_master_probe(i2c_ports[dev_const->port].bus_handle, dev_const->addr, CONFIG_I2CDEV_TIMEOUT); + xSemaphoreGive(i2c_ports[dev_const->port].lock); + + if (probe_res == ESP_OK) + { + ESP_LOGV(TAG, "[0x%02x at %d] Device probe successful - device present", dev_const->addr, dev_const->port); + return ESP_OK; + } + else + { + ESP_LOGV(TAG, "[0x%02x at %d] Device probe failed: %s", dev_const->addr, dev_const->port, esp_err_to_name(probe_res)); + return probe_res; + } + } + else + { + xSemaphoreGive(i2c_ports[dev_const->port].lock); + ESP_LOGW(TAG, "[0x%02x at %d] Cannot probe - bus not ready on port %d", dev_const->addr, dev_const->port, dev_const->port); + return ESP_ERR_INVALID_STATE; + } + } + else + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not take port mutex for probe", dev_const->addr, dev_const->port); + return ESP_ERR_TIMEOUT; + } + } + else + { + ESP_LOGE(TAG, "[0x%02x at %d] Invalid port or port not initialized", dev_const->addr, dev_const->port); + return ESP_ERR_INVALID_ARG; + } +} + +// Compatibility wrapper for legacy code that still calls i2c_dev_probe +// The new driver implementation uses i2c_master_probe which doesn't need operation_type +esp_err_t i2c_dev_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type) +{ + ESP_LOGV(TAG, "[0x%02x at %d] Legacy probe called (operation_type %d), redirecting to new implementation", dev->addr, dev->port, operation_type); + + return i2c_dev_check_present(dev); +} + +// Clean up function to be called at application exit +esp_err_t i2cdev_done(void) +{ + esp_err_t result = ESP_OK; + ESP_LOGV(TAG, "Cleaning up I2C subsystem (i2c_master)..."); + for (int i = 0; i < I2C_NUM_MAX; i++) + { + if (i2c_ports[i].lock) + { + ESP_LOGV(TAG, "[Port %d] Cleaning up port...", i); + if (xSemaphoreTake(i2c_ports[i].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) != pdTRUE) + { + ESP_LOGE(TAG, "[Port %d] Could not take port mutex for cleanup", i); + result = ESP_FAIL; + } + else + { + if (i2c_ports[i].installed) + { + ESP_LOGV(TAG, "[Port %d] Removing active devices before deleting bus...", i); + // Remove all registered devices for this port from the bus + for (int j = 0; j < CONFIG_I2CDEV_MAX_DEVICES_PER_PORT; j++) + { + i2c_dev_t *dev_ptr = active_devices[i][j]; + if (dev_ptr != NULL && dev_ptr->dev_handle != NULL) + { + ESP_LOGV(TAG, "[Port %d] Removing device 0x%02x (Handle %p)", i, dev_ptr->addr, dev_ptr->dev_handle); + esp_err_t rm_res = i2c_master_bus_rm_device(dev_ptr->dev_handle); + if (rm_res != ESP_OK) + { + ESP_LOGE(TAG, "[Port %d] Failed to remove device 0x%02x handle: %d", i, dev_ptr->addr, rm_res); + // Continue cleanup despite error + if (result == ESP_OK) + result = rm_res; // Report first error + } + dev_ptr->dev_handle = NULL; + } + } + + ESP_LOGV(TAG, "[Port %d] Deleting master bus handle %p...", i, i2c_ports[i].bus_handle); + esp_err_t del_res = i2c_del_master_bus(i2c_ports[i].bus_handle); + if (del_res != ESP_OK) + { + ESP_LOGE(TAG, "[Port %d] Failed to delete I2C bus during cleanup: %d", i, del_res); + if (result == ESP_OK) + result = del_res; + } + i2c_ports[i].installed = false; + i2c_ports[i].bus_handle = NULL; + i2c_ports[i].ref_count = 0; + } + xSemaphoreGive(i2c_ports[i].lock); + } // End else (mutex taken) + + ESP_LOGV(TAG, "[Port %d] Deleting port mutex...", i); + vSemaphoreDelete(i2c_ports[i].lock); + i2c_ports[i].lock = NULL; + // Clear the active device list for this port + memset(active_devices[i], 0, sizeof(active_devices[i])); + ESP_LOGV(TAG, "[Port %d] Cleanup complete.", i); + + } // end if lock exists + } // end for loop + ESP_LOGV(TAG, "I2C subsystem cleanup finished with result: %d", result); + return result; } diff --git a/components/i2cdev/i2cdev.h b/components/i2cdev/i2cdev.h index ce972a6b..6e1a16b1 100644 --- a/components/i2cdev/i2cdev.h +++ b/components/i2cdev/i2cdev.h @@ -1,26 +1,3 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2018 Ruslan V. Uss - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - /** * @file i2cdev.h * @defgroup i2cdev i2cdev @@ -28,30 +5,67 @@ * * ESP-IDF I2C master thread-safe functions for communication with I2C slave * - * Copyright (c) 2018 Ruslan V. Uss + * This implementation uses the newer ESP-IDF I2C master driver (v5.0+). + * For ESP-IDF versions using the legacy I2C driver, use i2cdev_legacy.c instead. + * + * Copyright (C) 2018 Ruslan V. Uss + * Updated 2025 by quinkq to use newer ESP-IDF I2C master driver API * * MIT Licensed as described in the file LICENSE + * + * ============================================================================ + * OPTIONAL I2C PULLUP AUTO-CONFIGURATION + * ============================================================================ + * + * This library can optionally enable internal I2C pullups when no explicit + * pullup configuration is provided. Feature is DISABLED by default for + * backward compatibility (CONFIG_I2CDEV_AUTO_ENABLE_PULLUPS=n). + * + * Optional auto-pullup (CONFIG_I2CDEV_AUTO_ENABLE_PULLUPS=y): + * - If both pullup flags are false (not set/default state), automatically enables internal pullups + * - Only available on ESP32 family (modern driver) + * - Legacy driver always uses explicit configuration + * + * + * Example - Enable internal pullups: + * i2c_dev_t sensor = { + * .port = I2C_NUM_0, + * .addr = 0x48, + * .cfg = { + * .sda_io_num = GPIO_NUM_21, + * .scl_io_num = GPIO_NUM_22, + * .sda_pullup_en = true, // Enable internal pullups + * .scl_pullup_en = true, // Enable internal pullups + * .master.clk_speed = 400000 + * } + * }; + * + * ============================================================================ */ + #ifndef __I2CDEV_H__ #define __I2CDEV_H__ +#include #include -#include -#include #include #include +#include +#include -#ifdef __cplusplus -extern "C" { +// Define missing types for older ESP-IDF versions +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 2, 0) +typedef enum { + I2C_ADDR_BIT_LEN_7 = 0, /*!< I2C 7bit address for slave mode */ + I2C_ADDR_BIT_LEN_10, /*!< I2C 10bit address for slave mode */ +} i2c_addr_bit_len_t; #endif +// Definition for I2CDEV_MAX_STRETCH_TIME #if HELPER_TARGET_IS_ESP8266 - #define I2CDEV_MAX_STRETCH_TIME 0xffffffff - #else - -#include +#include // For I2C_TIME_OUT_VALUE_V, etc. #if defined(I2C_TIME_OUT_VALUE_V) #define I2CDEV_MAX_STRETCH_TIME I2C_TIME_OUT_VALUE_V #elif defined(I2C_TIME_OUT_REG_V) @@ -59,188 +73,301 @@ extern "C" { #else #define I2CDEV_MAX_STRETCH_TIME 0x00ffffff #endif - #endif /* HELPER_TARGET_IS_ESP8266 */ +#ifndef CONFIG_I2CDEV_TIMEOUT +#define CONFIG_I2CDEV_TIMEOUT 1000 // Default 1 second timeout +#endif + +#ifndef CONFIG_I2CDEV_NOLOCK +#define CONFIG_I2CDEV_NOLOCK 0 // Enable locking by default +#endif + +#ifndef CONFIG_I2CDEV_MAX_DEVICES_PER_PORT +#define CONFIG_I2CDEV_MAX_DEVICES_PER_PORT 8 // Maximum devices per I2C port +#endif + +#ifndef CONFIG_I2CDEV_DEFAULT_SDA_PIN +#define CONFIG_I2CDEV_DEFAULT_SDA_PIN 21 // Default SDA pin +#endif + +#ifndef CONFIG_I2CDEV_DEFAULT_SCL_PIN +#define CONFIG_I2CDEV_DEFAULT_SCL_PIN 22 // Default SCL pin +#endif + +#ifndef CONFIG_FREERTOS_HZ +#define CONFIG_FREERTOS_HZ 100 // Default value in most ESP-IDF configs +#endif + +#ifndef CONFIG_LOG_MAXIMUM_LEVEL +#define CONFIG_LOG_MAXIMUM_LEVEL 3 // INFO level as default +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief I2C transaction type for legacy probe + */ +typedef enum { + I2C_DEV_WRITE = 0, /**< Write operation for probe */ + I2C_DEV_READ /**< Read operation for probe */ +} i2c_dev_type_t; + /** * I2C device descriptor + * + * This structure supports both legacy ESP-IDF I2C driver and modern i2c_master driver. + * + * @note INITIALIZATION CHECKLIST - Set these fields before calling i2c_dev_create_mutex(): + * + * ┌─── REQUIRED (Set by user) ───────────────────────────────────────────┐ + * │ - dev->port - I2C port number (e.g., I2C_NUM_0) │ + * │ - dev->addr - Device I2C address (e.g., 0x48) │ + * │ - dev->cfg.sda_io_num - SDA pin (-1 = use Kconfig default) │ + * │ - dev->cfg.scl_io_num - SCL pin (-1 = use Kconfig default) │ + * │ - dev->cfg.master.clk_speed - Clock speed in Hz (e.g., 400000) │ + * └──────────────────────────────────────────────────────────────────────┘ + * + * ┌─── OPTIONAL (Set by user if needed) ─────────────────────────────────┐ + * │ - dev->addr_bit_len - Address format (defaults to 7-bit) - NEW │ + * │ - dev->cfg.sda_pullup_en - Enable internal SDA pullup │ + * │ - dev->cfg.scl_pullup_en - Enable internal SCL pullup │ + * │ - dev->timeout_ticks - Legacy driver timeout (legacy only) │ + * └──────────────────────────────────────────────────────────────────────┘ + * + * ┌─── AUTO-POPULATED (library fills these) ─────────────────────────────┐ + * │ - dev->mutex - Device mutex handle │ + * │ - dev->dev_handle - I2C device handle (modern driver) - NEW │ + * │ - dev->sda_pin - Actual SDA pin used by bus │ + * │ - dev->scl_pin - Actual SCL pin used by bus │ + * └──────────────────────────────────────────────────────────────────────┘ + * + * @note BACKWARD COMPATIBILITY DESIGN: + * The custom 'cfg' structure mimics ESP-IDF's deprecated i2c_config_t layout + * to maintain zero-change compatibility with existing device drivers. + * ESP-IDF ≥5.2 deprecated i2c_config_t and split it into separate bus/device + * configs, but this library preserves the familiar field paths like: + * dev->cfg.sda_io_num, dev->cfg.scl_io_num, dev->cfg.master.clk_speed */ typedef struct { - i2c_port_t port; //!< I2C port number - i2c_config_t cfg; //!< I2C driver configuration - uint8_t addr; //!< Unshifted address - SemaphoreHandle_t mutex; //!< Device mutex - uint32_t timeout_ticks; /*!< HW I2C bus timeout (stretch time), in ticks. 80MHz APB clock - ticks for ESP-IDF, CPU ticks for ESP8266. - When this value is 0, I2CDEV_MAX_STRETCH_TIME will be used */ -} i2c_dev_t; + // ═══ Core Device Identity (REQUIRED) ═══ + i2c_port_t port; //!< I2C port number (e.g., I2C_NUM_0) + uint16_t addr; //!< Device I2C address (e.g., 0x48 for 7-bit) + i2c_addr_bit_len_t addr_bit_len; //!< Address format: I2C_ADDR_BIT_LEN_7 (default) or I2C_ADDR_BIT_LEN_10 -/** - * I2C transaction type - */ -typedef enum { - I2C_DEV_WRITE = 0, /**< Write operation */ - I2C_DEV_READ /**< Read operation */ -} i2c_dev_type_t; + // ═══ Library Internal State (AUTO-POPULATED) ═══ + SemaphoreHandle_t mutex; //!< Device mutex - Created by i2c_dev_create_mutex() + void *dev_handle; //!< Device handle - Modern driver only, created lazily (when actual I2C operation is performed) + int sda_pin; //!< Actual SDA pin used - Populated after port setup + int scl_pin; //!< Actual SCL pin used - Populated after port setup + + // ═══ Legacy Driver Compatibility ═══ + uint32_t timeout_ticks; //!< Clock stretching timeout - Legacy driver only + + // ═══ User Configuration (REQUIRED) ═══ + // Configuration structure with i2c_config_t compatible field layout. + struct + { + gpio_num_t sda_io_num; //!< Desired SDA pin (-1 = use Kconfig default) + gpio_num_t scl_io_num; //!< Desired SCL pin (-1 = use Kconfig default) + uint8_t sda_pullup_en; //!< Enable internal SDA pullup (optional) + uint8_t scl_pullup_en; //!< Enable internal SCL pullup (optional) + struct + { + uint32_t clk_speed; //!< Clock speed in Hz + } master; //!< Master-specific config (mimics old i2c_config_t.master) + } cfg; //!< Configuration set by device drivers (i2c_config_t compatible layout) +} i2c_dev_t; /** - * @brief Init library + * @brief Initialize I2C subsystem (port mutexes and internal states) * - * The function must be called before any other - * functions of this library. + * @note This should be called once at the beginning of your application + * before any I2C devices are initialized. * * @return ESP_OK on success */ -esp_err_t i2cdev_init(); +esp_err_t i2cdev_init(void); /** - * @brief Finish work with library + * @brief Release I2C subsystem (deletes all devices, buses, and mutexes) * - * Uninstall i2c drivers. + * @note Call this when no more I2C operations will be performed + * to clean up resources. * * @return ESP_OK on success */ -esp_err_t i2cdev_done(); +esp_err_t i2cdev_done(void); /** - * @brief Create mutex for device descriptor + * @brief Create mutex for device descriptor and register device * - * This function does nothing if option CONFIG_I2CDEV_NOLOCK is enabled. + * @note IMPORTANT: Before calling this function, you must properly initialize the i2c_dev_t + * structure with device address, port, and pin settings. For ESP-IDF legacy driver, + * set pins in dev->cfg.sda_io_num and dev->cfg.scl_io_num. For newer ESP-IDF version, + * either method is compatible. See the structure documentation for details. * - * @param dev Device descriptor - * @return ESP_OK on success + * @param dev Pointer to device descriptor + * @return `ESP_OK` on success */ esp_err_t i2c_dev_create_mutex(i2c_dev_t *dev); /** - * @brief Delete mutex for device descriptor + * @brief Delete mutex for device descriptor and perform device cleanup * - * This function does nothing if option CONFIG_I2CDEV_NOLOCK is enabled. + * @note This function performs cleanup tasks including removing the device from the + * I2C bus, deregistering it, and deleting its mutex. * - * @param dev Device descriptor - * @return ESP_OK on success + * @param dev Pointer to device descriptor + * @return `ESP_OK` on success */ esp_err_t i2c_dev_delete_mutex(i2c_dev_t *dev); /** * @brief Take device mutex * - * This function does nothing if option CONFIG_I2CDEV_NOLOCK is enabled. - * - * @param dev Device descriptor - * @return ESP_OK on success + * @param dev Pointer to device descriptor + * @return `ESP_OK` on success */ esp_err_t i2c_dev_take_mutex(i2c_dev_t *dev); /** * @brief Give device mutex * - * This function does nothing if option CONFIG_I2CDEV_NOLOCK is enabled. - * - * @param dev Device descriptor - * @return ESP_OK on success + * @param dev Pointer to device descriptor + * @return `ESP_OK` on success */ esp_err_t i2c_dev_give_mutex(i2c_dev_t *dev); /** - * @brief Check the availability of the device + * @brief Check the availability of a device on the I2C bus (New Driver) - legacy's i2c_dev_probe function equivalent. * - * Issue an operation of \p operation_type to the I2C device then stops. + * This function attempts to communicate with the I2C device to see if it ACKs. + * It is non-intrusive; if the device is found, any temporary setup for + * the check is torn down. Uses the new I2C driver logic. * - * @param dev Device descriptor - * @param operation_type Operation type - * @return ESP_OK if device is available + * @param dev Pointer to the device descriptor. Pins and address must be configured. + * @return `ESP_OK` if the device ACKs (is present), an error code otherwise. */ -esp_err_t i2c_dev_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type); +esp_err_t i2c_dev_check_present(const i2c_dev_t *dev); /** - * @brief Read from slave device + * @brief Check the availability of a device on the I2C bus (Legacy Driver). * - * Issue a send operation of \p out_data register address, followed by reading \p in_size bytes - * from slave into \p in_data . - * Function is thread-safe. + * Issue an operation of `operation_type` to the I2C device then stops. + * Primarily for use with the legacy i2cdev_legacy.c implementation. * - * @param dev Device descriptor - * @param out_data Pointer to data to send if non-null - * @param out_size Size of data to send - * @param[out] in_data Pointer to input data buffer - * @param in_size Number of byte to read - * @return ESP_OK on success + * @param dev Device descriptor. + * @param operation_type Operation type (I2C_DEV_WRITE or I2C_DEV_READ). + * @return `ESP_OK` if device is available for the specified operation type. */ -esp_err_t i2c_dev_read(const i2c_dev_t *dev, const void *out_data, - size_t out_size, void *in_data, size_t in_size); +esp_err_t i2c_dev_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type); /** - * @brief Write to slave device + * @brief Read from device * - * Write \p out_size bytes from \p out_data to slave into \p out_reg register address. - * Function is thread-safe. + * @param dev Pointer to device descriptor + * @param[in] out_data Data to write before reading (can be NULL if out_size is 0) + * @param out_size Size of data to write + * @param[out] in_data Buffer to store data read + * @param in_size Number of bytes to read + * @return `ESP_OK` on success + */ +esp_err_t i2c_dev_read(const i2c_dev_t *dev, const void *out_data, size_t out_size, void *in_data, size_t in_size); + +/** + * @brief Write to device * - * @param dev Device descriptor - * @param out_reg Pointer to register address to send if non-null + * @param dev Pointer to device descriptor + * @param[in] out_reg Register address to write to (can be NULL if out_reg_size is 0) * @param out_reg_size Size of register address - * @param out_data Pointer to data to send - * @param out_size Size of data to send - * @return ESP_OK on success + * @param[in] out_data Data to write (can be NULL if out_size is 0) + * @param out_size Size of data to write + * @return `ESP_OK` on success */ -esp_err_t i2c_dev_write(const i2c_dev_t *dev, const void *out_reg, - size_t out_reg_size, const void *out_data, size_t out_size); +esp_err_t i2c_dev_write(const i2c_dev_t *dev, const void *out_reg, size_t out_reg_size, const void *out_data, size_t out_size); /** - * @brief Read from register with an 8-bit address + * @brief Read from device register (8-bit register address) * - * Shortcut to ::i2c_dev_read(). - * - * @param dev Device descriptor - * @param reg Register address - * @param[out] in_data Pointer to input data buffer - * @param in_size Number of byte to read - * @return ESP_OK on success + * @param dev Pointer to device descriptor + * @param reg Command to write before reading + * @param[out] data Buffer to store data + * @param size Number of bytes to read + * @return `ESP_OK` on success */ -esp_err_t i2c_dev_read_reg(const i2c_dev_t *dev, uint8_t reg, - void *in_data, size_t in_size); +esp_err_t i2c_dev_read_reg(const i2c_dev_t *dev, uint8_t reg, void *data, size_t size); /** - * @brief Write to register with an 8-bit address - * - * Shortcut to ::i2c_dev_write(). + * @brief Write to device register (8-bit register address) * - * @param dev Device descriptor - * @param reg Register address - * @param out_data Pointer to data to send - * @param out_size Size of data to send - * @return ESP_OK on success + * @param dev Pointer to device descriptor + * @param reg Command to write before writing data + * @param data Buffer with data to write + * @param size Number of bytes to write + * @return `ESP_OK` on success + */ +esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, const void *data, size_t size); + +/** + * @brief Take device mutex with error checking + */ +#define I2C_DEV_TAKE_MUTEX(dev) \ + do \ + { \ + esp_err_t __ = i2c_dev_take_mutex(dev); \ + if (__ != ESP_OK) \ + return __; \ + } \ + while (0) + +/** + * @brief Give device mutex with error checking + */ +#define I2C_DEV_GIVE_MUTEX(dev) \ + do \ + { \ + esp_err_t __ = i2c_dev_give_mutex(dev); \ + if (__ != ESP_OK) \ + return __; \ + } \ + while (0) + +/** + * @brief Execute operation, assuming mutex is held. Gives mutex ONLY on error. + */ +#define I2C_DEV_CHECK(dev, X) \ + do \ + { \ + esp_err_t ___ = X; /* Execute operation */ \ + if (___ != ESP_OK) \ + { \ + /* Give mutex ONLY if error occurred */ \ + i2c_dev_give_mutex(dev); \ + return ___; \ + } \ + } \ + while (0) + +/** + * @brief Execute operation, assuming mutex is held. Gives mutex ONLY on error, logs error. */ -esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, - const void *out_data, size_t out_size); - -#define I2C_DEV_TAKE_MUTEX(dev) do { \ - esp_err_t __ = i2c_dev_take_mutex(dev); \ - if (__ != ESP_OK) return __;\ - } while (0) - -#define I2C_DEV_GIVE_MUTEX(dev) do { \ - esp_err_t __ = i2c_dev_give_mutex(dev); \ - if (__ != ESP_OK) return __;\ - } while (0) - -#define I2C_DEV_CHECK(dev, X) do { \ - esp_err_t ___ = X; \ - if (___ != ESP_OK) { \ - I2C_DEV_GIVE_MUTEX(dev); \ - return ___; \ - } \ - } while (0) - -#define I2C_DEV_CHECK_LOGE(dev, X, msg, ...) do { \ - esp_err_t ___ = X; \ - if (___ != ESP_OK) { \ - I2C_DEV_GIVE_MUTEX(dev); \ - ESP_LOGE(TAG, msg, ## __VA_ARGS__); \ - return ___; \ - } \ - } while (0) +#define I2C_DEV_CHECK_LOGE(dev, X, msg, ...) \ + do \ + { \ + esp_err_t ___ = X; /* Execute operation */ \ + if (___ != ESP_OK) \ + { \ + /* Give mutex ONLY if error occurred */ \ + i2c_dev_give_mutex(dev); \ + ESP_LOGE(TAG, msg, ##__VA_ARGS__); \ + return ___; \ + } \ + } \ + while (0) #ifdef __cplusplus } @@ -248,4 +375,4 @@ esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, /**@}*/ -#endif /* __I2CDEV_H__ */ +#endif /* __I2CDEV_H__ */ \ No newline at end of file diff --git a/components/i2cdev/i2cdev_legacy.c b/components/i2cdev/i2cdev_legacy.c new file mode 100644 index 00000000..4d7dd27f --- /dev/null +++ b/components/i2cdev/i2cdev_legacy.c @@ -0,0 +1,792 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 Ruslan V. Uss + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * @file i2cdev.c + * + * ESP-IDF I2C master thread-safe functions for communication with I2C slave + * + * Copyright (c) 2018 Ruslan V. Uss + * Updated 2025 by quinkq to use newer ESP-IDF I2C master driver API + * MIT Licensed as described in the file LICENSE + */ +#include "esp_idf_lib_helpers.h" // For HELPER_TARGET_IS_ESP32 etc. +#include "i2cdev.h" // Common header +#include // Legacy I2C driver +#include +#include +#include +#include +#include +#if !HELPER_TARGET_IS_ESP8266 +#include // For APB_CLK_FREQ +#endif +#include + +static const char *TAG = "i2cdev_legacy"; + +typedef struct +{ + SemaphoreHandle_t lock; + i2c_config_t config; // Use legacy config struct + bool installed; + uint32_t ref_count; + i2c_dev_t *devices[CONFIG_I2CDEV_MAX_DEVICES_PER_PORT]; // Track devices registered on this port +} i2c_port_state_t; + +static i2c_port_state_t states[I2C_NUM_MAX] = { 0 }; + +#if CONFIG_I2CDEV_NOLOCK +#define SEMAPHORE_TAKE(port) +#else +#define SEMAPHORE_TAKE(port) \ + do \ + { \ + if (!xSemaphoreTake(states[port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT))) \ + { \ + ESP_LOGE(TAG, "Could not take port mutex %d", port); \ + return ESP_ERR_TIMEOUT; \ + } \ + } \ + while (0) +#endif + +#if CONFIG_I2CDEV_NOLOCK +#define SEMAPHORE_GIVE(port) +#else +#define SEMAPHORE_GIVE(port) \ + do \ + { \ + if (!xSemaphoreGive(states[port].lock)) \ + { \ + ESP_LOGE(TAG, "Could not give port mutex %d", port); \ + return ESP_FAIL; \ + } \ + } \ + while (0) +#endif + +/** + * @brief Register an I2C device for tracking and resource management + * + * This function adds a device to the port's tracking array, which helps with: + * - Monitoring which devices are active on each port + * - Proper cleanup when the system shuts down + * - Diagnostics and debugging + * + * Each port can track up to CONFIG_I2CDEV_MAX_DEVICES_PER_PORT devices. + * + * @param dev Device descriptor to register + * @return ESP_OK if registration succeeded, or an error code + */ +static esp_err_t register_device(i2c_dev_t *dev) +{ + if (!dev || dev->port >= I2C_NUM_MAX) + return ESP_ERR_INVALID_ARG; + if (!states[dev->port].lock) + return ESP_ERR_INVALID_STATE; + + esp_err_t ret = ESP_ERR_NO_MEM; + + // Take the mutex directly instead of using the macro + if (xSemaphoreTake(states[dev->port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) != pdTRUE) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not take port mutex for registration", dev->addr, dev->port); + return ESP_ERR_TIMEOUT; + } + + // Search for an empty slot in the device tracking array + for (int i = 0; i < CONFIG_I2CDEV_MAX_DEVICES_PER_PORT; i++) + { + if (states[dev->port].devices[i] == NULL) + { + // Found empty slot - register the device here + states[dev->port].devices[i] = dev; + ESP_LOGV(TAG, "[0x%02x at %d] Registered device in slot %d", dev->addr, dev->port, i); + ret = ESP_OK; + break; + } + } + + // All slots full - this will still allow communication but prevents automatic cleanup + if (ret != ESP_OK) + { + ESP_LOGW(TAG, "[0x%02x at %d] No free slots to register device", dev->addr, dev->port); + } + + // Release the mutex + if (!xSemaphoreGive(states[dev->port].lock)) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not give port mutex after registration", dev->addr, dev->port); + // If can't give the mutex, that's a serious error that overrides the registration result + return ESP_FAIL; + } + + return ret; +} + +/** + * @brief Deregister a device and update reference counting + * + * This function: + * 1. Removes the device from the port's tracking array + * 2. Decrements the port's reference count + * 3. Cleans up the I2C driver if this was the last device on the port + * + * This is called during device cleanup to ensure proper resource management. + * + * @param dev Device descriptor to deregister + */ +static void deregister_device(i2c_dev_t *dev) +{ + if (!dev || dev->port >= I2C_NUM_MAX) + return; + + // Don't use macros that return values since this is a void function + if (states[dev->port].lock) + { + if (xSemaphoreTake(states[dev->port].lock, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)) != pdTRUE) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not take port mutex for deregistration", dev->addr, dev->port); + return; // Cannot proceed without lock + } + + // Find the device in the tracking array + for (int i = 0; i < CONFIG_I2CDEV_MAX_DEVICES_PER_PORT; i++) + { + if (states[dev->port].devices[i] == dev) + { + // Clear this slot + states[dev->port].devices[i] = NULL; + ESP_LOGV(TAG, "[0x%02x at %d] Deregistered device from slot %d", dev->addr, dev->port, i); + break; + } + } + + // Manage reference counting for this port + if (states[dev->port].ref_count > 0) + { + states[dev->port].ref_count--; + ESP_LOGD(TAG, "[Port %d] Decremented ref_count to %" PRIu32, dev->port, states[dev->port].ref_count); + + // If this was the last device using this port, clean up the driver + if (states[dev->port].ref_count == 0 && states[dev->port].installed) + { + ESP_LOGI(TAG, "[Port %d] Last device removed, uninstalling driver", dev->port); + i2c_driver_delete(dev->port); + states[dev->port].installed = false; + } + } + + // Release the mutex + if (!xSemaphoreGive(states[dev->port].lock)) + { + ESP_LOGE(TAG, "[Port %d] Could not give port mutex after deregistration", dev->port); + // Can't do much about this error except log it + } + } +} + +esp_err_t i2cdev_init() +{ + memset(states, 0, sizeof(states)); + +#if !CONFIG_I2CDEV_NOLOCK + for (int i = 0; i < I2C_NUM_MAX; i++) + { + states[i].lock = xSemaphoreCreateMutex(); + if (!states[i].lock) + { + ESP_LOGE(TAG, "Could not create port mutex %d", i); + return ESP_FAIL; + } + } +#endif + + return ESP_OK; +} + +esp_err_t i2cdev_done() +{ + ESP_LOGV(TAG, "Cleaning up I2C subsystem (legacy)..."); + for (int i = 0; i < I2C_NUM_MAX; i++) + { + if (!states[i].lock) + continue; + + if (states[i].installed) + { + SEMAPHORE_TAKE(i); + + // First, clean up any devices still registered on this port + for (int j = 0; j < CONFIG_I2CDEV_MAX_DEVICES_PER_PORT; j++) + { + if (states[i].devices[j] != NULL) + { + i2c_dev_t *dev = states[i].devices[j]; + ESP_LOGW(TAG, "[Port %d] Device 0x%02x still registered during cleanup", i, dev->addr); + states[i].devices[j] = NULL; + } + } + + i2c_driver_delete(i); + states[i].installed = false; + states[i].ref_count = 0; + + SEMAPHORE_GIVE(i); + } +#if !CONFIG_I2CDEV_NOLOCK + vSemaphoreDelete(states[i].lock); +#endif + states[i].lock = NULL; + } + ESP_LOGV(TAG, "I2C subsystem cleanup finished (legacy)."); + return ESP_OK; +} + +esp_err_t i2c_dev_create_mutex(i2c_dev_t *dev) +{ +#if !CONFIG_I2CDEV_NOLOCK + if (!dev) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Creating device mutex", dev->addr, dev->port); + + // Initialize device pins to -1 to ensure consistent pattern with new driver + if (dev->sda_pin == 0 && dev->scl_pin == 0) + { + dev->sda_pin = -1; + dev->scl_pin = -1; + ESP_LOGD(TAG, "[0x%02x at %d] Initialized pins to -1", dev->addr, dev->port); + } + + if (dev->mutex) + { + ESP_LOGW(TAG, "[0x%02x at %d] Device mutex already exists", dev->addr, dev->port); + return ESP_OK; // Already created + } + + dev->mutex = xSemaphoreCreateMutex(); + if (!dev->mutex) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not create device mutex", dev->addr, dev->port); + return ESP_FAIL; + } + + // Register device for tracking + esp_err_t reg_res = register_device(dev); + if (reg_res != ESP_OK) + { + ESP_LOGW(TAG, "[0x%02x at %d] Could not register device: %s", dev->addr, dev->port, esp_err_to_name(reg_res)); + // Continue anyway since this is not critical + } +#endif + + return ESP_OK; +} + +esp_err_t i2c_dev_delete_mutex(i2c_dev_t *dev) +{ +#if !CONFIG_I2CDEV_NOLOCK + if (!dev) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Deleting device mutex and cleaning up", dev->addr, dev->port); + + // Deregister and update ref counts + deregister_device(dev); + + // Delete mutex if exists + if (dev->mutex) + { + vSemaphoreDelete(dev->mutex); + dev->mutex = NULL; + } + else + { + ESP_LOGV(TAG, "[0x%02x at %d] Device mutex was NULL", dev->addr, dev->port); + } +#endif + return ESP_OK; +} + +esp_err_t i2c_dev_take_mutex(i2c_dev_t *dev) +{ +#if !CONFIG_I2CDEV_NOLOCK + if (!dev) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Taking mutex", dev->addr, dev->port); + + if (!dev->mutex) + { + ESP_LOGE(TAG, "[0x%02x at %d] Attempt to take NULL mutex!", dev->addr, dev->port); + return ESP_ERR_INVALID_STATE; + } + + if (!xSemaphoreTake(dev->mutex, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT))) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not take device mutex (timeout %d ms)", dev->addr, dev->port, CONFIG_I2CDEV_TIMEOUT); + return ESP_ERR_TIMEOUT; + } +#endif + return ESP_OK; +} + +esp_err_t i2c_dev_give_mutex(i2c_dev_t *dev) +{ +#if !CONFIG_I2CDEV_NOLOCK + if (!dev) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Giving mutex", dev->addr, dev->port); + + if (!dev->mutex) + { + ESP_LOGE(TAG, "[0x%02x at %d] Attempt to give NULL mutex!", dev->addr, dev->port); + return ESP_ERR_INVALID_STATE; + } + + if (!xSemaphoreGive(dev->mutex)) + { + ESP_LOGE(TAG, "[0x%02x at %d] Could not give device mutex", dev->addr, dev->port); + return ESP_FAIL; + } +#endif + return ESP_OK; +} + +inline static bool cfg_equal(const i2c_config_t *a, const i2c_config_t *b) +{ + bool clock_equal; +#ifdef CONFIG_IDF_TARGET_ESP8266 + clock_equal = (a->clk_stretch_tick == b->clk_stretch_tick); +#else + clock_equal = (a->master.clk_speed == b->master.clk_speed); +#endif + + return a->mode == b->mode && a->scl_io_num == b->scl_io_num && a->sda_io_num == b->sda_io_num && a->scl_pullup_en == b->scl_pullup_en && a->sda_pullup_en == b->sda_pullup_en && clock_equal; + // Note: Ignoring clk_flags for comparison as it might not be consistently set by users +} + +/** + * @brief Configure and initialize the I2C port for a device + * + * This function is responsible for: + * 1. Determining which pins to use (from device or config) + * 2. Validating pin configuration + * 3. Installing/configuring the I2C driver if not already done + * 4. Managing reference counting for the port + * 5. Setting up clock stretching timeout + * + * This is a critical function that must succeed before any I2C operations + * can be performed with a device. + * + * @param dev Device descriptor with configuration info + * @return ESP_OK on success, or an error code on failure + */ +static esp_err_t i2c_setup_port(i2c_dev_t *dev) +{ + if (!dev) + { + ESP_LOGE(TAG, "Device is NULL"); + return ESP_ERR_INVALID_ARG; + } + + if (dev->port >= I2C_NUM_MAX) + { + ESP_LOGE(TAG, "Invalid I2C port number: %d", dev->port); + return ESP_ERR_INVALID_ARG; + } + + // Pin Selection Logic: + // Pins are taken from dev->cfg.xyz_io_num. + // If -1, Kconfig defaults are used. + gpio_num_t sda_pin; // Effective SDA pin to be used + gpio_num_t scl_pin; // Effective SCL pin to be used + + if (dev->cfg.sda_io_num == (gpio_num_t)-1) + { + sda_pin = (gpio_num_t)CONFIG_I2CDEV_DEFAULT_SDA_PIN; + } + else + { + sda_pin = dev->cfg.sda_io_num; + } + + if (dev->cfg.scl_io_num == (gpio_num_t)-1) + { + scl_pin = (gpio_num_t)CONFIG_I2CDEV_DEFAULT_SCL_PIN; + } + else + { + scl_pin = dev->cfg.scl_io_num; + } + + ESP_LOGD(TAG, "[0x%02x at %d] Based on cfg: sda_cfg=%d, scl_cfg=%d. Effective pins for setup: SDA=%d, SCL=%d", dev->addr, dev->port, dev->cfg.sda_io_num, dev->cfg.scl_io_num, sda_pin, scl_pin); + + // Perform basic validation of effective pins + if (sda_pin < 0 || scl_pin < 0) + { + ESP_LOGE(TAG, "[0x%02x at %d] Invalid effective SDA/SCL pins (%d, %d). Check Kconfig defaults if cfg pins were -1.", dev->addr, dev->port, sda_pin, scl_pin); + return ESP_ERR_INVALID_ARG; + } + + if (sda_pin == scl_pin) + { + ESP_LOGE(TAG, "[0x%02x at %d] Effective SDA and SCL pins cannot be the same (%d).", dev->addr, dev->port, sda_pin); + return ESP_ERR_INVALID_ARG; + } + + // Initialize common fields + i2c_config_t legacy_cfg = { .mode = I2C_MODE_MASTER, + .sda_io_num = sda_pin, // Use locally determined pins + .scl_io_num = scl_pin, // Use locally determined pins + .sda_pullup_en = dev->cfg.sda_pullup_en ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE, + .scl_pullup_en = dev->cfg.scl_pullup_en ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE }; + +#ifdef CONFIG_IDF_TARGET_ESP8266 + // ESP8266 uses clk_stretch_tick instead of master.clk_speed + // Clock speed will be handled during driver installation + uint32_t desired_speed = dev->cfg.master.clk_speed > 0 ? dev->cfg.master.clk_speed : 400000; + ESP_LOGD(TAG, "Final I2C config for port %d: SDA=%d, SCL=%d, speed=%lu (ESP8266)", dev->port, legacy_cfg.sda_io_num, legacy_cfg.scl_io_num, (unsigned long)desired_speed); +#else + // ESP32 family uses master.clk_speed + legacy_cfg.master.clk_speed = dev->cfg.master.clk_speed > 0 ? dev->cfg.master.clk_speed : 400000; + ESP_LOGD(TAG, "Final I2C config for port %d: SDA=%d, SCL=%d, speed=%lu", dev->port, legacy_cfg.sda_io_num, legacy_cfg.scl_io_num, (unsigned long)legacy_cfg.master.clk_speed); +#endif + +#ifdef CONFIG_IDF_TARGET_ESP32 + legacy_cfg.clk_flags = 0; +#endif + + esp_err_t err = ESP_OK; + + // Part 1: Driver Installation / Reconfiguration + if (!cfg_equal(&legacy_cfg, &states[dev->port].config) || !states[dev->port].installed) + { + ESP_LOGD(TAG, "[0x%02x at %d] Reconfiguring I2C driver", dev->addr, dev->port); + + if (states[dev->port].installed) + { + ESP_LOGD(TAG, "Uninstalling previous I2C driver configuration for port %d", dev->port); + i2c_driver_delete(dev->port); + states[dev->port].installed = false; + states[dev->port].ref_count = 0; + } + + vTaskDelay(1); + +// Target-specific driver installation/configuration sequence +#if HELPER_TARGET_IS_ESP32 || HELPER_TARGET_IS_ESP32S2 || HELPER_TARGET_IS_ESP32S3 || HELPER_TARGET_IS_ESP32C3 || HELPER_TARGET_IS_ESP32C6 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + ESP_LOGD(TAG, "Using IDF >= 5.1.0 driver install order for ESP32 family"); + err = i2c_driver_install(dev->port, legacy_cfg.mode, 0, 0, 0); + if (err == ESP_OK) + { + err = i2c_param_config(dev->port, &legacy_cfg); + } +#else + ESP_LOGD(TAG, "Using IDF < 5.1.0 driver install order for ESP32 family"); + err = i2c_param_config(dev->port, &legacy_cfg); + if (err == ESP_OK) + { + err = i2c_driver_install(dev->port, legacy_cfg.mode, 0, 0, 0); + } +#endif +#elif HELPER_TARGET_IS_ESP8266 + ESP_LOGD(TAG, "Using ESP8266 specific driver installation"); + legacy_cfg.clk_stretch_tick = dev->timeout_ticks ? dev->timeout_ticks : I2CDEV_MAX_STRETCH_TIME; + err = i2c_driver_install(dev->port, legacy_cfg.mode); + if (err == ESP_OK) + { + err = i2c_param_config(dev->port, &legacy_cfg); + } + // ESP8266 note: Clock speed is not directly configurable through i2c_config_t + // The desired speed was: %lu Hz", desired_speed +#else + // If legacy mode is off, and target detection fails, this avoids a compile error. + // The legacy driver just won't support any target in this case. + ESP_LOGW(TAG, "i2cdev_legacy.c: No specific target (ESP32/ESP32-S2/ESP32-S3/ESP32-C3/ESP32-C6/ESP8266) detected " + "for driver installation. Legacy driver might be inactive or misconfigured."); + err = ESP_ERR_NOT_SUPPORTED; // Indicate that setup can't proceed. +#endif + + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to install/configure I2C driver for port %d: %d (%s)", dev->port, err, esp_err_to_name(err)); + states[dev->port].installed = false; // Ensure state reflects failure + return err; + } + + memcpy(&states[dev->port].config, &legacy_cfg, sizeof(i2c_config_t)); + states[dev->port].installed = true; + states[dev->port].ref_count++; + + dev->sda_pin = legacy_cfg.sda_io_num; + dev->scl_pin = legacy_cfg.scl_io_num; + + ESP_LOGD(TAG, "I2C driver successfully installed/reconfigured on port %d, ref_count=%" PRIu32, dev->port, states[dev->port].ref_count); + } + else + { + states[dev->port].ref_count++; + ESP_LOGV(TAG, "I2C driver already installed on port %d with matching config, ref_count=%" PRIu32, dev->port, states[dev->port].ref_count); + + dev->sda_pin = states[dev->port].config.sda_io_num; + dev->scl_pin = states[dev->port].config.scl_io_num; + } + +// Part 2: Timeout Configuration (ESP32 family specific hardware timeout) +#if HELPER_TARGET_IS_ESP32 || HELPER_TARGET_IS_ESP32S2 || HELPER_TARGET_IS_ESP32S3 || HELPER_TARGET_IS_ESP32C3 || HELPER_TARGET_IS_ESP32C6 + int current_timeout_hw; + err = i2c_get_timeout(dev->port, ¤t_timeout_hw); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to get HW timeout for port %d: %d (%s)", dev->port, err, esp_err_to_name(err)); + return err; + } + uint32_t timeout_ticks_val = dev->timeout_ticks ? dev->timeout_ticks : I2CDEV_MAX_STRETCH_TIME; + if (timeout_ticks_val != (uint32_t)current_timeout_hw) + { + ESP_LOGV(TAG, "Port %d: Updating HW timeout from %d to %" PRIu32 " ticks", dev->port, current_timeout_hw, timeout_ticks_val); + err = i2c_set_timeout(dev->port, timeout_ticks_val); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to set HW timeout for port %d: %d (%s)", dev->port, err, esp_err_to_name(err)); + return err; + } + ESP_LOGD(TAG, "HW Timeout: ticks = %" PRIu32 " (%" PRIu32 " usec) on port %d", timeout_ticks_val, timeout_ticks_val / 80, dev->port); + } +#endif + + return ESP_OK; +} + +esp_err_t i2c_dev_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type) +{ + if (!dev) + return ESP_ERR_INVALID_ARG; + + SEMAPHORE_TAKE(dev->port); + + esp_err_t res = i2c_setup_port((i2c_dev_t *)dev); + if (res == ESP_OK) + { + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, dev->addr << 1 | (operation_type == I2C_DEV_READ ? 1 : 0), true); + // Alternative Write-style probe for better device compatibility + // many devices don't respond well to blind read probes. + // i2c_master_write_byte(cmd, dev->addr << 1 | 0, true); // Force write bit (0) + i2c_master_stop(cmd); + + res = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); + + i2c_cmd_link_delete(cmd); + } + + SEMAPHORE_GIVE(dev->port); + + return res; +} + +esp_err_t i2c_dev_read(const i2c_dev_t *dev, const void *out_data, size_t out_size, void *in_data, size_t in_size) +{ + if (!dev || !in_data || !in_size) + return ESP_ERR_INVALID_ARG; + + SEMAPHORE_TAKE(dev->port); + + // Use a local status variable to track errors + esp_err_t err = i2c_setup_port((i2c_dev_t *)dev); + if (err == ESP_OK) + { + // Only create a command handle if setup was successful + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + + if (out_data && out_size) + { + // Write phase - typically used to specify a register address + i2c_master_start(cmd); + i2c_master_write_byte(cmd, dev->addr << 1, true); // Addr + Write bit (0) + i2c_master_write(cmd, (void *)out_data, out_size, true); + } + + // Read phase - get data from the device + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (dev->addr << 1) | 1, true); // Addr + Read bit (1) + i2c_master_read(cmd, in_data, in_size, + I2C_MASTER_LAST_NACK); // NACK the last byte to signal end + i2c_master_stop(cmd); + + // Execute the command + err = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "i2c_master_cmd_begin failed for read: %d (%s)", err, esp_err_to_name(err)); + } + + // Always delete the command handle + i2c_cmd_link_delete(cmd); + } + + // Always release the semaphore before returning + SEMAPHORE_GIVE(dev->port); + return err; +} + +esp_err_t i2c_dev_write(const i2c_dev_t *dev, const void *out_reg, size_t out_reg_size, const void *out_data, size_t out_size) +{ + if (!dev) + return ESP_ERR_INVALID_ARG; + if ((!out_reg || !out_reg_size) && (!out_data || !out_size)) + return ESP_ERR_INVALID_ARG; + + SEMAPHORE_TAKE(dev->port); + + // Use a local status variable to track errors + esp_err_t err = i2c_setup_port((i2c_dev_t *)dev); + if (err == ESP_OK) + { + // Only create a command handle if setup was successful + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, dev->addr << 1, true); + + // Write register address/command if provided + if (out_reg && out_reg_size) + { + i2c_master_write(cmd, (void *)out_reg, out_reg_size, true); + } + + // Write data if provided + if (out_data && out_size) + { + i2c_master_write(cmd, (void *)out_data, out_size, true); + } + + i2c_master_stop(cmd); + + // Execute the command + err = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "i2c_master_cmd_begin failed for write: %d (%s)", err, esp_err_to_name(err)); + } + + // Always delete the command handle + i2c_cmd_link_delete(cmd); + } + + // Always release the semaphore before returning + SEMAPHORE_GIVE(dev->port); + return err; +} + +esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, const void *out_data, size_t out_size) +{ + if (!dev || !out_data || !out_size) + return ESP_ERR_INVALID_ARG; + + SEMAPHORE_TAKE(dev->port); + + // Use a local status variable to track errors + esp_err_t err = i2c_setup_port((i2c_dev_t *)dev); + if (err == ESP_OK) + { + // Only create a command handle if setup was successful + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (dev->addr << 1) | I2C_MASTER_WRITE, true); // Addr + Write bit + i2c_master_write_byte(cmd, reg, true); // Register address + if (out_data && out_size) + { + i2c_master_write(cmd, (void *)out_data, out_size, true); // Data to write + } + i2c_master_stop(cmd); + + // Execute the command + err = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "i2c_master_cmd_begin failed for write_reg: %d (%s)", err, esp_err_to_name(err)); + } + + // Always delete the command handle + i2c_cmd_link_delete(cmd); + } + + // Always release the semaphore before returning + SEMAPHORE_GIVE(dev->port); + return err; +} + +esp_err_t i2c_dev_read_reg(const i2c_dev_t *dev, uint8_t reg, void *in_data, size_t in_size) +{ + if (!dev || !in_data || !in_size) + return ESP_ERR_INVALID_ARG; + + SEMAPHORE_TAKE(dev->port); + + // Use a local status variable to track errors + esp_err_t err = i2c_setup_port((i2c_dev_t *)dev); + if (err == ESP_OK) + { + // Only create a command handle if setup was successful + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (dev->addr << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(cmd, reg, true); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (dev->addr << 1) | I2C_MASTER_READ, true); + i2c_master_read(cmd, in_data, in_size, I2C_MASTER_LAST_NACK); + i2c_master_stop(cmd); + + // Execute the command + err = i2c_master_cmd_begin(dev->port, cmd, pdMS_TO_TICKS(CONFIG_I2CDEV_TIMEOUT)); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "i2c_master_cmd_begin failed for read_reg: %d (%s)", err, esp_err_to_name(err)); + } + + // Always delete the command handle + i2c_cmd_link_delete(cmd); + } + + // Always release the semaphore before returning + SEMAPHORE_GIVE(dev->port); + return err; +} + +// Implementation of i2c_dev_check_present (updated version of i2c_dev_probe) using legacy I2C driver +esp_err_t i2c_dev_check_present(const i2c_dev_t *dev) +{ + if (!dev) + return ESP_ERR_INVALID_ARG; + + ESP_LOGV(TAG, "[0x%02x at %d] Checking device presence (legacy driver)...", dev->addr, dev->port); + + // Use the exact same pattern as i2c_dev_probe with WRITE operation to ensure consistent behavior + return i2c_dev_probe(dev, I2C_DEV_WRITE); +}