I2C Master Bus multiple peripherals with same address

FrankNabas
Posts: 6
Joined: Wed Jun 18, 2025 12:44 am

I2C Master Bus multiple peripherals with same address

Postby FrankNabas » Wed Jun 18, 2025 1:13 am

Greetings friends.

I am currently working on a project where I want to connect 3 environment sensors to the same board.
The peripheral is the AHT21 + ENS160 which consists of two sensors, the AHT21 temperature and relative humidity sensor and the ENS160 "air quality" sensor. Both sensors works via I2C.
I have been able to successfully initialize and read from an individual sensor, now I want to connect more of them on the same board.
The problem of course is that they share the same I2C address and you cannot change them.
I am aware of the I2C multiplexer or I2C address translator, but I want to avoid adding new hardware and keep everything in software.

I have found a post where they accomplished a "sort of multiplexing" using the old i2c.h driver.
After initializing the driver and adding the device they used "i2c_set_pin" to change the SDA and SCL pins before reading from the device.
The problem is that it uses the old driver, and the library I have to read the ENS160 uses the new driver, thus I get an error saying that I can't use both drivers at the same time.

The question is, is there a way of accomplishing a similar thing using the new driver defined in "i2c_master.h"?

I also tried a very rudimentary way of reading the devices by creating and removing the master bus handle in between reads, however I get a weird behavior under the monitor where it prints weird characters and issues a CPU software reset. I thought that it might be due the fast creating/removing master buses, but adding a delay of up to 500ms in between didn't solved.
Here's the result I get from the monitor:

Image

This is my code (apologies if it's the wrong format, first post here) (in the test I only read from the AHT21 sensor because the ENS160 takes an hour to start up):

Code: Select all

// Test I2C mux.
#define I2C_MASTER_PORT (static_cast<i2c_port_t>(0))

#define I2C_SDA1 (static_cast<gpio_num_t>(21))
#define I2C_SCL1 (static_cast<gpio_num_t>(22))

#define I2C_SDA2 (static_cast<gpio_num_t>(16))
#define I2C_SCL2 (static_cast<gpio_num_t>(17))

#define I2C_SDA3 (static_cast<gpio_num_t>(3))
#define I2C_SCL3 (static_cast<gpio_num_t>(1))

struct test_aht2_result_t {
    sens_aht21_result_t result1;
    sens_aht21_result_t result2;
    sens_aht21_result_t result3;
};

static bool first_read = true;

/**
 * @brief Initializes I2C by creating a new master bus.
 * 
 * @param[in] sda The SDA GPIO number.
 * @param[in] slc The SCL GPIO number.
 * @param[out] bus_handle The output I2C Master Bus Handle.
 * 
 * @returns Nothing.
 */
void test_init_i2c_driver(gpio_num_t sda, gpio_num_t scl, i2c_master_bus_handle_t* bus_handle) {
    i2c_master_bus_config_t bus_config = {
        .i2c_port                      = I2C_MASTER_PORT,
        .sda_io_num                    = sda,
        .scl_io_num                    = scl,
        .clk_source                    = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt             = 7,
    };

    bus_config.flags.enable_internal_pullup = true;
    
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, bus_handle));
}

/**
 * @brief Switches I2C master buses by deleting the current bus and creating a new one.
 * 
 * @param[inout] bus_handle On input the current bus handle. On output the new bus handle.
 * @param[in] new_sda The new SDA GPIO number.
 * @param[in] new_scl The new SCL GPIO number.
 * 
 * @returns Nothing.
 */
void test_i2c_bus_switcharoo(i2c_master_bus_handle_t* bus_handle, gpio_num_t new_sda, gpio_num_t new_scl) {
    ESP_ERROR_CHECK(i2c_del_master_bus(*bus_handle));

    // Attempt to solve the CPU SW reset behavior with a delay between I2C buses creation/deletion.
    vTaskDelay(pdMS_TO_TICKS(500));

    i2c_master_bus_config_t bus_config = {
        .i2c_port                      = I2C_MASTER_PORT,
        .sda_io_num                    = new_sda,
        .scl_io_num                    = new_scl,
        .clk_source                    = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt             = 7,
    };

    bus_config.flags.enable_internal_pullup = true;
    
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, bus_handle));
}

/**
 * @brief Initializes the AHT21 sensor.
 * 
 * @param[in] bus_handle The I2C Master Bus Handle.
 * @param[out] device_handle The I2C Device Handle.
 * 
 * @returns Nothing.
 */
void test_init_aht21(i2c_master_bus_handle_t bus_handle, i2c_master_dev_handle_t* device_handle) {
    i2c_device_config_t aht21_device_config = {
        .dev_addr_length  = I2C_ADDR_BIT_LEN_7,
        .device_address   = SENS_AHT21_I2C_ADDRESS,
        .scl_speed_hz     = SENS_I2C_DEFAULT_SCL_CLK_SPEED,
    };

    ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &aht21_device_config, device_handle));

    uint8_t aht21_init_attempt_count = 1;
    uint8_t aht21_init_cmd[] = { SENS_AHT21_CMD_INIT, 0x08, 0x00 };

    ESP_ERROR_CHECK(i2c_master_transmit(*device_handle, aht21_init_cmd, sizeof(aht21_init_cmd), SENS_AHT21_TRANS_TIMEOUT_MS));

    vTaskDelay(pdMS_TO_TICKS(40));
}

