i2cdev: ESP-IDF I2C master thread-safe utilities
Defines
- I2CDEV_MAX_STRETCH_TIME
- CONFIG_I2CDEV_TIMEOUT
- CONFIG_I2CDEV_NOLOCK
- CONFIG_I2CDEV_MAX_DEVICES_PER_PORT
- CONFIG_I2CDEV_DEFAULT_SDA_PIN
- CONFIG_I2CDEV_DEFAULT_SCL_PIN
- I2C_DEV_TAKE_MUTEX(dev)
Take device mutex with error checking.
Note
With
CONFIG_I2CDEV_NOLOCKenabled this is a no-op that returnsESP_OK; onlydevis validated.
- I2C_DEV_GIVE_MUTEX(dev)
Give device mutex with error checking.
Note
With
CONFIG_I2CDEV_NOLOCKenabled this is a no-op that returnsESP_OK; onlydevis validated.
- I2C_DEV_CHECK(dev, X)
Execute operation, assuming the device mutex is held.
Gives the mutex ONLY on error.
On success the mutex stays taken, so the caller must release it with I2C_DEV_GIVE_MUTEX(). With
CONFIG_I2CDEV_NOLOCKthat release is a no-op.
- I2C_DEV_CHECK_LOGE(dev, X, msg, ...)
Execute operation, assuming the device mutex is held.
Gives the mutex ONLY on error and logs the failure.
Like I2C_DEV_CHECK(), the mutex stays taken on success, so the caller must call I2C_DEV_GIVE_MUTEX(). The failure is logged through
TAG, which has to be defined in the scope where the macro is used.Enums
Functions
- esp_err_t i2cdev_init(void)
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.
Note
Calling it more than once is allowed and costs nothing beyond creating the port mutexes that are missing, so it is safe to call again after i2cdev_done(). A port whose bus is still installed (because its cleanup failed) keeps that bus instead of being reset.
- Returns:
ESP_OKon success,ESP_ERR_NO_MEMif a port mutex could not be created
- esp_err_t i2cdev_done(void)
Release I2C subsystem (deletes all devices, buses, and mutexes)
Note
Call this when no more I2C operations will be performed to clean up resources.
Note
Cleanup is best effort and may be partial. If a bus or one of its devices cannot be released (for example, the bus was handed out via i2cdev_get_shared_handle() and is still used by another component), the port state is kept intact and its mutex is not deleted, so the call can be repeated once the obstacle is gone. Ports whose mutex is held by another task are skipped for the same reason.
- Returns:
ESP_OK if every port was released; on a partial failure the first error is returned (ESP_ERR_TIMEOUT if a port mutex could not be taken, otherwise the error reported by the I2C driver). A non-ESP_OK result means the subsystem was not fully released and i2cdev_done() may be called again.
- esp_err_t i2c_dev_create_mutex(i2c_dev_t *dev)
Create mutex for device descriptor.
Note
The descriptor fields (port, address, pins, speed) are read by the data transfer functions, so they must be filled before the first I/O operation on the device, not before this call.
Note
This call only creates the per-device mutex and normalises dev->addr_bit_len: it does not touch the bus. The port mutexes are created by i2cdev_init(), the bus is created lazily by the first operation on the port (i2c_dev_check_present() and i2c_dev_probe() create the bus as well), and the device handle is added to the bus on the first real transfer. Probe calls do not attach the device.
- Parameters:
dev – Pointer to device descriptor
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevis NULL,ESP_ERR_NO_MEMif the device mutex could not be created. WithCONFIG_I2CDEV_NOLOCKno mutex is created, so this call cannot fail withESP_ERR_NO_MEMin that configuration.
- esp_err_t i2c_dev_delete_mutex(i2c_dev_t *dev)
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.
Note
The bus itself is NOT deleted here: it lives from i2cdev_init() (or the first I2C operation) until i2cdev_done(). Removing the last tracked device therefore leaves the shared bus available for any other consumer of i2cdev_get_shared_handle(). A device handle whose removal failed is preserved instead of being dropped: the failure is reported and the operation can be retried.
Note
With
CONFIG_I2CDEV_NOLOCKenabled no per-device mutex exists, but the cleanup described above is still performed in that configuration.Note
If the descriptor’s port is out of range, or i2cdev_init() was never called, nothing port-scoped can be cleaned up: the call logs a warning, deletes only the per-device mutex and returns
ESP_OK.
- Parameters:
dev – Pointer to device descriptor
- Returns:
ESP_OKon success or when the port was never initialised (see the note above),ESP_ERR_INVALID_ARGifdevis NULL,ESP_ERR_TIMEOUTif the port mutex could not be taken, otherwise the error reported by the I2C driver while removing the device
- esp_err_t i2c_dev_take_mutex(i2c_dev_t *dev)
Take device mutex.
Note
With
CONFIG_I2CDEV_NOLOCKenabled this is a no-op that returnsESP_OK.
- Parameters:
dev – Pointer to device descriptor
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevis NULL,ESP_ERR_INVALID_STATEif the device mutex was not created,ESP_ERR_TIMEOUTif the mutex was not released withinCONFIG_I2CDEV_TIMEOUTmilliseconds
- esp_err_t i2c_dev_give_mutex(i2c_dev_t *dev)
Give device mutex.
Note
With
CONFIG_I2CDEV_NOLOCKenabled this is a no-op that returnsESP_OK.
- Parameters:
dev – Pointer to device descriptor
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevis NULL,ESP_ERR_INVALID_STATEif the device mutex was not created,ESP_FAILif the mutex was not taken before
- esp_err_t i2c_dev_check_present(const i2c_dev_t *dev)
Check the availability of a device on the I2C bus.
Probes the bus with a bare START/STOP pair and reports whether the device ACKs. No device handle is added to the bus and the per-device mutex is not used, so the device list stays untouched. Two side effects remain: the port is set up if this is the first operation on it (the bus is created and the internal pullups are configured from the descriptor), and the probe always runs at 100 kHz, independent of dev->cfg.master.clk_speed. dev->addr_bit_len is ignored: the address is sent as a 7-bit address.
- Parameters:
dev – Pointer to the device descriptor. Pins and address must be configured.
- Returns:
ESP_OKif the device ACKs (is present), an error code otherwise.ESP_ERR_INVALID_ARGifdevis NULL or its port is out of range,ESP_ERR_INVALID_STATEif the port was never initialized (call i2cdev_init() first),ESP_ERR_TIMEOUTif the port mutex could not be taken. If the bus answered but the device did not ACK, the error comes from the I2C driver (typicallyESP_ERR_NOT_FOUNDorESP_ERR_TIMEOUT).
- esp_err_t i2c_dev_probe(const i2c_dev_t *dev, i2c_dev_type_t operation_type)
Check the availability of a device on the I2C bus.
Legacy-compatible entry point. The legacy implementation issues an operation of
operation_type; the i2c_master based implementation ignores that argument and forwards the call to i2c_dev_check_present().
- Parameters:
dev – Device descriptor.
operation_type – Operation type (I2C_DEV_WRITE or I2C_DEV_READ), ignored by the i2c_master based implementation.
- Returns:
Same codes as i2c_dev_check_present():
ESP_OKif the device ACKs,ESP_ERR_INVALID_ARGfor a NULL descriptor or a port out of range,ESP_ERR_INVALID_STATEif the port was never initialised,ESP_ERR_TIMEOUTif the port mutex could not be taken, otherwise the error reported by the I2C driver (ESP_ERR_NOT_FOUNDwhen the device does not ACK).
- 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)
Read from device.
out_dataandout_sizecount only together: if either of them is empty, a plain read is performed and nothing is written first. Reading a register therefore needs both; i2c_dev_read_reg() is the shorthand for it.Note
The call is retried up to four times (I2C_MAX_RETRIES + 1) with 20, 40 and 80 ms of backoff, and the device is set up again before every attempt. The retry loop also covers errors that cannot go away, such as
ESP_ERR_INVALID_ARG. In the worst case the call blocks for about four timesCONFIG_I2CDEV_TIMEOUTplus the backoff delays.
- Parameters:
dev – Pointer to device descriptor
out_data – [in] Data to write before reading, or NULL for a plain read
out_size – Size of data to write before reading
in_data – [out] Buffer to store data read
in_size – Number of bytes to read
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevorin_datais NULL, ifin_sizeis 0 or if the descriptor’s port is out of range,ESP_ERR_INVALID_STATEif i2cdev_init() was not called,ESP_ERR_TIMEOUTif the port mutex could not be taken, otherwise the error reported by the I2C driver
- 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)
Write to device.
out_regandout_reg_sizecount only together, the same forout_dataandout_size:an empty half of a pair is ignored.Note
The retry policy is the same as in i2c_dev_read().
- Parameters:
dev – Pointer to device descriptor
out_reg – [in] Register address to write to, or NULL to write data as is
out_reg_size – Size of register address
out_data – [in] Data to write, or NULL to write only the register
out_size – Size of data to write
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevis NULL, if both pairs are empty or if the descriptor’s port is out of range,ESP_ERR_INVALID_STATEif i2cdev_init() was not called,ESP_ERR_TIMEOUTif the port mutex could not be taken, otherwise the error reported by the I2C driver
- esp_err_t i2c_dev_read_reg(const i2c_dev_t *dev, uint8_t reg, void *data, size_t size)
Read from device register (8-bit register address)
Note
The register address is a single byte, sent as a write before the repeated START of the read. The retry policy is the same as in i2c_dev_read().
- Parameters:
dev – Pointer to device descriptor
reg – Command to write before reading
data – [out] Buffer to store data
size – Number of bytes to read
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevordatais NULL, ifsizeis 0 or if the descriptor’s port is out of range,ESP_ERR_INVALID_STATEif i2cdev_init() was not called,ESP_ERR_TIMEOUTif the port mutex could not be taken, otherwise the error reported by the I2C driver
- esp_err_t i2c_dev_write_reg(const i2c_dev_t *dev, uint8_t reg, const void *data, size_t size)
Write to device register (8-bit register address)
Note
The register address is a single byte prepended to
data. To write only the register byte use i2c_dev_write(dev, ®, 1, NULL, 0). The retry policy is the same as in i2c_dev_read().
- Parameters:
dev – Pointer to device descriptor
reg – Command to write before writing data
data – Buffer with data to write
size – Number of bytes to write
- Returns:
ESP_OKon success,ESP_ERR_INVALID_ARGifdevordatais NULL, ifsizeis 0 or if the descriptor’s port is out of range,ESP_ERR_INVALID_STATEif i2cdev_init() was not called,ESP_ERR_TIMEOUTif the port mutex could not be taken, otherwise the error reported by the I2C driver
Get shared I2C bus handle for external ESP-IDF components.
This function allows external ESP-IDF components (like esp_lcd) to access the i2cdev-managed I2C bus handle for shared bus operations. The bus lock of the ESP-IDF driver serialises transactions, so i2cdev-based sensors and native ESP-IDF components can share one bus. Only the bus is covered by that: devices added by an external component are unknown to i2cdev, so removing them stays the caller’s job (see the cleanup step in the example).
// 2. Describe a device on the port; the fields are read by the first operation i2c_dev_t sensor = { .port = I2C_NUM_0, .addr = 0x29, // TSL2591 light sensor .cfg = { .sda_io_num = GPIO_NUM_21, .scl_io_num = GPIO_NUM_22, .master.clk_speed = 400000 } }; i2c_dev_create_mutex(&sensor); // The bus does not exist until the first operation on the port: this call // creates it. Its result is not checked on purpose - any transfer that sets // the port up does the job, even one that fails to find the device. (void)i2c_dev_check_present(&sensor);
// 3. Get the shared bus handle i2c_master_bus_handle_t bus_handle; ESP_ERROR_CHECK(i2cdev_get_shared_handle(I2C_NUM_0, (void **)&bus_handle));
// 4. Use it with esp_lcd or other ESP-IDF components esp_lcd_panel_io_handle_t io_handle; esp_lcd_panel_io_i2c_config_t io_config = { .dev_addr = 0x3C, // SSD1306 display .control_phase_bytes = 1, .dc_bit_offset = 6, .lcd_cmd_bits = 8, .lcd_param_bits = 8, }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(bus_handle, &io_config, &io_handle));
// 5. Cleanup, in this order esp_lcd_panel_io_del(io_handle); // external device leaves the bus i2c_dev_delete_mutex(&sensor); // per-device mutex and device handle i2cdev_done(); // now the bus can be deleted
// Both the sensor and display use the same I2C bus. Transactions are // serialised by the ESP-IDF bus lock; per-device serialisation stays the // caller’s job: an i2c_dev_t handle must be used under // I2C_DEV_TAKE_MUTEX()/I2C_DEV_GIVE_MUTEX(), and the external component // serialises its own device handle. Delete the external device before // i2cdev_done(): i2cdev does not track it and cannot remove it from the bus.
Note
USAGE PATTERN:
Initialize i2cdev subsystem with i2cdev_init()
Create at least one i2c_dev_t device on the desired port and perform at least one I2C operation on it (the bus is created lazily, on the first operation)
Call this function to retrieve the bus handle
Pass the handle to ESP-IDF components (e.g., esp_lcd_new_panel_io_i2c)
Note
USAGE EXAMPLE: // 1. Initialize i2cdev i2cdev_init();
Warning
The returned handle is managed by i2cdev. Do NOT call i2c_del_master_bus() on it directly: the bus lives until i2cdev_done(), which releases it.
- Parameters:
port – I2C port number (e.g., I2C_NUM_0)
bus_handle – [out] Pointer to store the bus handle
- Returns:
ESP_OK on success
- Returns:
ESP_ERR_INVALID_ARG if port is invalid or bus_handle is NULL
- Returns:
ESP_ERR_INVALID_STATE if i2cdev_init() was not called or bus for this port is not initialized yet
- Returns:
ESP_ERR_TIMEOUT if port mutex could not be acquired
- struct i2c_dev_t
#include <i2cdev.h>I2C device descriptor.
This structure supports both legacy ESP-IDF I2C driver and modern i2c_master driver.
REQUIRED (set by user):
dev->port: port number, 0 .. I2C_NUM_MAX-1
dev->addr: device I2C address (for example 0x48)
dev->cfg.sda_io_num: SDA pin, -1 means “use the Kconfig default”
dev->cfg.scl_io_num: SCL pin, -1 means “use the Kconfig default”
OPTIONAL (set by user if needed):
dev->addr_bit_len: address format, defaults to 7-bit
dev->cfg.master.clk_speed: clock speed in Hz, 0 means 400000 with a warning
dev->cfg.sda_pullup_en: enable internal SDA pullup
dev->cfg.scl_pullup_en: enable internal SCL pullup
dev->timeout_ticks: clock stretching timeout, legacy driver only
AUTO-POPULATED (library fills these):
dev->dev_handle: device handle, created lazily, modern driver only
dev->sda_pin: actual SDA pin used by the bus
dev->scl_pin: actual SCL pin used by the bus
Note
INITIALIZATION CHECKLIST - fill the descriptor fields before the first I/O operation on it, not before i2c_dev_create_mutex().
Note
dev->mutex is NOT populated automatically: it is created by i2c_dev_create_mutex() and does not exist at all with CONFIG_I2CDEV_NOLOCK enabled.
Note
dev->cfg.clk_flags exists only to keep the i2c_config_t compatible layout; both implementations ignore it.
Note
One port is bound to one pin pair: the first operation fixes SDA/SCL for the port, and a device that asks for other pins on the same port fails with ESP_ERR_INVALID_STATE.
Note
10-bit addressing requires SOC_I2C_SUPPORT_10BIT_ADDR; elsewhere addr_bit_len is forced to 7-bit (the driver logs a warning).
Note
LIFETIME: the library keeps a pointer to the descriptor once the device has been added to the bus. The descriptor must stay valid until i2c_dev_delete_mutex() was called for it (or i2cdev_done() released the port): do not copy, move or free it while registered, i2cdev_done() walks the registered descriptors.
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
Public Members
- i2c_port_t port
I2C port number: 0 .
. I2C_NUM_MAX-1
- 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 where SOC_I2C_SUPPORT_10BIT_ADDR.
- SemaphoreHandle_t mutex
Device mutex - Created by i2c_dev_create_mutex(), unused with CONFIG_I2CDEV_NOLOCK.
- 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.
- uint32_t timeout_ticks
Clock stretching timeout - Legacy driver only.
- 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
Reserved for i2c_config_t layout compatibility: ignored by both drivers.
- uint32_t clk_speed
Clock speed in Hz, 0 = 400000 with a warning in the log.