HomeArduino TutorialsComplete Arduino I2C Tutorial with Examples: Wire Library & Projects

Arduino I2C Tutorial: Wire Library & Practical Examples

I2C (also called I²C or TWI) is a simple and powerful communication method. It only uses two wires — SDA (data) and SCL (clock). That’s why many sensors, modules, and displays use it.

In this guide, you’ll learn how Arduino handles I2C with the Wire library. I’ll walk you through both the theory and real examples. You’ll see how to send data, read data, and connect devices like an I2C LCD.

By the end, you’ll be able to build your own Arduino I2C projects confidently. Let’s dive in!

Arduino I2C Tutorial: Wire Library & Practical Examples

What Is I2C & Why Use It

I2C(I2C), short for Inter-Integrated Circuit, is a simple communication protocol designed for short-distance data transfer between microcontrollers and peripherals. It was originally developed by Philips and has become a standard for many embedded systems and sensors.

The main reason for using I2C is its efficiency — it requires only two wires, no matter how many devices are connected. These two lines are shared by all devices on the bus:

  • SDA (Serial Data Line) – carries the actual data.
  • SCL (Serial Clock Line) – provides the timing signal for synchronization.

Each device on the I2C bus has a unique address, allowing the master (like an Arduino) to communicate with multiple slaves (like sensors or displays) using the same pair of wires.
This reduces wiring complexity and makes it easy to expand your system with more devices.

How multiple devices along with Arduino microcontroller can be connected to same I2C bus.

I2C is especially useful when you need to:

  • Connect multiple devices with limited I/O pins.
  • Communicate with sensors, memory chips, or real-time clocks.
  • Exchange small amounts of data quickly and efficiently.

I2C Basics & Signals

The I2C communication uses specific signal sequences to manage data transfer between the master and slave devices. The main signals are:

  • Start Condition:
    The master begins communication by pulling the SDA line low while SCL remains high. This signals all connected devices that a new transmission is about to start.
  • Stop Condition:
    After completing the data transfer, the master releases the SDA line (pulls it high) while SCL is high. This marks the end of communication.
  • ACK (Acknowledge):
    After each byte of data is sent, the receiving device pulls the SDA line low to confirm successful reception.
  • NACK (Not Acknowledge):
    If the receiver does not pull SDA low after a byte, it indicates a problem or that no more data is needed.
I2C Timing diagram showing Start Condition, Stop Condition, ACK and NACK.

These signals ensure proper synchronization between devices and reliable data transfer even when multiple devices share the same bus.


I2C Speed Modes & Bus Limits

I2C supports several speed modes, which define how fast data can be transferred. The most common ones are:

ModeSpeed
Standard ModeUp to 100 kHz
Fast ModeUp to 400 kHz
Fast Mode PlusUp to 1 MHz
High-Speed ModeUp to 3.4 MHz

While faster modes allow quicker communication, they may also increase noise and signal distortion, especially over longer wires.
In most Arduino projects, Standard or Fast Mode is sufficient for sensors and small peripherals.

It’s also important to remember that the total bus length and number of devices affect performance. The more devices connected, the higher the chance of interference, so keeping the wiring short helps maintain signal reliability.


Physical Layer & Pull-ups

The I2C bus uses open-drain (or open-collector) outputs. This means that devices can only pull the line low, not drive it high. Because of this, both SDA and SCL lines need pull-up resistors to keep the lines high when no device is pulling them low.

Typically, resistors between 4.7 kΩ and 10 kΩ are used, depending on the bus voltage and number of devices. Without proper pull-ups, the signals might not reach valid logic levels, causing data errors or unstable communication.

Arduino & I2C: Wire Library Overview

The Arduino provides built-in support for I2C communication using the Wire library. This library makes it simple to connect your Arduino board to I2C-compatible devices such as sensors, LCD displays, and EEPROMs.
With just a few lines of code, you can send or receive data over the I2C bus without worrying about the low-level timing or protocol details.

When you use the Wire library, Arduino acts as either a master (initiating communication) or a slave (responding to a master). Most of the time, your Arduino will be the master, sending commands to connected devices.

Let’s explore how Arduino supports I2C communication, which pins are used, and what functions are available in the Wire library.

Arduino I2C Pins on Different Boards

