What Is I2C? Talking to Multiple Sensors on One Bus
Beginner1/8/2026- Author: IoTSpark Maker

What Is I2C? Talking to Multiple Sensors on One Bus

Learn the I2C protocol (SDA/SCL, 7-bit addressing, ACK/NACK) through a hands-on example: pairing a BMP280 (0x76) and an SSD1306 OLED (0x3C) on the same 2 wires with an ESP32 DevKit.

I2CprotocolESP32BMP280OLEDkien-thuc-nen-tang
0 steps3 components

I2C (Inter-Integrated Circuit) is a 2-wire serial communication protocol (SDA - data, SCL - clock) that lets multiple devices (master/slave) share a single bus. Each slave device is identified by a unique 7-bit address, letting a single ESP32 board read many sensors/displays at once using just 2 signal wires (plus power/ground).

This lesson uses a BMP280 (address 0x76) and an SSD1306 OLED (address 0x3C) on the same GPIO21 (SDA) / GPIO22 (SCL) bus on an ESP32 DevKit to demonstrate the address-based bus-sharing mechanism.

Detailed guide

I2C principles, how multiple devices share 2 SDA/SCL wires via 7-bit addressing, with a hands-on example pairing a BMP280 + SSD1306 OLED on an ESP32.

1. Introduction

I2C (Inter-Integrated Circuit, developed by Philips/NXP) is a synchronous serial protocol using 2 signal wires: SDA (Serial Data) for data, and SCL (Serial Clock) for the clock signal generated by the master (here, the ESP32). Both wires are open-drain, so a pull-up resistor (typically 4.7kΩ) to the supply rail is mandatory — most cheap sensor/display modules already include this resistor onboard.

I2C's strength is that multiple devices (multi-drop) can share exactly the same 2 SDA/SCL wires. The master distinguishes each device by a 7-bit address (0x08–0x77) sent right after the START bit.

Whichever device matches that address responds with an ACK bit (pulling SDA low); if no device answers, the master reads a NACK and knows the bus is "silent" at that address. This lesson pairs a BMP280 (default address 0x76, changeable to 0x77 via the SDO pin) and an SSD1306 OLED display (fixed address 0x3C) on the same GPIO21 (SDA) / GPIO22 (SCL) wires on an ESP32 DevKit to prove: with just 2 wires, the ESP32 can still read both devices separately thanks to their different addresses.

2. Components Needed

ComponentQtyNote
ESP32 DevKit V41Main board, acts as the I2C master
BMP280 (pressure/temperature, I2C)1Address 0x76 (default) or 0x77
0.96" 128x64 I2C OLED (SSD1306)1Address 0x3C
Jumper wires4-6SDA, SCL, VCC, GND for each module

3. Wiring Diagram

BMP280ESP32
VCC3V3
GNDGND
SCLGPIO22
SDAGPIO21
OLED SSD1306ESP32
VCC3V3
GNDGND
SCLGPIO22 (shared wire with the BMP280)
SDAGPIO21 (shared wire with the BMP280)

Note: both modules' SDA and SCL connect in parallel to the same GPIO21/GPIO22 wires — this is the key difference from SPI, where every device needs its own CS pin. See the project's Workspace Diagram / Connections tab for the full diagram with exact pin positions.

4. Example #1 — Scanning the Bus and Reading Both Devices

A sketch that uses Wire.begin(21, 22), scans the full address range 1–126 to print which devices ACK, then initializes both Adafruit_BMP280 (0x76) and Adafruit_SSD1306 (0x3C) and reads/displays data every 2 seconds.

/*
  What Is I2C? — Example #1: Scanning the Bus and Reading Two Devices
  Board: ESP32 DevKit V4
  Devices on the same I2C bus (SDA=GPIO21, SCL=GPIO22):
    - BMP280 (pressure/temperature)  -> address 0x76 (SDO tied to GND) or 0x77 (SDO tied to VCC)
    - OLED SSD1306 128x64            -> address 0x3C

  Demonstrated idea: multiple devices can share the same 2 SDA/SCL wires
  because each is distinguished by a unique 7-bit I2C address.
*/

#include <Wire.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SDA_PIN 21
#define SCL_PIN 22

#define OLED_ADDR 0x3C
#define BMP280_ADDR 0x76

Adafruit_BMP280 bmp;
Adafruit_SSD1306 display(128, 64, &Wire, -1);

bool bmpReady = false;
bool oledReady = false;

void scanBus() {
  Serial.println(F("Quet dia chi I2C tren bus..."));
  uint8_t found = 0;
  for (uint8_t addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    uint8_t err = Wire.endTransmission();
    if (err == 0) {
      Serial.printf("  -> Tim thay thiet bi tai 0x%02X\n", addr);
      found++;
    }
  }
  Serial.printf("Tong: %u thiet bi\n", found);
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println(F("=== I2C multi-device demo boot ==="));

  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(100000); // 100kHz Standard-mode, an toan cho day breadboard dai

  scanBus();

  oledReady = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  if (!oledReady) {
    Serial.println(F("[WARN] Khong tim thay OLED tai 0x3C"));
  } else {
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    display.println(F("I2C multi-device"));
    display.display();
  }

  bmpReady = bmp.begin(BMP280_ADDR);
  if (!bmpReady) {
    Serial.println(F("[WARN] Khong tim thay BMP280 tai 0x76 (thu 0x77?)"));
  }
}

