Beginner

I2C Communication Protocol

Master Inter-Integrated Circuit (I2C) communication with detailed explanations, practical examples, and troubleshooting tips. Learn everything from protocol fundamentals to real-world sensor integration and multi-master configurations.

1. Introduction to I2C

Inter-Integrated Circuit (I2C), pronounced "I-squared-C", is a synchronous, multi-master, multi-slave, serial communication protocol developed by Philips Semiconductor (now NXP Semiconductors) in 1982. It enables communication between microcontrollers and peripheral devices using just two bidirectional lines, making it extremely popular in embedded systems.

I2C is widely used for connecting low-speed peripherals such as sensors, EEPROMs, real-time clocks, displays, and other integrated circuits on the same circuit board. Its simplicity and efficiency have made it a standard protocol in modern electronics.

Key Advantage: I2C requires only two wires (SDA and SCL) regardless of the number of devices connected, significantly reducing PCB complexity compared to parallel or SPI communication.
Common Applications
  • Sensor interfacing (temperature, pressure, accelerometer)
  • EEPROM and Flash memory access
  • Real-Time Clock (RTC) modules
  • Display controllers (OLED, LCD)
  • ADC and DAC converters
  • GPIO expanders and port extenders

2. I2C Basics and Architecture

Two-Wire Interface

I2C uses only two bidirectional lines for communication:

  • SDA (Serial Data Line): Bidirectional data line for transmitting and receiving data
  • SCL (Serial Clock Line): Clock signal generated by the master to synchronize data transfer

Both lines are open-drain/open-collector and require external pull-up resistors to VCC (typically 3.3V or 5V). This configuration allows multiple devices to share the bus and enables multi-master operation.

[I2C Bus Architecture Diagram]
Master Device → SDA/SCL Bus → Multiple Slave Devices
with Pull-up Resistors on both lines
Open-Drain Configuration

The open-drain nature of I2C lines means that devices can only pull the line LOW. The pull-up resistors pull the line HIGH when no device is actively driving it LOW. This configuration enables:

  • Multiple devices to share the same bus
  • Clock stretching (slave can hold SCL low to slow down master)
  • Bus arbitration in multi-master configurations
  • Detection of bus conflicts
Speed Modes

I2C supports multiple speed modes:

Mode Max Speed Typical Use
Standard Mode 100 kbps Basic sensors, EEPROMs
Fast Mode 400 kbps Most modern devices
Fast Mode Plus 1 Mbps High-speed sensors
High Speed Mode 3.4 Mbps Specialized applications

3. Addressing and Data Transfer

7-bit and 10-bit Addressing

I2C uses addressing to identify specific slave devices on the bus:

  • 7-bit addressing: Most common, supports up to 128 devices (0x00 to 0x7F)
  • 10-bit addressing: Extended mode, supports up to 1024 devices

The address is followed by a Read/Write bit (R/W̅): 0 for Write, 1 for Read.

Start and Stop Conditions

I2C communication begins with a START condition and ends with a STOP condition:

  • START (S): SDA transitions from HIGH to LOW while SCL is HIGH
  • STOP (P): SDA transitions from LOW to HIGH while SCL is HIGH
  • Repeated START (Sr): START condition without preceding STOP, used for direction changes
Important: Between START and STOP conditions, SDA can only change when SCL is LOW. This rule enables the bus to distinguish between data bits and control conditions.
Data Transfer Sequence

A typical I2C write transaction follows this sequence:

  1. Master sends START condition
  2. Master sends 7-bit slave address + Write bit (0)
  3. Addressed slave sends ACK
  4. Master sends data byte
  5. Slave sends ACK
  6. Steps 4-5 repeat for multiple bytes
  7. Master sends STOP condition

For read operations:

  1. Master sends START condition
  2. Master sends 7-bit slave address + Read bit (1)
  3. Addressed slave sends ACK
  4. Slave sends data byte
  5. Master sends ACK (or NACK for last byte)
  6. Steps 4-5 repeat for multiple bytes
  7. Master sends STOP condition
ACK and NACK

Acknowledgment signals are critical for I2C communication:

  • ACK (Acknowledge): Receiver pulls SDA LOW during the 9th clock pulse to acknowledge receipt
  • NACK (Not Acknowledge): Receiver leaves SDA HIGH during the 9th clock pulse
  • Master sends NACK after receiving the last byte to signal end of read
  • Slave sends NACK if unable to receive more data

4. Pull-up Resistor Calculations

Pull-up resistors are essential for I2C operation. Selecting the correct value is crucial for reliable communication.

Calculating Minimum Resistance

The minimum pull-up resistance is limited by the maximum current the I2C pins can sink:

R_min = (V_DD - V_OL) / I_OL

Where:
- V_DD = Supply voltage (e.g., 3.3V or 5V)
- V_OL = Output LOW voltage (typically 0.4V)
- I_OL = Maximum LOW-level output current (typically 3mA)

Example for 5V system:
R_min = (5V - 0.4V) / 3mA = 1.53 kΩ

Practical minimum: 1.8 kΩ
Calculating Maximum Resistance

The maximum pull-up resistance is limited by the rise time requirement:

R_max = t_r / (0.8473 × C_bus)

Where:
- t_r = Maximum rise time (depends on I2C speed mode)
- C_bus = Total bus capacitance (wire + device capacitance)

Rise time requirements:
- Standard Mode (100 kHz): t_r = 1000 ns
- Fast Mode (400 kHz): t_r = 300 ns
- Fast Mode Plus (1 MHz): t_r = 120 ns

Example for Fast Mode with 100pF bus capacitance:
R_max = 300ns / (0.8473 × 100pF) = 3.54 kΩ

Practical maximum: 3.3 kΩ
Recommended Values
I2C Speed Bus Capacitance Recommended Resistor
100 kHz 100-200 pF 4.7 kΩ - 10 kΩ
400 kHz 100-200 pF 2.2 kΩ - 4.7 kΩ
400 kHz 200-400 pF 1.0 kΩ - 2.2 kΩ
1 MHz 100-200 pF 1.0 kΩ - 2.2 kΩ
Warning: Using pull-up resistors that are too weak (high resistance) will cause slow rise times and communication errors. Using resistors that are too strong (low resistance) will increase power consumption and may exceed the current sink capability of the I2C pins.

5. Multi-Master Configuration and Arbitration

I2C supports multiple master devices on the same bus. When two or more masters attempt to communicate simultaneously, an arbitration mechanism determines which master continues.

Clock Synchronization

When multiple masters generate clock signals:

  • All masters use the same SCL line
  • The actual SCL signal is the logical AND of all master clocks
  • The master with the slowest clock dominates
  • All masters wait for SCL to go HIGH before proceeding
Arbitration Process

Arbitration occurs bit-by-bit during address and data transmission:

  1. Each master monitors the SDA line while transmitting
  2. If a master transmits HIGH but reads LOW, it loses arbitration
  3. The losing master immediately stops driving the bus
  4. The winning master continues uninterrupted
  5. The losing master waits and retries later
Non-Destructive Arbitration: I2C arbitration is non-destructive, meaning the winning master's message is not corrupted. The losing master simply backs off and retries later.
Clock Stretching

Slaves (and masters when receiving) can hold the SCL line LOW to slow down communication:

  • Used when slave needs more time to process data
  • Master must wait for SCL to be released before continuing
  • Common in slower devices like EEPROMs during write operations
  • Some microcontrollers don't support clock stretching - check datasheets

6. Interfacing Common I2C Devices

Popular I2C Devices and Addresses
Device Default Address Application
DS1307 RTC 0x68 Real-Time Clock
MPU6050 0x68 / 0x69 Gyroscope + Accelerometer
BMP280 0x76 / 0x77 Temperature + Pressure Sensor
SSD1306 OLED 0x3C / 0x3D 128x64 OLED Display
AT24C256 EEPROM 0x50-0x57 256 Kbit EEPROM
PCF8574 GPIO 0x20-0x27 8-bit I/O Expander
ADS1115 0x48-0x4B 16-bit ADC
Scanning for I2C Devices

Use an I2C scanner to detect all devices on the bus - useful for troubleshooting:

Arduino I2C Scanner
#include <Wire.h>

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

Serial.println("I2C Scanner");
Serial.println("Scanning...");

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

Serial.print("Found ");
Serial.print(count);
Serial.println(" device(s)");
}

void loop() {
}
Best Practices for Device Integration
  • Always check device datasheet for correct I2C address
  • Some devices have configurable addresses via hardware pins
  • Use appropriate pull-up resistors (typically 4.7kΩ for 100kHz)
  • Keep I2C wire length under 1 meter for reliable operation
  • Add decoupling capacitors (0.1µF) near device power pins
  • Use shielded cables for noisy environments
  • Level shifters required when mixing 3.3V and 5V devices

7. Code Examples

Arduino - Basic I2C Write
Writing to I2C Device
#include <Wire.h>

#define DEVICE_ADDRESS 0x68 // Example: DS1307 RTC

void setup() {
Serial.begin(9600);
Wire.begin(); // Initialize I2C as master

Serial.println("I2C Master initialized");
}

void writeRegister(uint8_t reg, uint8_t value) {
Wire.beginTransmission(DEVICE_ADDRESS);
Wire.write(reg); // Register address
Wire.write(value); // Data to write
byte error = Wire.endTransmission();

if (error == 0) {
Serial.println("Write successful");
} else {
Serial.print("Write failed, error: ");
Serial.println(error);
}
}

void loop() {
writeRegister(0x00, 0x45); // Write 0x45 to register 0x00
delay(1000);
}
Arduino - Basic I2C Read
Reading from I2C Device
#include <Wire.h>

#define DEVICE_ADDRESS 0x68

uint8_t readRegister(uint8_t reg) {
Wire.beginTransmission(DEVICE_ADDRESS);
Wire.write(reg); // Register to read from
Wire.endTransmission(false); // Repeated start

Wire.requestFrom(DEVICE_ADDRESS, 1); // Request 1 byte

if (Wire.available()) {
return Wire.read();
}
return 0;
}

void readMultipleBytes(uint8_t reg, uint8_t *buffer, uint8_t length) {
Wire.beginTransmission(DEVICE_ADDRESS);
Wire.write(reg);
Wire.endTransmission(false);

Wire.requestFrom(DEVICE_ADDRESS, length);

for (int i = 0; i < length && Wire.available(); i++) {
buffer[i] = Wire.read();
}
}

void loop() {
uint8_t value = readRegister(0x00);
Serial.print("Register value: 0x");
Serial.println(value, HEX);

delay(1000);
}
STM32 - HAL I2C Implementation
STM32 HAL I2C Configuration
/* I2C1 initialization - Configure via CubeMX */
void I2C_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100 kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

HAL_I2C_Init(&hi2c1);
}

/* Write single byte to register */
void I2C_WriteRegister(uint8_t devAddr, uint8_t reg, uint8_t data) {
uint8_t buffer[2];
buffer[0] = reg;
buffer[1] = data;

HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, buffer, 2, HAL_MAX_DELAY);
}

/* Read single byte from register */
uint8_t I2C_ReadRegister(uint8_t devAddr, uint8_t reg) {
uint8_t data;

HAL_I2C_Mem_Read(&hi2c1, devAddr << 1, reg,
I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);

return data;
}
/* Read multiple bytes from consecutive registers */
void I2C_ReadMultiple(uint8_t devAddr, uint8_t reg, uint8_t *buffer, uint8_t length) {
HAL_I2C_Mem_Read(&hi2c1, devAddr << 1, reg,
I2C_MEMADD_SIZE_8BIT, buffer, length, HAL_MAX_DELAY);
}
MPU6050 Accelerometer Example
Complete MPU6050 Integration
#include <Wire.h>

#define MPU6050_ADDR 0x68
#define PWR_MGMT_1 0x6B
#define ACCEL_XOUT_H 0x3B

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

// Wake up MPU6050
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(PWR_MGMT_1);
Wire.write(0); // Set to zero to wake up
Wire.endTransmission(true);

Serial.println("MPU6050 initialized");
}

void readAccelData(int16_t *ax, int16_t *ay, int16_t *az) {
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(ACCEL_XOUT_H);
Wire.endTransmission(false);

Wire.requestFrom(MPU6050_ADDR, 6, true);

*ax = (Wire.read() << 8) | Wire.read();
*ay = (Wire.read() << 8) | Wire.read();
*az = (Wire.read() << 8) | Wire.read();
}

void loop() {
int16_t ax, ay, az;
readAccelData(&ax, &ay, &az);

Serial.print("X: "); Serial.print(ax);
Serial.print(" | Y: "); Serial.print(ay);
Serial.print(" | Z: "); Serial.println(az);

delay(500);
}

8. Troubleshooting Common Issues

Common Problems and Solutions

1. No Device Found / No ACK Received

  • Check wiring: Ensure SDA and SCL are correctly connected
  • Verify power supply to slave device
  • Confirm correct I2C address (use scanner code)
  • Check if pull-up resistors are present (4.7kΩ typical)
  • Some devices require initialization sequence before responding

2. Intermittent Communication Errors

  • Reduce bus speed (try 100kHz instead of 400kHz)
  • Shorten wire length (keep under 30cm for reliable operation)
  • Add decoupling capacitors near device power pins
  • Check for electrical noise sources nearby
  • Verify pull-up resistor values are appropriate for bus capacitance

3. Bus Lockup / Frozen Communication

  • Power cycle all devices to reset bus state
  • Implement software I2C reset by toggling SCL while monitoring SDA
  • Check if slave is holding SCL low (clock stretching issue)
  • Verify no short circuits on SDA or SCL lines

4. Wrong Data Received

  • Verify endianness (MSB vs LSB first)
  • Check register addresses against datasheet
  • Ensure proper delays between read/write operations
  • Confirm device is in correct operating mode
Using Logic Analyzer for Debugging

A logic analyzer is invaluable for I2C debugging:

  • Capture and decode I2C transactions
  • Verify START and STOP conditions
  • Check ACK/NACK signals
  • Measure timing parameters
  • Detect bus conflicts in multi-master systems
Important: Always check slave device datasheet for specific timing requirements, initialization sequences, and register maps. Many I2C issues stem from incorrect device configuration.

9. Advantages and Disadvantages

Advantages
  • Only two wires required regardless of number of devices
  • Simple hardware implementation
  • Built-in addressing system supports multiple devices
  • Multi-master capability with arbitration
  • Widely supported across microcontroller platforms
  • ACK/NACK provides built-in error detection
  • Lower pin count than SPI or parallel interfaces
  • Hot-swapping capability (devices can be added/removed)
Disadvantages
  • Slower than SPI (typically 100-400 kbps vs 10+ Mbps)
  • Half-duplex communication (cannot transmit and receive simultaneously)
  • Pull-up resistors increase power consumption
  • Limited to short distances (typically under 1 meter)
  • Bus capacitance limits number of devices and speed
  • Open-drain outputs require pull-up resistors
  • More complex protocol than UART
  • Potential address conflicts with multiple devices
When to Choose I2C

Use I2C when:

  • Multiple devices need to share a bus with minimal wiring
  • Board space is limited (fewer pins/traces needed)
  • Communication speed is not critical (< 1 Mbps acceptable)
  • Built-in addressing simplifies device management

Consider alternatives when:

  • High-speed communication required → Use SPI
  • Long-distance communication needed → Use UART/RS485
  • Robust automotive environment → Use CAN bus
  • Very simple point-to-point communication → Use UART

10. Summary and Next Steps

You've learned the fundamentals of I2C communication including:

  • Two-wire architecture with SDA and SCL lines
  • 7-bit and 10-bit addressing schemes
  • START, STOP, and ACK/NACK conditions
  • Pull-up resistor calculations and selection
  • Multi-master operation and arbitration
  • Practical device interfacing and troubleshooting
  • Code implementation on Arduino and STM32
Next Steps:
  • Build a project with multiple I2C devices (sensor + display + RTC)
  • Experiment with different pull-up resistor values and measure effects
  • Implement I2C communication in bare-metal (without libraries)
  • Try interfacing more complex devices like IMUs or OLED displays
  • Learn about I2C level shifting for mixed-voltage systems
  • Explore I2C expanders to overcome pin limitations
Recommended Projects
  1. Weather Station: Combine BMP280 sensor with OLED display and DS1307 RTC
  2. Motion Tracker: Use MPU6050 to track movement and log to EEPROM
  3. Multi-Sensor Dashboard: Interface multiple sensors and display data in real-time
  4. GPIO Expander: Use PCF8574 to control multiple LEDs or read buttons