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 used 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:

As you can see in the diagram above, the advantage of using I2C is that you only need two wires to communicate with multiple devices. All data passes through the two wires to and from the master and slave devices. Since the Arduino has a limited number of input/output pins, I2C can allow you to connect more devices.

Many Arduino sensors and modules are enabled for I2C communication.

BONUS: I made a quick start guide for this tutorial that you can download and go back to later if you can’t set this up right now. It covers all of the steps, diagrams, and code you need to get started.

The I2C Network

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

Slave Devices

Each slave device has an I2C address that is used to identify the device. 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

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

Logic Levels

The Arduino outputs I2C signals at a 5V logic level. But I2C devices can operate at a range of different logic level voltages. 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.

To learn more about the details of I2C communication, check out our article on the 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. This project will read the position of a potentiometer connected to a master Arduino, send the information over I2C, and change the blink rate of the LED on the slave Arduino.

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:

Figure 2: Arduino I2C pin locations

Hardware Components

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

Wiring Diagram

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

We don’t need pull-up resistors on the SDA and SCL lines, because they’re built into the Arduino’s I2C pins already.

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.

Open the Arduino IDE and upload the code below to the master Arduino:

// Arduino master sketch #include <Wire.h> byte i2c_rcv; // data received from I2C bus unsigned long time_start; // start time in milliseconds int stat_LED; // status of LED: 1 = ON, 0 = OFF byte value_pot; // potentiometer position void setup(){ Wire.begin(); // join I2C bus as the master // initialize global variables i2c_rcv = 255; time_start = millis(); stat_LED = 0; pinMode(13, OUTPUT); // set pin 13 as an output } void loop(){ // read potentiometer position value_pot = analogRead(A0); // read the voltage 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

, , and at the top of our sketch Initialize values of the global variables inside the setup() section

section Initialize pin 13 of the Arduino as an output pin using pinMode()

Add the blink logic code inside the loop()

The Wire Library

To use the Arduino’s built-in I2C interface, we will use the Wire library. This library is included with the Arduino IDE, so there’s no need to install it.

The Wire library has ready-made I2C functions to make the programming easier for us. To use the functions in the Wire library, we first need to add it to our sketch. In the sketch above, we do that with #include <Wire.h> .

After including the library, the next 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 the setup() function.

Now in the loop() section, the code will make the Arduino read the potentiometer value connected to pin A0 , and save that value in the variable value_pot .

Sending Data Over I2C

After saving the value from pin A0 in 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:

It informs the bus that we will be sending data It notifies the slave to prepare to receive the data

Wire.write()

Then we send the value stored in the value_pot variable using the function Wire.write(value) . The value argument is the variable that stores the data you want to send.

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 with the function Wire.endTransmission() .

Receiving Data Over I2C

The master Arduino also needs to receive the potentiometer position from the slave Arduino. To receive data over I2C, we use the following three functions:

Wire.requestFrom()

Wire.available()

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. So 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 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 the code. 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 the setup() section.

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 request events. Data requests come from master devices. So in the setup() section we add the code Wire.onRequest(dataRqst); .

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 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.