/** * @file i2cdev.h * @defgroup i2cdev i2cdev * @{ * * ESP-IDF I2C master thread-safe functions for communication with I2C slave * * 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 // 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 // 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) #define I2CDEV_MAX_STRETCH_TIME I2C_TIME_OUT_REG_V #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 { // ═══ 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 // ═══ 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) uint32_t clk_flags; //!< Bitwise of ``I2C_SCLK_SRC_FLAG_**FOR_DFS**`` for clk source choice 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 Initialize I2C subsystem (port mutexes and internal states) * * @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(void); /** * @brief Release I2C subsystem (deletes all devices, buses, and mutexes) * * @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(void); /** * @brief Create mutex for device descriptor and register device * * @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 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 and perform device cleanup * * @note This function performs cleanup tasks including removing the device from the * I2C bus, deregistering it, and deleting its mutex. * * @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 * * @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 * * @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 a device on the I2C bus (New Driver) - legacy's i2c_dev_probe function equivalent. * * 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 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_check_present(const i2c_dev_t *dev); /** * @brief Check the availability of a device on the I2C bus (Legacy Driver). * * 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 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_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type); /** * @brief Read from device * * @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 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[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); /** * @brief Read from device register (8-bit register address) * * @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 *data, size_t size); /** * @brief Write to device register (8-bit register address) * * @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. */ #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 } #endif /**@}*/ #endif /* __I2CDEV_H__ */