In this tutorial, we’ll discuss what the I2C communication protocol is, how it works, and how to use it on the Arduino. To demonstrate, we’re going to build a project that uses I2C communication to exchange data between two Arduino microcontrollers.

What is I2C?

I2C is an acronym for Inter-Integrated Circuit. I2C is a low-speed serial communication protocol suitable for transferring data over short distances. If you need to transfer data over a large distance, this protocol is not recommended. An example of a simple I2C network is shown below.

Arduino-I2C-Tutorial-sample-I2C-network
Figure 1: Sample I2C Network

As you can see in the diagram, the advantage of using I2C is that you only need two wires to communicate with multiple devices. All communication passes through the two wires to and from the master and slave devices. This is very useful when doing Arduino projects, since the Arduino has a limited number of input/output pins. Many sensors and modules designed for the Arduino use I2C to communicate.

The I2C Network

An I2C network consists of master and slave devices connected by a bus. An I2C network can have multiple master devices and slave devices.

Slave Devices

All slave devices have an I2C address that is used for identifying the device on the network. The I2C address makes it possible for a master device to send data to a particular slave device on the bus.

Master Devices

Master devices can send and receive data. Slave devices respond to whatever a master device sends. When sending data on the bus, only one device can send data at a time.

The Bus

The bus in I2C is simply the two wires that interconnect all of the I2C devices in the network. The two wires are called SDA and SCL. The SDA wire is used for communication back and forth between the master and slave devices. The SCL line carries the clock signal used for proper communication timing. Pull-up resistors are needed to keep both wires in a HIGH state.

Logic Levels

Care should be taken when connecting I2C devices to the Arduino. The Arduino outputs I2C signals at a 5V logic level, but I2C devices operate at a range of different logic level voltages. So an I2C device that operates at 3.3V could be damaged if connected to the Arduino. The device’s datasheet should tell you it’s logic level voltage. If the pull-up resistors are connected to +5V, all devices should be compatible to work with +5V logic level.

For further reading about I2C, check out our article on Basics of the I2C Communication Protocol.

Making the Arduino Talk I2C

To demonstrate how to use I2C on the Arduino, let’s build a project that sends data back and forth between two Arduinos. We will use I2C communication to change the blink rate of the pin 13 LED on one Arduino, based on the position of a potentiometer connected to the other Arduino. One Arduino will act as the master and the other Arduino will act as the slave.

Arduino I2C Pins

The Arduino has dedicated pins for I2C, which have built-in pull-up resistors as required by the I2C protocol.

For Arduino Uno boards, these are pins A4 and A5. Pin A4 is the SDA pin, and pin A5 is the SCL pin. In the Arduino Uno R3 version, there is another set of I2C pins near the USB socket:

Arduino-UNO-I2C-pins-R3
Figure 2: Arduino I2C pin locations

Hardware Components

To build this project, you’ll need the following parts:

Wiring Diagram

After you gather all the parts, it’s time to assemble the project. Follow the wiring diagram below to connect everything:

How to Set Up I2C Communication for Arduino - Wiring Diagram

You might have noticed that we don’t have pull-up resistors on the SDA and SCL lines. The pull-up resistors are built into the Arduino’s I2C pins already, so we don’t need them.

Sketch for the Arduino Master Device

We have two Arduinos in our I2C network, so we have two sets of sketches. One is for the master Arduino, and the other is for the slave Arduino. There is not much difference between the two sketches as you will see later on.

Now, open the Arduino IDE and upload the code below to the master Arduino:

// Arduino I2C Tutorial
// MASTER sketch

#include <Wire.h>

byte i2c_rcv;               // data received from I2C bus
unsigned long time_start;   // start time in mSec
int stat_LED;               // status of LED: 1 = ON, 0 = OFF
byte value_pot;             // potentiometer position

void setup()
{
	Wire.begin(); // join I2C bus as Master
	
	// initialize global variables
	i2c_rcv = 255;
	time_start = millis();
	stat_LED = 0;
	
	pinMode(13, OUTPUT);    // set pin 13 mode to output
}

void loop()
{
	// read potentiometer position
	value_pot = analogRead(A0);   // read analog value at pin A0 (potentiometer voltage)

	// send potentiometer position to Slave device 0x08
	Wire.beginTransmission(0x08); // informs the bus that we will be sending data to slave device 8 (0x08)
	Wire.write(value_pot);        // send value_pot
	Wire.endTransmission();       // informs the bus and the slave device that we have finished sending data

	Wire.requestFrom(0x08, 1);    // request potentiometer position from slave 0x08
	if(Wire.available()) {	      // read response from slave 0x08
		i2c_rcv = Wire.read();
	}
	
	// blink logic code
	if((millis() - time_start) > (1000 * (float)(i2c_rcv/255))) {
		stat_LED = !stat_LED;
		time_start = millis();
	}
	digitalWrite(13, stat_LED);
}

Explanation of the Code

The basic part of the code for both the master and slave devices is what I call the blink logic code. To blink the pin 13 LED on the Arduinos, we need to do the following:

  • Add global variables byte i2c_rcv,int time_start, stat_LED and byte value_pot at the top of our sketch.
  • Initialize values of the global variables inside setup()
  • Initialize pin 13 of the Arduino as output pin inside setup() using pinMode()
  • Add the blink logic code inside the loop()

Wire Library

To use the Arduino’s built-in I2C interface, we will use the Wire library. This library comes standard with the Arduino IDE. As with other Arduino libraries, the Wire library has ready-made I2C functions to make coding easier for us. To use the functions of the Wire Library, we need to add it first to our sketch. In the sketch above, we have the following line at the top: #include <Wire.h>

After including the library, we can use the library’s built-in functions. The first thing to do is to join the device on the I2C bus. The syntax for this is Wire.begin(address). The address is optional for master devices. So, for the master Arduino sketch, we just add the code Wire.begin(); inside setup().

Now, we move on to the loop(). Our code will make the Arduino read the potentiometer value connected to pin A0 and save the value to the variable value_pot.

Sending Data

After saving the value from pin A0 to the variable value_pot, we can send the value over I2C. Sending data over I2C involves three functions:

Wire.beginTransmission()

Wire.write()

Wire.endTransmission()

Wire.beginTransmission()

We initiate a send command by first informing the devices on the bus that we will be sending data. To do this, we call the function Wire.beginTransmission(address). The address is the I2C address of the slave device that will receive the data. This function does two things:

  1. It informs the bus that we will be sending data
  2. It informs the intended recipient of the data to be ready to receive
Wire.write()

And then we will be sending the value of the variable value_to_send using the function Wire.write(value).

Wire.endTransmission()

After sending the data, we need to free up the network to allow other devices to communicate over the network. This is done by using the function Wire.endTransmission().

Our master device also needs to get the potentiometer position from the slave device. We do this by using Wire.requestFrom(), Wire.available() and Wire.read().

Wire.requestFrom()

The complete syntax to request data from a slave device is Wire.requestFrom(address, quantity). The address is the I2C address of the slave device we need to get data from and quantity is the number of bytes we need. For our project, the slave device address is 0x08 and we need one byte.

Inside the loop(), we use Wire.requestFrom(0x08, 1); to request one byte of data from slave 0x08.

After issuing the command Wire.requestFrom(0x08, 1), it should be followed by a read command to get the response from the I2C bus.

Write.available()

First, we check if there is data available on the bus. We do this by using the function Write.available() inside a conditional if()statement. The function Write.available() returns the number of bytes waiting to be read.

Wire.read();

To get the data available, we use the function Wire.read() and save the return value to the variable i2c_rcv. Each call to the function Wire.read() gets only one byte of data from the I2C bus.

Sketch for the Arduino Slave Device

Now upload this code to the slave Arduino:

// Arduino I2C Tutorial
// SLAVE sketch

#include <Wire.h>

byte i2c_rcv;               // data received from I2C bus
unsigned long time_start;   // start time in mSec
int stat_LED;               // status of LED: 1 = ON, 0 = OFF
byte value_pot;             // potentiometer position

void setup()
{
	Wire.begin(0x08);           // join I2C bus as Slave with address 0x08
	
	// event handler initializations
	Wire.onReceive(dataRcv);    // register an event handler for received data
	Wire.onRequest(dataRqst);   // register an event handler for data requests
	
	// initialize global variables
	i2c_rcv = 255;
	time_start = millis();
	stat_LED = 0;
	
	pinMode(13, OUTPUT);        // set pin 13 mode to output
}

void loop()
{

	value_pot = analogRead(A0); // read analog value at pin A0 (potentiometer voltage)
	
	// blink logic code
        if((millis() - time_start) > (1000 * (float)(i2c_rcv/255))) {
		stat_LED = !stat_LED;
		time_start = millis();
	}
	digitalWrite(13, stat_LED);
}

//received data handler function
void dataRcv(int numBytes)
{
	while(Wire.available()) {	// read all bytes received
		i2c_rcv = Wire.read();
	}
}

// requests data handler function
void dataRqst()
{
	Wire.write(value_pot); // send potentiometer position
}

Explanation of the Code

For the slave device, there is a slight difference in coding I2C communication. The first difference is with Wire.begin(address). For slave devices, the address is a requirement. For our project, the address for the slave device will be 0x08. It can be any address you want, but make sure it is unique in the I2C network. Some I2C slave devices also have their I2C addresses defined, so check the datasheet first.

We will join the I2C network as a slave device by adding the code Wire.begin(0x08);inside setup().

Event Handlers

The next task is to add event handlers to our code to manage the data received from other devices in the I2C network. Event handlers are pieces of code that manage events that our device will likely encounter while running.

Wire.onReceive()

In the setup() part of the sketch, we add the function Wire.onReceive(handler) to register a function (the handler) that will manage the data received. we’ll call our handler function dataRcv(). Take note that the function name can be anything you want. In the sketch above, Wire.onReceive(dataRcv); in called in the setup() section. At the end of the sketch is the code for the handler function. It is initialized as void dataRcv(int numBytes). The parameter int numBytes contains the number of bytes of data received.

Wire.onRequest()

The next event handler that we will use is Wire.onRequest(handler). This function is used on slave devices and works similarly to Wire.onReceive(). The only difference is that it handles data requests events. Data requests come from master devices.

In setup(), we add the code Wire.onRequest(dataRqst);. And at the end of our sketch, we add the function void dataRqst(). Note that Wire.onRequest() handlers do not accept any parameters. The function dataRqst() only contains Wire.write(). We don’t need Wire.beginTransmission() and Wire.endTransmission() because the Wire library already handles the responses from the slave devices.

Testing our Arduino I2C Communication Project

Here comes the most exciting part – power-up and testing!

Using the Arduino IDE, upload the master Arduino sketch to one of the Arduinos. Then upload the slave Arduino sketch to the other Arduino.

Operation

  • Adjust the potentiometer on the master device to control the blink rate of the slave device LED.
  • Adjust the potentiometer on the slave device to control the blink rate of the master device LED.

Our code takes the master’s potentiometer position and sends it over to the slave device over I2C. The slave device then uses the received value to adjust the blink delay time of the LED. The same thing happens with the slave’s potentiometer position.