ESP32 (23) – I2C basic

luca 09/10/2017 2

In today’s tutorial I’ll explain you how to interface the esp32 chip to external devices (sensors, displays…) using a very widespread bus: the I2C bus.


I2C (pronounced i-squared-c) is a serial communication bus – invented by Philips in 1982 – that allows two or more devices to communicate. The devices connected to the bus can act as masters (devices that “control” the bus) or slaves. Usually, a bus has only one master and one or more slaves, but the standard allows also more complex topologies. Each slave device must have an unique address.

Two different transmission speeds are available: standard (100Kbit/s) and fast (400Kbit/s).

The I2C bus requires only two communication lines that connect the devices:

  • SDA, Serial DAta – where data transit
  • SCLSerial CLock – where the master generates the clock signal

The two lines must be connected to a reference voltage (Vdd) via pull-up resistors:


If you want to learn more about the I2C bus, I strongly suggest the website


The esp32 chip offers two I2C controllers, that can act both as master and slave and communicate in standard and fast speed.

The I2C controllers are internally connected to the IO_MUX matrix, so – as I explained in a previous post – you can assign different pins (with some exceptions) to them in your program.

the esp-idf framework includes a driver which makes it easy to work with those controllers, without worring about how the different registers have to be configured. To use the driver in your program, you only need to include its header file:

#include "driver/i2c.h"

First you have to configure the controller (port) you want to use. The configuration is performed with the i2c_param_config() method, which accepts as parameter an i2c_config_t struct that contains the settings for the controller:

esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t* i2c_conf);

The two available ports are listed in an enum variable in the i2c.h file:


The i2c_config_t struct is also defined in the same header file:

typedef struct{
  i2c_mode_t mode
  gpio_num_t sda_io_num;
  gpio_pullup_t sda_pullup_en;
  gpio_num_t scl_io_num;
  gpio_pullup_t scl_pullup_en;
  union {
    struct {
      uint32_t clk_speed;
    } master;
    struct {
      uint8_t addr_10bit_en;
      uint16_t slave_addr;
    } slave;
} i2c_config_t;

Here’s the meaning of the different parameters:

  • mode is the working mode of the controller (it can be I2C_MODE_SLAVE or I2C_MODE_MASTER)
  • sda_io_num and scl_io_num configure the pins connected to SDA and SCL signals
  • sda_pullup_en and scl_pullup_en allow to enable or disable the internal pull-up resistors (possible values: GPIO_PULLUP_DISABLE or GPIO_PULLUP_ENABLE)
  • master.clk_speed is the speed in hertz of the clock if in master mode (100000 for standard and 400000 for fast speed)
  • slave.slave_addr is the device address if in slave mode
  • slave.addr_10bit_en tells the controller to use an extended (10 instead of 7bits) address (if value is 0, the 10bit address mode is disabled)

pin choice

The esp32 chip allows, using the IO_MUX matrix, to assign “almost” all the pins to the two I2C controllers. The I2C driver is able to verify if the pins specified in the configuration can be used; if not, it stops the program with an error.

For example, if you want to configure the first I2C controller in master mode, with standard speed and use pins 18 and 19 without enabling the internal pull-up resistors, this is your code:

i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = 18;
conf.scl_io_num = 19;
conf.sda_pullup_en = GPIO_PULLUP_DISABLE;
conf.scl_pullup_en = GPIO_PULLUP_DISABLE;
conf.master.clk_speed = 100000;
i2c_param_config(I2C_NUM_0, &conf);

Once configured the controller, you can install the driver with i2c_driver_install():

esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, 
 size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags)

Besides the controller number and its mode, you have to specify the size for the transmitting and receiving buffers (only in slave mode) and additional flags used to allocate the interrupt (usually this parameter is 0).

For the controller configured in the example above, the driver is installed as it follows:

i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0)


Let’s now learn how to use the controller in master mode, to send commands and receive data from a slave.

First, you have to create a command link, that is a “logical” element that will contain the list of actions to be performed to interact with the slave device. The command link is created by the i2c_cmd_link_create() method which returns a pointer to the command link handler:

i2c_cmd_handle_t cmd = i2c_cmd_link_create();

You can now use some methods to add actions to the command link:

  • i2c_master_start and i2c_master_stop
  • i2c_master_write and i2c_master_write_byte
  • i2c_master_read and i2c_master_read_byte

To understand their meaning, first you have to learn how a master device communicate with slaves. First, the master sends on the bus the START signal, followed by the address (7bits) of the slave device and a bit that specifies the requested operation (0 for WRITE, 1 for READ). After every byte sent by the master (including the one that contains the address and the operation bit) the slave answers with an ACK bit:


In your program this translates into:

i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, true);

The i2c_master_start() method adds to the cmd handler the action to send the START signal, while the i2c_master_write_byte() method sends one byte on the bus. This byte is composed of 7bits for the slave address (slave_addr) left-shifted to 1 bit (<< 1) and of the bit 0 (= IC2_MASTER_WRITE). If you want to perform a READ, you can instead use the I2C_MASTER_READ constant.

