I2C is the two-wire bus (SDA for the data, SCL for the clock) that links several peripherals — sensors, OLED displays, real-time clocks — to the ESP32, each one identified by an address. On the ESP32 it is driven with the Wire library. Its default pins are GPIO21 (SDA) and GPIO22 (SCL), but they are remappable, and the chip has two controllers (Wire and Wire1).

What is it for?

A single pair of wires is enough to question a whole cluster of components:

  • Reading sensors. Temperature, pressure, humidity, accelerometer: most digital sensors speak I2C, each with its own library.
  • Displaying. Small OLED screens (SSD1306) and LCDs on an I2C backpack are addressed over two wires, without tying up a dozen pins.
  • Chaining several modules. As long as their addresses differ, sensors and display share the same SDA/SCL bus.

How the protocol works (addressing, ACK, controller/target) is described in the cross-cutting concept page I2C. Here you see how to wire it and code it on the ESP32.

Step by step

Four moves: wire it, initialise, scan the addresses, use a library.

1. Wire it: two wires, two pull-up resistors

SDA and SCL are open-drain lines: they need pull-up resistors (~4.7 kΩ) to 3.3 V to come back up to the high state. Most modules already carry them: no need to add any if a single module is present.

I2C wiring between an ESP32 and a module: SDA on GPIO21, SCL on GPIO22, 3.3 V supply, common ground, pull-up resistors to 3.3 V|600

Warning

Pull up to 3.3 V, not to 5 V. The SDA/SCL lines of a module powered at 5 V come back up to 5 V and can damage the pins of the ESP32. Power the module at 3.3 V, or insert a level shifter (see logic levels).

2. Initialise the bus

Wire.begin() with no argument uses the default pins (GPIO21/22). For other pins, you pass them explicitly:

#include <Wire.h>
 
const int SDA_PIN = 21;
const int SCL_PIN = 22;
 
void setup() {
  Wire.begin(SDA_PIN, SCL_PIN);   // same as Wire.begin() here (default pins)
}

3. Scan the addresses

Before using a module, you check that it answers, and at which address. The scanner questions every possible address and notes the ones that acknowledge (endTransmission() returns 0).

This scanner is diagnostic tool number one of the I2C: a silent bus (no address found) almost always betrays a wiring or pull-up problem.

4. Use a library

Once the address is known, you install the library of the component (Adafruit, Sparkfun…), which hides the raw exchanges. You almost never handle Wire.write() / Wire.read() by hand.

Example — Scanning the bus and reading the addresses

A concrete case: list in the monitor every peripheral present on the bus. You wire one or more modules, upload, and read the addresses found.

#include <Wire.h>
 
const int SDA_PIN = 21;
const int SCL_PIN = 22;
 
void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
  Serial.println("Scanning the I2C bus...");
}
 
void loop() {
  int trouves = 0;
  for (byte adr = 1; adr < 127; adr++) {   // 7-bit addresses: 1 to 126
    Wire.beginTransmission(adr);
    if (Wire.endTransmission() == 0) {     // 0 = a module acknowledged (ACK)
      Serial.print("  module at address 0x");
      if (adr < 16) Serial.print("0");     // print on 2 digits
      Serial.println(adr, HEX);
      trouves++;
    }
  }
  if (trouves == 0) {
    Serial.println("  no module - check wiring and pull-ups");
  }
  delay(3000);
}

endTransmission() returns 0 when a target answers at the address tried: that is the acknowledge (ACK) of the protocol. You sweep from 1 to 126 (the 7-bit addresses) and print the ones that answer in hexadecimal. An SSD1306 shows up at 0x3C, a BME280 at 0x76…

With an SSD1306 display and a BME280 sensor on the bus, the monitor prints:

Scanning the I2C bus...
  module at address 0x3C
  module at address 0x76
  module at address 0x3C
  module at address 0x76

The list repeats every 3 seconds: unplugging a module mid-scan makes it disappear from the next sweep.

Pitfalls

No pull-up resistors. Pitfall number one: the scanner finds nothing. Without a pull-up to 3.3 V, SDA/SCL stay stuck low and the bus is mute. A module with built-in pull-ups is enough. Otherwise, add two ~4.7 kΩ resistors.

Powering at 5 V. The lines then come back up to 5 V on 3.3 V pins: risk of destruction. Power the module at 3.3 V.

Two modules at the same address. They answer at the same time and jam each other. The fix: an address jumper on one of them, or the second bus Wire1 (see Special case).

SDA and SCL swapped. Nothing answers. Check that SDA goes to GPIO21 and SCL to GPIO22 (or the declared pins).

Ground not tied. Like every bus, the I2C needs a common ground between the ESP32 and the modules.

Wires too long at 400 kHz. The capacitance of the wires distorts the edges. On a long link, drop back to 100 kHz (Wire.setClock(100000)).

Exercises

Exercise 1 — Printing on an OLED display

Wire an SSD1306 OLED display (I2C) and print “Bonjour ESP32”. The usual address is 0x3C. Hint: use a library (Adafruit SSD1306 + GFX).

Exercise 2 — Two sensors at the same address

You have two identical sensors (so the same address), which you cannot change. How do you read both on the same ESP32?

Special case — The second bus and the speed

The ESP32 has two I2C controllers. The first is Wire. The second is instantiated through TwoWire(1) (often named Wire1), on the pins of your choice. Useful to separate two modules at the same address, or to isolate a slow sensor from a fast bus.

The speed is set with Wire.setClock(100000) (standard, 100 kHz) or 400000 (fast, 400 kHz). The fast mode suits short links. On long wires, stay at 100 kHz.

On the C3 / S3 variants, the default pins differ from GPIO21/22. Check the pinout through configuring the GPIO and pass them to Wire.begin(sda, scl).

Where it fits in the project

  • Proof of concept — an I2C sensor (BME280, MPU6050) validates a measurement with no interface electronics. The scanner confirms the wiring in a minute.
  • Integration and testing — several modules on a single bus cut the wiring of the embedded subsystem. The I2C display shows the state live during the trials.

See also