In this article we will learn to use the ESP32 I2C communication protocol (in Arduino IDE). We’ll go through how ESP32 I2C works, how to alter the default I2C pins (SDA, SCL), how to connect numerous I2C devices on the bus with proper addressing, and how to make an ESP32 I2C scanner to check for available I2C devices on the bus and obtain their addresses.

I2C COMMUNICATION PROTOCOL

I2C (i-square-c) stands for “Inter-Integrated-Circuit,” and it was invented in 1982 by Philips Semiconductors (now NXP). The I2C bus is a synchronous, bidirectional, half-duplex serial communication bus with many masters and slaves.

It’s commonly used in short-distance intra-board communication to connect lower-speed peripheral ICs to processors and microcontrollers. External EEPROMs, Digital Temperature Sensors, LCD Controllers, OLED Displays, RTCs, and other components are also available.

ESP32 I2C BUS INTERFACE

The I2C-bus was limited to 100 kbit/s operations. Over time there have been several additions to the specification so that there are now five operating speed categories. Standard-mode, Fast-mode (Fm), Fast-mode Plus (Fm+), and High-speed mode (Hs-mode)

Bidirectional bus:

  • Standard-Mode (Sm), with a bit rate up to 100 kbit/s
  • Fast-Mode (Fm), with a bit rate up to 400 kbit/s
  • Fast-Mode Plus (Fm+), with a bit rate up to 1 Mbit/s
  • High-speed Mode (Hs-mode), with a bit rate up to 3.4 Mbit/s.

Unidirectional bus:

  • Ultra Fast-Mode (UFm), with a bit rate of up to 5 Mbit/s

Connecting I2C Devices with ESP32

The I2C communication protocol sends and receives data via two wires. One is used to send and receive data(SDA), while the other is used to send the clock signal (SCL).

Because the SDA and SCL lines are active low, resistors should be used to draw them up. Typical values for 5V devices are 4.7k Ohm and 2.4k Ohm for 3.3V devices.

The majority of the sensors we use in our projects are breakout boards with resistors already built in. So, while working with this type of electronic component, you usually don’t have to worry about it.

Connecting an I2C device to an ESP32 is usually as easy as connecting GND to GND, SDA to SDA, SCL to SCL, and a positive power source to a peripheral, commonly 3.3V (depending on the module).

When using the ESP32 with Arduino IDE, the default I2C pins are GPIO 22 (SCL) and GPIO 21 (SDA) but you can configure your code to use any other pins.

Scan I2C Address with ESP32

Each slave on the bus has its own address, a hexadecimal number that allows the ESP32 to communicate with each device using I2C communication.

The I2C address is usually listed on the datasheet of the component. However, if determining the I2C address is tricky, you may need to use an I2C scanner sketch.

Code:

#include <Wire.h>
 
void setup() {
  Wire.begin();
  Serial.begin(115200);
  Serial.println("\nI2C Scanner");
}
 
void loop() {
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
      nDevices++;
    }
    else if (error==4) {
      Serial.print("Unknow error at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found\n");
  }
  else {
    Serial.println("done\n");
  }
  delay(5000);          
}

Use Different I2C Pins with ESP32 (change default I2C pins)

You can set practically any pin on the ESP32 to have I2C capabilities; all you have to do is set it in your code. When using the Arduino IDE with the ESP32, utilise the Wire h library to interface with I2C devices.

Wire.begin(SDA0_Pin, SCL0_Pin);

So all you have to do is set the I2C SDA and I2C SCL variables to your selected SDA and SCL GPIOs.

However, if you’re using libraries to interface with those sensors, this may not work, and selecting alternative pins may be difficult. This arises because if you don’t pass your own Wire instance while initialising the library, those libraries may overwrite your pins.

In certain circumstances, we’ll need to look at the.CPP library files closely to figure out how to pass your own TwoWire arguments.

SCHEMATIC