/**
 * @brief Switches sensors by creating a new I2C master bus and initializing the new sensor.
 * 
 * @param[inout] bus_handle On input the old I2C bus handle. On output the new bus handle.
 * @param[inout] device_handle On input the old I2C device handle. On output the new device handle.
 * @param[in] new_sda The new SDA GPIO number.
 * @param[in] new_scl The new SCL GPIO number.
 * 
 * @returns Nothing.
 */
void test_sensor_switch(i2c_master_bus_handle_t* bus_handle, i2c_master_dev_handle_t* device_handle, gpio_num_t new_sda, gpio_num_t new_scl) {
    ESP_ERROR_CHECK(i2c_master_bus_rm_device(*device_handle));
    test_i2c_bus_switcharoo(bus_handle, new_sda, new_scl);
    test_init_aht21(*bus_handle, device_handle);
}

/**
 * @brief Reads all sensors by switching between active I2C buses.
 * 
 * @param[out] result The reading results.
 * 
 * @returns Nothing.
 */
void test_read_all(test_aht2_result_t& result) {
    i2c_master_bus_handle_t bus_handle;
    i2c_master_dev_handle_t device_handle;
    
    if (first_read) {
        vTaskDelay(pdMS_TO_TICKS(20));

        test_init_i2c_driver(I2C_SDA1, I2C_SCL1, &bus_handle);
        test_init_aht21(bus_handle, &device_handle);

        // The function 'sens_aht21_read' is defined in another header file, but it reads from the sensor using I2C.
        ESP_ERROR_CHECK(sens_aht21_read(device_handle, result.result1));
        first_read = false;
    }
    else {
        test_sensor_switch(&bus_handle, &device_handle, I2C_SDA1, I2C_SCL1);
        ESP_ERROR_CHECK(sens_aht21_read(device_handle, result.result1));
    }
    
    test_sensor_switch(&bus_handle, &device_handle, I2C_SDA2, I2C_SCL2);
    ESP_ERROR_CHECK(sens_aht21_read(device_handle, result.result2));

    test_sensor_switch(&bus_handle, &device_handle, I2C_SDA3, I2C_SCL3);
    ESP_ERROR_CHECK(sens_aht21_read(device_handle, result.result2));
}

/**
 * @brief Main test function to be called on 'app_main'. It reads from all 3 sensors and writes the result to the console.
 */