The last parameter, set to true, tells the master to wait for the slave to send the ACK bit.

If the requested operation is write, now the master can send n bytes to the slave. At the end, the master sends the STOP signal:

i2c_master_write(cmd, data_array, data_size, true);

I used the i2c_master_write method that allows to send an array (uint8_t*) of data. The data_size parameter is the size of the array. Alternatively, I could have used more times the i2c_master_write_byte used previously.

To “execute” the command link, you have to call the i2c_master_cmd_begin() method:

i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS);

its parameters are the number of the I2C controller, the command link handler and the maximum number of ticks it can wait (this method is indeed blocking; in the example below it waits for a maximum of 1 second).

Finally you can free the resources used by the command link with i2c_cmd_link_delete(cmd).

The read operation is slightly more complex. First you have to send to the slave device the command for the data you want to read. In the following tutorial I’ll show you a real application, for now let’s assume that the slave device is a temperature sensor with address 0x40 and that the command measure the actual temperature corresponds to byte 0xF3.

You’ve already learned how to send the command:

i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_write_byte(cmd, (0x40 << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, (0xF3, true);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS);

After having successfully sent the command (and eventually after having waited for the sensor to execute it) you can read the output of the sensor creating a new command link with the sensor address (but this time with the READ mode) and adding one or mor read actions (based on the number of bytes the sensor sends):

uint8_t first_byte, second_byte;
cmd = i2c_cmd_link_create();
i2c_master_write_byte(cmd, (0x40 << 1) | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, &first_byte, ACK_VAL);
i2c_master_read_byte(cmd, &second_byte, NACK_VAL);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS);

The main difference is that, after having read the last byte, the master sends the NACK signal. With this signal, it tells that it cannot receive more bytes and therefore the slave must stop transmitting.

ACK and NACK constants are defined as follows:

#define ACK_VAL    0x0
#define NACK_VAL   0x1


At the end of this post, I want to show you a classic example that uses the master mode: an I2C scanner. Goal of the program is to analyze the bus looking for any slave devices and print their address on screen.

It’s a very simple program, up to you to understand its source code in my Github repository. In the following post you’ll learn how to interface with a real sensor to get its data.



  1. Antonio 10/10/2017 at 10:13 - Reply

    Ciao Luca,

    ottimo articolo, come del resto tutti gli altri. Sono curioso di leggere i prossimi, relativi all’I2C, ma anticipo un problema che mi sta tormentando e che è in parte legato al tuo esempio di scanner. Ho letto il codice che fa ovviamente il suo mestiere, lo “scanner”, cicla su tutti gli indirizzi alla ricerca di una risposta. In pratica un polling su tutti gli indirizzi in attesa di un sensore attivo. Funziona, ma oneroso in termini computazionali, e soprattutto non garantisce la pronta identificazione, qualora risulti necessaria. Quello che voglio dire è che, a parte il carico sul master legato al polling, il sensore potrebbe essere rilevato con un certo ritardo. Trattandosi della rilevazione di un sensore nella stragrande maggioranza dei casi questo non è un problema, ma se invece della rilevazione di un sensore passiamo alla lettura del suo valore, allora le cose cambiano, in quanto potrei voler reagire immediatamente ad esempio ad un cambio del valore di un potenziometro o alla pressione di un tasto. E qui vengo al mio problema che mi cruccia. Come fare in modo che il master riceva immediatamente la notifica di un valore che cambia (o nel caso della scanner che un nuovo sensore è attivo) ? Vedo due soluzioni:
    – L’uso di interrupt, con linee da affiancare ai due pin I2C. Ma in questo caso come fare ? Un interrupt per sensore ? Un pin condiviso ?
    – L’uso di un protocollo multi-master (I2C dovrebbe esserlo, giusto ?) nel quale un nuovo sensore si “annuncia” nell’esempio dello scanner e comunica immediatamente di aver a disposizione un nuovo valore.
    Che ne pensi ? Hai affrontato questi temi ?

    • luca 10/10/2017 at 12:46 - Reply

      Ciao Antonio! Normalmente è sempre il master che va in polling sui dispositivi slave. Non esiste qualcosa di nativo per i2c (a differenza ad es di SMBus), se quindi il tuo sensore è i2c slave non vedo altre possibilità che l’uso “standard” che il bus ti mette a disposizione. Se stai invece sviluppando tu un dispositivo slave, potresti si pensare ad una forma di notifica “out of bus”, tipo un altro PIN che lo slave può utilizzare per dire al master che c’è un dato pronto x lui.

Leave A Response »

This website uses cookies to ensure you get the best experience on our website maggiori informazioni

Questo sito utilizza i cookie per fonire la migliore esperienza di navigazione possibile. Continuando a utilizzare questo sito senza modificare le impostazioni dei cookie o clicchi su "Accetta" permetti al loro utilizzo.