CODE

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define I2C_SDA 33
#define I2C_SCL 32

#define SEALEVELPRESSURE_HPA (1013.25)

TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

unsigned long delayTime;

void setup() {
  Serial.begin(115200);
  Serial.println(F("BME280 test"));
  I2CBME.begin(I2C_SDA, I2C_SCL, 100000);

  bool status;

  // default settings
  // (you can also pass in a Wire library object like &Wire2)
  status = bme.begin(0x76, &I2CBME);  
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  Serial.println("-- Default Test --");
  delayTime = 1000;

  Serial.println();
}

void loop() { 
  printValues();
  delay(delayTime);
}

void printValues() {
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println(" *C");
  
  // Convert temperature to Fahrenheit
  /*Serial.print("Temperature = ");
  Serial.print(1.8 * bme.readTemperature() + 32);
  Serial.println(" *F");*/
  
  Serial.print("Pressure = ");
  Serial.print(bme.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.print("Approx. Altitude = ");
  Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
  Serial.println(" m");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println(" %");

  Serial.println();
}

HOW CODE WORKS

First, define your new I2C pins on the I2C_SDA and I2C_SCL variables. In this case, we’re using GPIO 33 and GPIO 32.

#define I2C_SDA 33

#define I2C_SCL 32

Create a new TwoWire instance. In this case, it’s called I2CBME. This simply creates an I2C bus.

TwoWire I2CBME = TwoWire(0);

In the setup(), initialize the I2C communication with the pins you’ve defined earlier. The third parameter is the clock frequency.

I2CBME.begin(I2C_SDA, I2C_SCL, 400000);

Finally, initialize a BME280 object with your sensor address and your TwoWire object.

status = bme.begin(0x76, &I2CBME);

After this, we can  use the usual methods on your bme object to request temperature, humidity and pressure.

Article 2: 

ESP32 with Multiple I2C Devices

 Each I2C device has its own address, so it is possible to have multiple I2C devices on the same bus.

Multiple I2C devices (same bus, different addresses)

When there are numerous devices with distinct addresses, setting them up is simple

  • connect both peripherals to the ESP32 SCL and SDA lines.
  • in the code, refer to each peripheral by its address.

We can utilise the same SDA and SCL lines without issue because the OLED and the BME280 have distinct addresses. The BME280 address is 0x76, while the OLED display address is 0x3C.

Code

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define SDA_1 27
#define SCL_1 26

#define SDA_2 33
#define SCL_2 32

TwoWire I2Cone = TwoWire(0);
TwoWire I2Ctwo = TwoWire(1);

Adafruit_BME280 bme1;
Adafruit_BME280 bme2;

void setup() {
  Serial.begin(115200);
  Serial.println(F("BME280 test"));

  I2Cone.begin(SDA_1, SCL_1, 100000); 
  I2Ctwo.begin(SDA_2, SCL_2, 100000);

  bool status1 = bme1.begin(0x76, &I2Cone);  
  if (!status1) {
    Serial.println("Could not find a valid BME280_1 sensor, check wiring!");
    while (1);
  }
  
  bool status2 = bme2.begin(0x76, &I2Ctwo);  
  if (!status2) {
    Serial.println("Could not find a valid BME280_2 sensor, check wiring!");
    while (1);
  }
  
  Serial.println();
}

void loop() { 
  // Read from bme1
  Serial.print("Temperature from BME1= ");
  Serial.print(bme1.readTemperature());
  Serial.println(" *C");

  Serial.print("Humidity from BME1 = ");
  Serial.print(bme1.readHumidity());
  Serial.println(" %");

  Serial.print("Pressure from BME1 = ");
  Serial.print(bme1.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.println("--------------------");

  // Read from bme2
  Serial.print("Temperature from BME2 = ");
  Serial.print(bme2.readTemperature());
  Serial.println(" *C");

  Serial.print("Humidity from BME2 = ");
  Serial.print(bme2.readHumidity());
  Serial.println(" %");

  Serial.print("Pressure from BME2 = ");
  Serial.print(bme2.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.println("--------------------");
  
  delay(5000);
}

SCHEMATIC

if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 

  Serial.println(F(“SSD1306 allocation failed”));

  for(;;);

}

  bool status = bme.begin(0x76);  

if (!status) {

  Serial.println(“Could not find a valid BME280 sensor, check wiring!”);

  while (1);

}

ESP32 MULTIPLE 12C DEVICES

If Multiple peripherals has same address then 

  • change the device I2C address;
  • use an I2C multiplexer.

CHANGING THE 12C ADDRESS

Many breakout boards have the option to change the I2C address depending on its circuitry.By placing the resistor on one side or the other, we can select different I2C addresses. This also happens with other components.

USING A 12C MULTIPLEXER

Changing the I2C address is not always simple. So, if you want to connect with up to 8 devices with the same address on the same I2C bus, you can use an I2C multiplexer like the TCA9548A.

ESP32 Using Two I2C Bus Interfaces

To use the two I2C bus interfaces of the ESP32, you need to create two TwoWire instances.

TwoWire I2Cone = TwoWire(0);

TwoWire I2Ctwo = TwoWire(1)

Then, initialize I2C communication on your desired pins with a defined frequency.

void setup() {

  I2Cone.begin(SDA_1, SCL_1, freq1);

  I2Ctwo.begin(SDA_2, SCL_2, freq2); 

}

Then, you can use the methods from the Wire.h library to interact with the I2C bus interfaces.

Using the provided Wire() and Wire1() objects is a simpler option. Wire().begin() starts an I2C conversation using the default pins and frequency on the first I2C bus. You should supply your preferred SDA and SCL pins, as well as the frequency, to Wire1.begin().

setup(){

  Wire.begin(); //uses default SDA and SCL and 100000HZ freq

  Wire1.begin(SDA_2, SCL_2, freq);

}

This method allows you to use two I2C buses, one of them uses the default parameters.

Each sensor is connected to a different I2C bus.

  • I2C Bus 1: uses GPIO 27 (SDA) and GPIO 26 (SCL);
  • I2C Bus 2: uses GPIO 33 (SDA) and GPIO 32 (SCL);

SCHEMATIC

CODE

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);

  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  
  bool status = bme.begin(0x76);  
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
  
  delay(2000);
  display.clearDisplay();
  display.setTextColor(WHITE);
}