void test_main_task() {
    for (;;) {
        test_aht2_result_t result{ };
        test_read_all(result);

        ESP_LOGI(tag_main, "SENSOR 1: Temperature: %.2f°C; RH: %.2f%%", result.result1.temperature, result.result1.relative_humidity);
        ESP_LOGI(tag_main, "SENSOR 2: Temperature: %.2f°C; RH: %.2f%%", result.result2.temperature, result.result2.relative_humidity);
        ESP_LOGI(tag_main, "SENSOR 3: Temperature: %.2f°C; RH: %.2f%%", result.result3.temperature, result.result3.relative_humidity);

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

FrankNabas
Posts: 6
Joined: Wed Jun 18, 2025 12:44 am

Re: I2C Master Bus multiple peripherals with same address

Postby FrankNabas » Wed Jun 18, 2025 5:16 pm

EDIT: I was able to read the 3 sensors by deleting/creating the master bus, I was using TX/RX as GPIO for one of the sensors and it was messing with the monitor.

I'm still curious if we can do it setting the pins with "i2c_master.h". Recreating the bus every read is not the most efficient thing.

MicroController
Posts: 2672
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: I2C Master Bus multiple peripherals with same address

Postby MicroController » Thu Jun 19, 2025 6:41 am

ESP32 has 2 I2C controller(s) (also called port), responsible for handling communication on the I2C bus.
(https://docs.espressif.com/projects/esp ... s/i2c.html)
So you can set up 2 independent I2C buses.

You should also be able to 'time-multiplex' one I2C bus to different pins by setting up a second SCL pin and connecting and disconnecting the SCL signal back and forth.

FrankNabas
Posts: 6
Joined: Wed Jun 18, 2025 12:44 am

Re: I2C Master Bus multiple peripherals with same address

Postby FrankNabas » Sun Jun 22, 2025 7:19 am

Hi, thank you for your response, apologies on my delay.

I am aware that we have two I2C ports available, but I'm trying to use 3 (or more) devices.
However I was able to make it work using your suggestion to connect/disconnect the GPIO pins.
After implementing the solution I had to change my CMakeLists.txt on the idf_component_register -> INCLUDE_DIRS to include the paths to some of the esp-idf private headers. The ones I used were 'esp_driver_i2c/i2c_private.h' and 'esp_driver_gpio/include/esp_private/gpio.h'.

You can find my implementation here:
https://github.com/FranciscoNabas/ESP-I ... ontext.cpp

The only problem I had was with the i2c_private.h header where it uses the keyword '_Atomic' that the compiler can't find the definition. For testing only I removed these references and it worked like a charm. I will research more deeply to see if I can find a better solution.
Example of the '_Atomic' usage (from i2c_private.h):

Code: Select all

struct i2c_master_bus_t {
    i2c_bus_t *base;                                                 // bus base class
    SemaphoreHandle_t bus_lock_mux;                                  // semaphore to lock bus process
    int cmd_idx;                                                     //record current command index, for master mode
    _Atomic i2c_master_status_t status;                              // record current command status, for master mode
    i2c_master_event_t event;                                        // record current i2c bus event
    int rx_cnt;                                                      // record current read index, for master mode
    i2c_transaction_t i2c_trans;                                     // Pointer to I2C transfer structure
    i2c_operation_t i2c_ops[I2C_STATIC_OPERATION_ARRAY_MAX];         // I2C operation array
    _Atomic uint16_t trans_idx;                                      // Index of I2C transaction command.
    SemaphoreHandle_t cmd_semphr;                                    // Semaphore between task and interrupt, using for synchronizing ISR and I2C task.
    QueueHandle_t event_queue;                                       // I2C event queue
    uint32_t read_buf_pos;                                           // Read buffer position
    bool contains_read;                                              // Whether command array includes read operation, true: yes, otherwise, false.
    uint32_t read_len_static;                                        // Read static buffer length
    uint32_t w_r_size;                                               // The size send/receive last time.
    bool trans_over_buffer;                                          // Data length is more than hardware fifo length, needs interrupt.
    bool async_trans;                                                // asynchronous transaction, true after callback is installed.
    bool ack_check_disable;                                          // Disable ACK check
    volatile bool trans_done;                                        // transaction command finish
    bool bypass_nack_log;                                             // Bypass the error log. Sometimes the error is expected.
    SLIST_HEAD(i2c_master_device_list_head, i2c_master_device_list) device_list;      // I2C device (instance) list
    // async trans members
    bool async_break;                                                // break transaction loop flag.
    i2c_addr_bit_len_t addr_10bits_bus;                              // Slave address is 10 bits.
    size_t queue_size;                                               // I2C transaction queue size.
    size_t num_trans_inflight;                                       // Indicates the number of transactions that are undergoing but not recycled to ready_queue
    size_t num_trans_inqueue;                                        // Indicates the number of transaction in queue transaction.
    void* queues_storage;                                            // storage of transaction queues
    bool sent_all;                                                   // true if the queue transaction is sent
    bool in_progress;                                                // true if current transaction is in progress
    bool trans_finish;                                               // true if current command has been sent out.
    bool queue_trans;                                                // true if current transaction is in queue
    bool new_queue;                                                  // true if allow a new queue transaction
    QueueHandle_t trans_queues[I2C_TRANS_QUEUE_MAX];                 // transaction queues.
    StaticQueue_t trans_queue_structs[I2C_TRANS_QUEUE_MAX];          // memory to store the static structure for trans_queues
    i2c_operation_t (*i2c_async_ops)[I2C_STATIC_OPERATION_ARRAY_MAX]; // pointer to asynchronous operation(s).
    uint32_t ops_prepare_idx;                                        // Index for the operations can be written into `i2c_async_ops` array.
    uint32_t ops_cur_size;                                           // Indicates how many operations have already put in `i2c_async_ops`.
    i2c_transaction_t i2c_trans_pool[];                              // I2C transaction pool.
};
Thank you very much!

MicroController
Posts: 2672
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: I2C Master Bus multiple peripherals with same address

Postby MicroController » Mon Jun 23, 2025 9:21 am

The only problem I had was with the i2c_private.h header where it uses the keyword '_Atomic' that the compiler can't find the definition.
#include <stdatomic.h> before including i2c_private.h.

"esp_driver_gpio/include/esp_private/gpio.h" should work without changing the include path via #include "esp_private/gpio.h".

FrankNabas
Posts: 6
Joined: Wed Jun 18, 2025 12:44 am

Re: I2C Master Bus multiple peripherals with same address

Postby FrankNabas » Tue Jun 24, 2025 11:37 pm

Awesome, thank you very much for the help!

eriksl
Posts: 199
Joined: Thu Dec 14, 2023 3:23 pm
Location: Netherlands

Re: I2C Master Bus multiple peripherals with same address

Postby eriksl » Mon Jul 21, 2025 10:13 am

My 2cts

If you really can't change one of the sensors' addresses, use a bus multiplexer. A common/popular one can multiplex up to 8 busses. Highly recommended,

Who is online

Users browsing this forum: No registered users and 3 guests