794  I2C: Inter-Integrated Circuit Protocol

794.1 Learning Objectives

By the end of this section, you will be able to:

  • Understand I2C Architecture: Explain the two-wire bus structure with SDA and SCL
  • Configure Pull-Up Resistors: Select appropriate resistor values for reliable communication
  • Address I2C Devices: Use 7-bit addressing to communicate with specific devices
  • Read and Write Data: Implement I2C read and write transactions
  • Debug I2C Issues: Troubleshoot address conflicts, missing pull-ups, and clock stretching
  • Write I2C Code: Program Arduino/ESP32 to communicate with sensors and displays

794.2 Prerequisites

Before diving into this chapter, you should be familiar with:

  • Wired Communication Fundamentals: Understanding of synchronous communication, multi-drop topology, master-slave architecture
  • Digital Electronics: Understanding of open-drain outputs and pull-up resistors

794.3 I2C Overview

I2C (I2C, I-squared-C) is a popular multi-device communication bus developed by Philips (now NXP) in the 1980s.

Pronunciation: “I-squared-C” or “I-two-C”

Full name: Inter-Integrated Circuit

794.3.1 Characteristics

Feature Value
Wires 2 only (SDA + SCL)
Topology Multi-drop (bus)
Sync/Async Synchronous (SCL = clock)
Duplex Half-duplex (SDA is bidirectional)
Speed 100 kHz (standard), 400 kHz (fast), 1 MHz (fast+), 3.4 MHz (high-speed)
Distance <1 meter (same PCB or short cable)
Devices Up to 112 (7-bit addressing) or 1024 (10-bit)
Master/Slave Multi-master capable
I2C physical wiring diagram showing SDA and SCL lines with pull-up resistors to VCC, connecting master and multiple slave devices in parallel bus configuration
Figure 794.1: I2C physical connections showing SDA and SCL with pull-up resistors
I2C signal timing diagram showing start condition with SDA falling while SCL high, 8-bit data transfer with clock pulses, ACK/NACK bit, and stop condition with SDA rising while SCL high
Figure 794.2: I2C signal timing showing start, data, acknowledge, and stop conditions

794.5 I2C Signals

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22'}}}%%
graph LR
    M["Master"]
    S1["Slave 1<br/>0x3C"]
    S2["Slave 2<br/>0x76"]
    VCC["VCC +3.3V"]
    R1["4.7k"]
    R2["4.7k"]

    VCC --> R1
    VCC --> R2
    R1 -.->|"Pull-up"| SDA
    R2 -.->|"Pull-up"| SCL

    M ---|"SDA (Data)"| SDA[" "]
    M ---|"SCL (Clock)"| SCL[" "]
    SDA --- S1
    SCL --- S1
    SDA --- S2
    SCL --- S2

    style M fill:#2C3E50,stroke:#16A085,color:#fff
    style S1 fill:#16A085,stroke:#16A085,color:#fff
    style S2 fill:#16A085,stroke:#16A085,color:#fff
    style VCC fill:#E67E22,stroke:#16A085,color:#fff
    style R1 fill:#7F8C8D,stroke:#16A085,color:#fff
    style R2 fill:#7F8C8D,stroke:#16A085,color:#fff

Figure 794.4: I2C Bus Wiring with Pull-Up Resistors

SDA (Serial Data):

  • Bidirectional data line
  • Carries address and data
  • Open-drain (requires pull-up resistor)

SCL (Serial Clock):

  • Clock signal from master
  • Slaves can hold it LOW (“clock stretching”) to slow master
  • Open-drain (requires pull-up resistor)

794.6 Pull-Up Resistors

Critical requirement: Both SDA and SCL need pull-up resistors to VCC.

Typical values:

  • 4.7 kohm - Most common
  • 10 kohm - For slower speeds, longer cables
  • 2.2 kohm - For faster speeds, more devices

Why needed? I2C uses open-drain outputs - can only pull LOW, not HIGH. Pull-up resistors provide the HIGH level.