void loop() {
  static unsigned long lastRead = 0;
  if (millis() - lastRead >= 2000) {
    lastRead = millis();

    float tempC = bmpReady ? bmp.readTemperature() : NAN;
    float pressHpa = bmpReady ? bmp.readPressure() / 100.0F : NAN;

    if (bmpReady) {
      Serial.printf("BMP280 (0x76): temp=%.2fC pressure=%.2fhPa\n", tempC, pressHpa);
    } else {
      Serial.println(F("BMP280 (0x76): khong co du lieu"));
    }

    if (oledReady) {
      display.clearDisplay();
      display.setCursor(0, 0);
      display.println(F("I2C: BMP280 + OLED"));
      display.setCursor(0, 16);
      if (bmpReady) {
        display.printf("T: %.1f C\n", tempC);
        display.printf("P: %.1f hPa\n", pressHpa);
      } else {
        display.println(F("BMP280: N/A"));
      }
      display.setCursor(0, 48);
      display.println(F("addr 0x76 / 0x3C"));
      display.display();
    }
  }
}

5. Example #2 — The Real Application: a Pressure/Temperature Display Station

A second variant that skips manual scanning, initializing both devices directly at their known addresses and showing temperature, pressure, and estimated altitude (calculated from a sea-level reference pressure of 1013.25 hPa) directly on the OLED.

/*
  What Is I2C? — Example #2: The Real Application — a Pressure/Temperature Display Station
  Board: ESP32 DevKit V4 + BMP280 (0x76) + OLED SSD1306 (0x3C) on the same I2C bus.
  Doesn't need Serial to operate — all data is shown directly on the OLED,
  demonstrating the real-world use of sharing one bus across multiple sensors.
*/

#include <Wire.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define OLED_ADDR 0x3C
#define BMP280_ADDR 0x76

Adafruit_BMP280 bmp;
Adafruit_SSD1306 display(128, 64, &Wire, -1);

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println(F("=== Tram hien thi ap suat/nhiet do (I2C shared bus) ==="));

  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(100000);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println(F("[ERROR] OLED khong phan hoi tai 0x3C"));
  }
  if (!bmp.begin(BMP280_ADDR)) {
    Serial.println(F("[ERROR] BMP280 khong phan hoi tai 0x76"));
  }

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(F("Khoi dong tram do..."));
  display.display();
}

void loop() {
  float tempC = bmp.readTemperature();
  float pressHpa = bmp.readPressure() / 100.0F;
  float altitude = bmp.readAltitude(1013.25F); // ap suat quy chieu muc nuoc bien

  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("TRAM DO AP SUAT"));
  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);

  display.setTextSize(2);
  display.setCursor(0, 16);
  display.printf("%.1fC\n", tempC);

  display.setTextSize(1);
  display.setCursor(0, 40);
  display.printf("Ap suat: %.1f hPa\n", pressHpa);
  display.setCursor(0, 52);
  display.printf("Do cao ~%.0f m\n", altitude);

  display.display();

  delay(1000);
}

6. Real Applications & Extensions

Because I2C uses addresses to distinguish devices, a single ESP32 I2C bus (the default Wire bus, or Wire1 for a second bus) can host dozens of different sensors/displays as long as no two share an address. Some ICs let you change their address via a hardware configuration pin (e.g. the BMP280's 0x76/0x77 via its SDO pin) — useful when you need two identical modules on the same bus.

Real applications: mini weather stations, multi-channel sensor dashboards, and I2C I/O-expander modules (PCF8574, MCP23017) all rely on exactly this addressing mechanism.

7. Common Issues

IssueCauseFix
Scanning the bus finds no devicesMissing pull-up resistors, or SDA/SCL are swappedCheck whether the module already has pull-ups; swap SDA↔SCL
The BMP280 doesn't respond at 0x76The module is set to address 0x77 (SDO tied to VCC)Try again with bmp.begin(0x77) or check the SDO pin
The OLED and BMP280 "fight", data comes out wrongBoth devices share the same I2C address (rare for this pair, but common when adding a third identical module)Change one module's address via its configuration pin, or use a second I2C bus (Wire1)
Data gets noisy over a long wire runI2C's 400kHz speed is too high for a long breadboard wireDrop to 100kHz with Wire.setClock(100000)

8. Summary

I2C solves the "many devices, few wires" problem by addressing each device individually on the same 2 SDA/SCL wires.

The trade-off is lower speed than SPI (typically 100kHz-400kHz versus SPI's several MHz) and the need to manage addresses to avoid collisions. It's the default choice for small sensors/displays with light data needs where saving ESP32 GPIO pins matters.