void loop() {
  display.clearDisplay();
  // display temperature
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0,10);
  display.print(String(bme.readTemperature()));
  display.print(" ");
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  display.print("C");
  
  // display humidity
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(String(bme.readHumidity()));
  display.print(" %"); 
  
  display.display();

  delay(1000);
}

HOW CODE WORKS

Define the SDA and SCL pins you want to use:

#define SDA_1 27

#define SCL_1 26

#define SDA_2 33

#define SCL_2 32

Create two TwoWire objects (two I2C bus interfaces):

TwoWire I2Cone = TwoWire(0);

TwoWire I2Ctwo = TwoWire(1);

Create two instances of the Adafruit_BME280 library to interact with your sensors: bme1 and bme2.

Adafruit_BME280 bme1;

Adafruit_BME280 bme2;

Initialize an I2C communication on the defined pins and frequency:

I2Cone.begin(SDA_1, SCL_1, 100000); 

I2Ctwo.begin(SDA_2, SCL_2, 100000);

Then, you should initialize the bme1 and bme2 objects with the right address and I2C bus. bme1 uses I2Cone:

bool status = bme1.begin(0x76, &I2Cone); 

And bme2 uses I2Ctwo:

bool status1 = bme2.begin(0x76, &I2Ctwo);

Now, we can use the methods from the Adafruit_BME280 library on your bme1 and bme2 objects to read temperature, humidity and pressure.

You can also check on:

author avatar
Aravind S S