Geometric diagram of I2C bus showing two-wire connection (SDA for data, SCL for clock) between microcontroller master and multiple slave devices. Demonstrates pull-up resistors on both lines, open-drain configuration, and 7-bit addressing scheme allowing up to 127 devices on a single bus

I2C Bus Connections
Figure 794.5: I2C uses just two wires (SDA and SCL) with open-drain configuration and pull-up resistors. Multiple slave devices share the same bus, each responding only to its unique 7-bit address.

794.7 I2C Addressing

Each device on the bus needs a unique 7-bit address.

Address space:

  • 7-bit addresses: 0x08 to 0x77 (112 addresses)
  • Some addresses reserved (0x00-0x07, 0x78-0x7F)

Common I2C device addresses:

Device Address (Hex) Address (Decimal)
OLED Display (SSD1306) 0x3C or 0x3D 60 or 61
BME280 (Temp/Humidity) 0x76 or 0x77 118 or 119
MPU6050 (IMU) 0x68 or 0x69 104 or 105
RTC DS3231 0x68 104
EEPROM 24C256 0x50 - 0x57 80 - 87

Finding device addresses:

// I2C Scanner
#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Serial.println("I2C Scanner");

  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
      Serial.print("Device found at 0x");
      if (addr < 16) Serial.print("0");
      Serial.println(addr, HEX);
    }
  }
  Serial.println("Scan complete");
}

void loop() {}

794.8 I2C Communication Protocol

Artistic representation of I2C signal timing showing START condition (SDA falling while SCL high), data transmission with clock synchronization, ACK/NACK bits after each byte, and STOP condition (SDA rising while SCL high). Demonstrates master-controlled clock stretching by slow slave devices

I2C Signal Timing
Figure 794.6: I2C protocol timing: START and STOP conditions frame each transaction, with data sampled on SCL rising edge. The acknowledgment bit after each byte confirms successful reception.

Start and Stop Conditions:

     SDA  ____        ________________        ____
              \_______/                \______/

     SCL  _________                ____________
                   \_______/\_____/

          <-START-> <-DATA-> <-STOP->

START: SDA goes HIGH->LOW while SCL is HIGH

STOP: SDA goes LOW->HIGH while SCL is HIGH

Data Transfer (1 byte + ACK):

Bit:  7   6   5   4   3   2   1   0  ACK
     ___     ___         ___     ___ ___
SDA:    \___/   \_______/   \___/      (Data bits + ACK)

     _   _   _   _   _   _   _   _   _
SCL: _|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_ (Clock pulses)

     <--------- 8 Data Bits -------> <A>
                                      |
                                     ACK (0=success, 1=fail)

Data transfer rules:

  • SDA can only change when SCL is LOW
  • SDA is sampled when SCL is HIGH
  • Data transmitted MSB first (most significant bit first)
Complete I2C transaction timing showing start condition, 7-bit slave address, R/W bit, acknowledge, data bytes with ACK after each byte, and stop condition for write and read operations
Figure 794.7: I2C communication protocol timing showing complete transaction

794.9 I2C Write Transaction

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22'}}}%%
sequenceDiagram
    participant M as Master
    participant S as Slave (0x68)

    M->>S: START
    M->>S: Address (0x68) + W (0)
    S->>M: ACK
    M->>S: Register Address (0x6B)
    S->>M: ACK
    M->>S: Data Byte (0x00)
    S->>M: ACK
    M->>S: STOP

    Note over M,S: Write Complete

Figure 794.8: I2C Write Transaction Sequence

Steps:

  1. Master sends START
  2. Master sends slave address + write bit (0)
  3. Slave acknowledges (ACK)
  4. Master sends register address
  5. Slave ACKs
  6. Master sends data byte
  7. Slave ACKs
  8. Master sends STOP