Each Arduino board has dedicated pins for I2C communication, and these pins are connected internally to the microcontroller’s I2C hardware. Knowing which pins to use is essential to get your I2C setup working properly.

Here’s a quick list of I2C pins for popular Arduino boards:

Arduino BoardSDA PinSCL Pin
Arduino Uno / NanoA4A5
Arduino Mega 25602021
Arduino Leonardo / Micro23
Arduino Due2021
Arduino Uno R4SDA / SCL Pins Near USB Port

On most modern Arduino boards, you’ll also find SDA and SCL labeled directly on the board, making it easier to locate them.
Some advanced boards even support multiple I2C ports, which can be useful if you want to connect several devices that share the same address.


Arduino Wire Library Functions Explained

The Wire library provides a simple and powerful interface for I2C communication. Below are the most important functions you’ll use in your Arduino sketches:

  • Wire.begin()
    Initializes the I2C interface. If no address is provided, the Arduino acts as a master. You can also call Wire.begin(address) to make the Arduino a slave with the specified address.
  • Wire.beginTransmission(address)
    Starts communication with a slave device at the given address (address of the slave device).
  • Wire.write(data)
    Sends data to the slave device. You can send a single byte, multiple bytes, or even strings.
  • Wire.endTransmission()
    Ends the transmission and sends all written bytes to the slave.
  • Wire.requestFrom(address, quantity)
    Requests a specific number of bytes from a slave device.
  • Wire.read()
    Reads the incoming data that was requested from the slave.
  • Wire.available()
    Returns how many bytes are available to read.

These functions handle all the background work for you — such as generating start and stop conditions, managing ACK/NACK signals, and timing synchronization.
That’s what makes the Wire library perfect for beginners and professionals alike.


Addressing & 7-bit vs 8-bit

Every device on the I2C bus has a unique address, which allows the master (Arduino) to identify and communicate with it. Most I2C devices use a 7-bit address, which gives 128 possible addresses (from 0x00 to 0x7F).

When using the Wire library, always provide the 7-bit address as shown in the device’s datasheet. The library automatically adds the read/write bit (the 8th bit) depending on whether you are sending or receiving data.

For example:

  • If your device has a 7-bit address of 0x68, use Wire.beginTransmission(0x68) to communicate with it.
  • Don’t shift the address left or right — the library takes care of that automatically.

Common I2C addresses you might encounter include:

  • LCD with PCF8574 chip: 0x27 or 0x3F
  • MPU6050 sensor: 0x68
  • DS3231 RTC module: 0x68

If you’re unsure of your device’s address, you can easily find it using an I2C Scanner sketch, which checks all possible addresses and prints any detected devices on the Serial Monitor.

Arduino I2C Master & Slave Programming

The Arduino can act as either an I2C master or an I2C slave.

  • As a master, the Arduino initiates communication and controls the clock line.
  • As a slave, it responds when the master sends data or requests information.

Let’s look at how to write simple I2C programs for both modes using the Wire library.

Writing Data as Master

When the Arduino acts as a master, it can send commands or data to any slave device on the bus.
Here’s the basic process:

  1. Start the communication with the target device’s address using Wire.beginTransmission(address).
  2. Send data using Wire.write().
  3. End the transmission using Wire.endTransmission().

Example Code – Arduino I2C Master Write:

#include <Wire.h>

void setup() {
  Wire.begin();  // Initialize as master
}

void loop() {
  Wire.beginTransmission(0x27);  // Address of the I2C device
  Wire.write("Hello");           // Send string data
  Wire.endTransmission();        // Stop communication
  delay(1000);
}

This code sends the word “Hello” every second to a device with the I2C address 0x27 (for example, an I2C LCD).
The Wire library automatically generates the start and stop conditions, so you don’t need to handle them manually.


Reading Data as Master

To read data from an I2C device, the Arduino requests bytes from a specific slave using Wire.requestFrom(). After that, the data can be read one byte at a time with Wire.read().

Example Code – Arduino I2C Master Read:

#include <Wire.h>

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

void loop() {
  Wire.requestFrom(0x68, 2);  // Request 2 bytes from the slave

  while (Wire.available()) {
    char c = Wire.read();     // Read each byte
    Serial.print(c);          // Print received data
  }

  delay(1000);
}

In this example, the master requests 2 bytes from the device at address 0x68 and prints them to the Serial Monitor.
This method is often used to read sensor data like temperature, humidity, or accelerometer values.


Slave Mode Setup & Callbacks

You can also make your Arduino act as an I2C slave, allowing it to communicate with another master (for example, another Arduino).
To do this, you’ll assign it a unique address and define callback functions to handle incoming data or requests.

Example Code – Arduino I2C Slave:

#include <Wire.h>

void setup() {
  Wire.begin(0x08);              // Initialize as slave with address 0x08
  Wire.onReceive(receiveEvent);  // Called when data is received
  Wire.onRequest(requestEvent);  // Called when master requests data
  Serial.begin(9600);
}

void loop() {
  delay(100);
}

void receiveEvent(int howMany) {
  while (Wire.available()) {
    char c = Wire.read();        // Read received data
    Serial.print(c);
  }
  Serial.println();
}

void requestEvent() {
  Wire.write("ACK");             // Send response to master
}

This example shows how to use an Arduino as an I2C slave device using the Wire library. The slave remains ready to receive messages or respond to requests from the master device. Let’s understand it in detail.

In the setup() function, we initialize the Arduino as a slave with a unique address and attach two event functions:

void setup() {
  Wire.begin(0x08);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
  Serial.begin(9600);
}

Here’s what each function does:

  • Wire.begin(0x08) – Initializes the Arduino as an I2C slave with address 0x08.
    The master will use this address to communicate with the slave.
  • Wire.onReceive(receiveEvent) – Registers a callback that executes when the master sends data.
  • Wire.onRequest(requestEvent) – Registers a callback that executes when the master requests data.
  • Serial.begin(9600) – Starts the Serial Monitor for debugging and displaying incoming data.

This setup ensures that all I2C communication is handled automatically through interrupts. This means that the Arduino doesn’t have to continuously check for incoming messages.


The function receiveEvent() is automatically called whenever the slave receives data from the master:

void receiveEvent(int howMany) {
  while (Wire.available()) {
    char c = Wire.read();
    Serial.print(c);
  }
  Serial.println();
}

Here’s how it works step-by-step:

  1. howMany tells how many bytes have been received.
  2. Wire.read() reads each byte one by one from the I2C buffer.
  3. Serial.print(c) displays the received characters in the Serial Monitor.
  4. Serial.println() adds a newline for readability after the full message.

For example, if the master sends the word "Hello", the Serial Monitor will show:

Hello

This confirms that the slave successfully received data over I2C.


The requestEvent() function is called whenever the master requests data from the slave:

void requestEvent() {
  Wire.write("ACK");
}

This function simply sends back the message “ACK” as a response. You can replace this with any data you want to send, such as a sensor value, temperature reading, or system status.

For instance, when the master executes Wire.requestFrom(0x08, 3);, the slave will respond with the three characters A, C, and K.


I2C Scanner & Device Discovery

When working with I2C devices, one of the first challenges you might face is finding the correct I2C address of your module or sensor. Some devices come with multiple possible addresses, while others may have solder jumpers that change their default address.

To make things easier, you can use an I2C Scanner — a simple Arduino sketch that automatically detects and lists all active I2C devices connected to your board.
This method saves time, helps avoid address conflicts, and ensures your communication setup is correct before you write your main program.

Why Use an I2C Scanner

An I2C scanner is a must-have tool for every Arduino user working with I2C peripherals. Here’s why it’s so useful:

  • Find Unknown Addresses:
    Many sensors and modules don’t clearly mention their I2C address on the board. The scanner quickly reveals it.
  • Detect Multiple Devices:
    If you have multiple I2C modules connected, the scanner helps confirm that each one is recognized on the bus.
  • Check Wiring and Connections:
    If a device doesn’t show up in the scan, it could mean there’s a wiring issue, a missing pull-up resistor, or a faulty connection.
  • Debug Communication Issues:
    When your project isn’t responding, running an I2C scan is a quick way to confirm whether the device is communicating correctly with the Arduino.

Example Scanner Sketch

Here’s a simple and effective Arduino I2C scanner code that works on all boards using the Wire library:

#include <Wire.h>

void setup() {
  Wire.begin();
  Serial.begin(9600);
  Serial.println("I2C Scanner Starting...");
}

void loop() {
  byte error, address;
  int nDevices;

  Serial.println("Scanning for I2C devices...");
  nDevices = 0;

  for (address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println(" !");
      nDevices++;
    } else if (error == 4) {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }

  if (nDevices == 0)
    Serial.println("No I2C devices found.\n");
  else
    Serial.println("Scan complete.\n");

  delay(5000); // Wait before next scan
}

How the Code Works

  1. The code initializes I2C communication using Wire.begin() and starts the Serial Monitor.
  2. It loops through all possible 7-bit I2C addresses (1 to 126).
  3. For each address, it tries to start a transmission and checks the return code from Wire.endTransmission().
  4. If no error occurs (error == 0), that means a device responded — and its address is printed on the Serial Monitor.
  5. The loop continues until all possible addresses are tested.

Once you upload this sketch and open the Serial Monitor (set to 9600 baud), you’ll see output like:

I2C Scanner Starting...
Scanning for I2C devices...
I2C device found at address 0x27 !
Scan complete.

This confirms that a device was found at address 0x27, which you can now use in your main Arduino code.

If you don’t see any devices listed, double-check your SDA and SCL connections, pull-up resistors, and power supply.

Troubleshooting & Optimization

When working with I2C communication, things don’t always go smoothly. You may face problems like missing devices, communication errors, or bus conflicts. Fortunately, most of these issues are easy to fix once you understand their causes.
Let’s go through some common I2C problems, and how to solve them effectively.

Address Conflicts & Duplicates — How to Handle Duplicate I2C Addresses

Each I2C device on the bus must have a unique 7-bit address. However, sometimes two devices come with the same default address. When that happens, both respond at once — causing data corruption and failed communication.

How to fix duplicate addresses:

  • Check the datasheet: Many I2C modules include address selection pins (like A0, A1, A2). You can tie these pins to HIGH or LOW to change the device address.
  • Use jumpers or solder pads: Some breakout boards have solder jumpers to modify the address.
  • Use an I2C multiplexer: If address modification isn’t possible, a multiplexer (like TCA9548A) can isolate each device on its own channel.
  • Separate buses: As a last resort, use separate I2C buses on different microcontroller pins if your board supports it.

Always verify addresses with an I2C scanner sketch before starting your main program to ensure all devices are uniquely identified.


Using I2C Multiplexers — Expandable Bus Options

An I2C multiplexer is a special device that allows you to connect multiple devices with the same I2C address to a single microcontroller.
The most popular example is the TCA9548A, which provides 8 separate I2C channels.

Here’s how it works:

  • The Arduino communicates with the multiplexer at its fixed address.
  • You select which channel to enable using a command.
  • Only the devices connected to that channel are active, while others stay disconnected.

Benefits of using an I2C multiplexer:

  • Eliminates address conflicts completely.
  • Supports long wiring by isolating devices across channels.
  • Makes complex sensor networks easier to manage.

Multiplexers are ideal when building large I2C sensor arrays, or when using identical sensors with fixed addresses.


Pull-up Resistor Tips & Level Shifting — Ensure Signal Integrity

The I2C bus relies on pull-up resistors to keep the SDA and SCL lines high when no device is pulling them low.
Without proper pull-ups, the signals may float, causing unreliable communication or random device detection errors.

Key points about pull-ups:

  • Typical resistor values range between 4.7kΩ and 10kΩ.
  • You only need one set of pull-ups per bus, not on every device.
  • If you have multiple boards with built-in pull-ups, you may need to remove extra ones to avoid loading the bus.

Voltage level shifting:
Make sure all devices on the I2C bus use the same voltage level.
For example:

  • If your Arduino runs at 5V and the sensor works at 3.3V, use a bi-directional logic level shifter to protect the low-voltage device.
  • Some I2C modules already include built-in level shifters — always check the datasheet.

Tip: Keep I2C wires as short as possible. Long wires increase capacitance, which can distort signals, especially at higher speeds like 400 kHz or more.

Conclusion

In this post, we learned how I2C communication works and how to use it with Arduino. We started with the basics of I2C signals and speed modes, then explored the Wire library and its key functions. You saw examples of master and slave communication, learned how to run an I2C scanner, and built a real project using an I2C LCD.
We also covered bus configurations, common issues, and best practices for stable performance. With this knowledge, you can now confidently connect multiple I2C devices and build smarter Arduino projects using this powerful two-wire interface.

Browse More Arduino Tutorials

Arduino I2C FAQs

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments