Communication Protocols

Lesson 77: Using Wire (I2C) Communication

Learn I2C fundamentals, Wire library flow, and how master/slave devices exchange data safely.

Progress indicator

Lesson 77 of 77

Learning Objectives

  • Understand what I2C communication is and why it is useful for connecting many peripherals.
  • Learn how the Wire library manages master-side setup, transmission, and data requests.
  • Understand SDA/SCL wiring rules, device addressing, and byte-level transaction flow.
  • Use Wire.write(), Wire.read(), and Wire.available() in a safe and repeatable beginner pattern.
  • Avoid common I2C mistakes such as wrong address format, pull-up issues, and unchecked status codes.

Concept Explanation

What is I2C Communication

I2C (Inter-Integrated Circuit) is a two-wire communication protocol that allows one master to communicate with multiple slave devices using addresses.

It is widely used in Arduino/ESP32 projects because wiring stays simple even when you add multiple sensors and display modules.

Wire Library Overview

The Wire library is the standard Arduino API for I2C. It handles start, address, read/write bytes, and stop behavior for you.

  • Begin bus with Wire.begin().
  • Send data with beginTransmission() + write() + endTransmission().
  • Receive data with requestFrom() + available() + read().

I2C Master vs Slave

  • Master starts transactions and controls the clock.
  • Slave responds when its address is requested by master.
  • Many slaves can share the same SDA/SCL bus lines.
  • Each slave must have a unique address on the same bus.

I2C Signal Lines (SDA, SCL)

  • SDA: serial data line.
  • SCL: serial clock line controlled by the master.
  • Both lines usually need pull-up resistors for stable operation.
  • All devices must share common ground to communicate reliably.

How Wire.begin() Works

Wire.begin() initializes I2C hardware as master. After this, your code can start write and read transactions to device addresses.

In most beginner setups, calling Wire.begin() once inside setup()is enough to start master-mode communication.

Wire Transmission Process

  1. Wire.beginTransmission(address) starts target write sequence.
  2. Wire.write(...) pushes bytes into TX buffer.
  3. Wire.endTransmission() sends buffered bytes and returns status code.
  4. Wire.requestFrom(...) asks slave for N bytes.
  5. Wire.read() pulls received bytes from RX buffer.
  6. Application logic converts bytes into meaningful values (registers, sensor data).

Wire.read() and Wire.write()

Use Wire.write() for outgoing bytes and Wire.read() for incoming bytes after requestFrom(). Always check Wire.available() before reading.

A common pattern is writing a register address first, then requesting bytes from that register. This is how many I2C sensors are read.

When to Use I2C

  • Connecting sensors, RTC modules, OLED displays, and IO expanders.
  • When you need many peripherals with only two signal wires.
  • When bandwidth needs are moderate and bus simplicity matters.
  • When module ecosystem and library support are important for fast prototyping.

Example Code

This example writes config bytes, then reads two bytes from an I2C device.

#include <Wire.h>

const byte SENSOR_ADDR = 0x3C;

void setup() {
  Serial.begin(115200);
  Wire.begin(); // Start as I2C master
  Serial.println("I2C master ready");
}

void loop() {
  Wire.beginTransmission(SENSOR_ADDR);
  Wire.write(0x01);      // Register address
  Wire.write(0x64);      // Example config value
  byte txStatus = Wire.endTransmission();

  Serial.print("TX status: ");
  Serial.println(txStatus);

  Wire.requestFrom(SENSOR_ADDR, (byte)2);
  if (Wire.available() >= 2) {
    byte highByte = Wire.read();
    byte lowByte = Wire.read();
    int value = (highByte << 8) | lowByte;

    Serial.print("I2C value: ");
    Serial.println(value);
  } else {
    Serial.println("No I2C data available");
  }

  delay(800);
}

Example Code Explanation

  1. Wire.begin() initializes ESP32/Arduino as I2C master device.
  2. beginTransmission(0x3C) selects the target slave address.
  3. First Wire.write(0x01) sets target register address.
  4. Second Wire.write(0x64) sends example config payload byte.
  5. endTransmission() sends bytes and returns status: 0 means ACK success.
  6. Status is printed to Serial so bus errors are visible during testing.
  7. requestFrom() asks slave for exactly two bytes of data.
  8. Wire.available() >= 2 ensures full payload exists before reading.
  9. highByte and lowByte are combined into one 16-bit value.
  10. Final value is printed to Serial Monitor for validation and debugging.

What Happens Inside

  1. Master generates START condition on the I2C bus.
  2. Address + write/read bit is placed on SDA and clocked by SCL.
  3. Slave sends ACK/NACK to confirm reception.
  4. Data bytes move across SDA one byte at a time with ACK steps.
  5. Master ends transaction with STOP condition.
  6. Wire library buffers bytes, then your code interprets those bytes into final values.
  7. Serial logging gives visibility into each transmission success/failure state.

Common Mistakes with Wire

  • Using the wrong slave address (7-bit vs shifted address confusion).
  • Missing pull-up resistors on SDA/SCL, causing unstable communication.
  • Calling Wire.read() without checking Wire.available().
  • Ignoring endTransmission() status code while debugging.
  • Forgetting shared GND between microcontroller and I2C modules.
  • Requesting more bytes than the slave actually provides.

Best Practices for Wire

  • Verify device address with an I2C scanner before coding full logic.
  • Check return values and print debug messages for each transaction stage.
  • Keep wire length short and shared ground stable for reliable communication.
  • Read datasheet register map carefully before write/read sequences.
  • Start with slow, simple reads first, then add multi-byte parsing logic.
  • Keep a consistent byte-order (endianness) rule when combining high/low bytes.

Try it now

Open the simulator workspace and step through I2C Wire transaction flow and byte parsing.

Run in Simulator