794.10 I2C Read Transaction

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22'}}}%%
sequenceDiagram
    participant M as Master
    participant S as Slave (0x68)

    M->>S: START
    M->>S: Address (0x68) + W (0)
    S->>M: ACK
    M->>S: Register Address (0x3B)
    S->>M: ACK
    M->>S: RESTART
    M->>S: Address (0x68) + R (1)
    S->>M: ACK
    S->>M: Data Byte 1
    M->>S: ACK
    S->>M: Data Byte 2
    M->>S: NACK (finished)
    M->>S: STOP

    Note over M,S: Read Complete

Figure 794.9: I2C Read Transaction with Restart Condition

794.11 Arduino/ESP32 I2C Example

#include <Wire.h>

// I2C device address
#define MPU6050_ADDR 0x68

void setup() {
  Serial.begin(115200);
  Wire.begin();  // Initialize I2C

  // Wake up MPU6050 (write 0 to PWR_MGMT_1 register)
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x6B);  // Register address
  Wire.write(0);     // Wake up
  Wire.endTransmission(true);

  Serial.println("MPU6050 Initialized");
}

void loop() {
  // Read accelerometer X-axis (registers 0x3B-0x3C)
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x3B);  // Starting register
  Wire.endTransmission(false);  // Repeated start

  Wire.requestFrom(MPU6050_ADDR, 2, true);  // Request 2 bytes

  if (Wire.available() >= 2) {
    int16_t accelX = Wire.read() << 8 | Wire.read();

    Serial.print("Accel X: ");
    Serial.println(accelX);
  }

  delay(500);
}

Wiring (ESP32):

ESP32 GPIO 21 (SDA) -> Sensor SDA
ESP32 GPIO 22 (SCL) -> Sensor SCL
ESP32 3.3V          -> Sensor VCC
ESP32 GND           -> Sensor GND

(Add 4.7k pull-up resistors on SDA and SCL if not on sensor board)

794.12 Clock Stretching

Problem: Slave needs more time to process data.

Solution: Slave holds SCL LOW to pause communication.

Master releases SCL (expects HIGH)
Slave holds SCL LOW (clock stretching)
Master waits...
Slave releases SCL (ready to continue)
Communication resumes

Use case: Slow sensors (temperature readings take time), EEPROM write operations.


TipPractical Tips

Avoiding Address Conflicts:

  • Check device datasheets before buying
  • Many sensors have address select pins (A0, AD0) to change address
  • Use I2C scanner sketch to verify addresses
  • Plan device selection to avoid conflicts

Common Conflicts:

  • 0x68: MPU6050, DS1307/DS3231 RTC -> Use MPU6050 AD0 pin for 0x69
  • 0x76/0x77: BMP280, BME280, BME680 -> Use SDO pin
  • 0x27/0x3F: LCD backpack modules -> Check board

Pull-up Resistors:

  • 4.7k: Most common (400 kHz)
  • 2.2k: Long cables or many devices
  • 10k: Short cables, few devices

794.13 Hands-On Lab: I2C Scanner

Objective: Find all I2C devices connected to your ESP32/Arduino.

#include <Wire.h>

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

  Serial.println("\nI2C Scanner");
  Serial.println("Scanning for devices...\n");

  byte count = 0;

  for (byte i = 8; i < 120; i++) {
    Wire.beginTransmission(i);
    if (Wire.endTransmission() == 0) {
      Serial.print("Found device at address: 0x");
      if (i < 16) Serial.print("0");
      Serial.print(i, HEX);
      Serial.print(" (");
      Serial.print(i);
      Serial.println(")");
      count++;
    }
  }

  Serial.print("\nTotal devices found: ");
  Serial.println(count);
}

void loop() {}

Expected output:

I2C Scanner
Scanning for devices...

Found device at address: 0x3C (60)
Found device at address: 0x76 (118)

Total devices found: 2

794.14 Hands-On Lab: BME280 Temperature Sensor

Objective: Read temperature from BME280 sensor via I2C.

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

Adafruit_BME280 bme;  // I2C

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

  if (!bme.begin(0x76)) {  // I2C address
    Serial.println("BME280 sensor not found!");
    while (1);  // Halt
  }

  Serial.println("BME280 Ready");
}

void loop() {
  float temperature = bme.readTemperature();
  float humidity = bme.readHumidity();
  float pressure = bme.readPressure() / 100.0F;

  Serial.print("Temp: ");
  Serial.print(temperature);
  Serial.print(" C, Humidity: ");
  Serial.print(humidity);
  Serial.print(" %, Pressure: ");
  Serial.print(pressure);
  Serial.println(" hPa");

  delay(2000);
}

794.15 Knowledge Check: I2C Multi-Master Arbitration

Scenario: You’re designing a robotics platform with two microcontrollers (ESP32 and Arduino) that both need to control a shared OLED display (I2C address 0x3C) and BME280 sensor (0x76). Both MCUs operate as I2C masters on the same bus with 4.7k pull-ups.

Think about:

  1. What happens if both masters try to send a START condition simultaneously?
  2. Why doesn’t this cause electrical damage to the devices?

Key Insight: I2C uses wired-AND arbitration through open-drain outputs. When multiple masters transmit simultaneously, any device pulling SDA LOW wins (LOW overrides HIGH). During arbitration, masters compare transmitted bits to actual bus state:

  • Master transmits 1 (releases line HIGH)
  • Bus reads 0 (another master pulled LOW)
  • First master detects mismatch -> backs off gracefully
  • No electrical conflict because open-drain outputs never drive HIGH (only pull LOW or float)

This elegant mechanism prevents bus damage. Push-pull outputs (like SPI) would short VCC to GND if two devices drive opposite levels, potentially destroying the chips. The trade-off: RC charging through pull-up resistors limits rise time, restricting I2C to ~400 kHz vs SPI’s 80+ MHz.

Verify Your Understanding:

  • Calculate arbitration delay: With 400 pF bus capacitance and 4.7k pull-ups, rise time = 2.2 x 4700 ohm x 400pF = 4.1 us. This limits maximum clock frequency to ~200 kHz for reliable multi-master operation.
  • Why faster rise times require smaller pull-ups: At 100 kHz (standard mode), 10k resistors work. At 400 kHz (fast mode), 2.2k needed for adequate rise time.

794.16 Quiz: I2C Protocol

Question 1: An Arduino connects to a BME280 sensor (I2C address 0x76) and an OLED display (I2C address 0x3C) on the same I2C bus. Both devices ACK their addresses. What happens if you send data to 0x76 without specifying it’s a write vs read operation?

Explanation: I2C addresses are 7-bit, transmitted in the most significant 7 bits of the first byte. The LSB (bit 0) is the R/W bit: 0 = Write, 1 = Read. So address 0x76 is actually sent as 0xEC (write) or 0xED (read). Only the device matching both the address AND the R/W direction responds. Multiple devices can share a bus because: (1) addresses are unique, (2) only addressed device ACKs and responds, (3) unaddressed devices ignore the transaction. This is why I2C supports 127 devices on one bus (7-bit address space).


Complete I2C protocol overview showing physical layer (two-wire bus), data layer (address frame with R/W bit, data frames with ACK), and logical layer (master-slave communication with clock stretching and arbitration for multi-master scenarios)

I2C Protocol Overview
Figure 794.10: I2C provides a simple, efficient interface for connecting low-speed peripherals like sensors, EEPROMs, and displays. Its two-wire design minimizes PCB routing complexity while supporting multiple devices.

794.17 Summary

This chapter covered I2C (Inter-Integrated Circuit) protocol:

  • Two-wire bus (SDA for data, SCL for clock) supports multiple devices
  • Open-drain outputs with pull-up resistors (typically 4.7k) enable multi-master operation
  • 7-bit addressing allows up to 112 devices on a single bus
  • Synchronous protocol with master-controlled clock and optional clock stretching
  • Half-duplex communication with START, STOP, and ACK/NACK conditions
  • Common devices: Temperature sensors (BME280), displays (SSD1306), IMUs (MPU6050), RTCs (DS3231)

794.18 What’s Next

Continue learning about wired protocols: