I2C Tutorial for Arduino and ESP8266

I2C Tutorial for Arduino and ESP8266

In this tutorial we dive deeper into the I2C communication protocol.

You learn which practical examples:

  • which pins you need for Arduino and ESP8266 microcontroller to connect devices through I2C.
  • the advantages and disadvantages of this communication protocol
  • how to use a I2C multiplexer
LCD example

The I2C communication is one out of three possible communication protocols, the Arduino / ESP8266 is able to communicate with other devices like OLED displays, barometric pressure sensors and so on. The two other communication protocols are SPI and UART.

I2C stands for Inter-Integrated Circuit was invented 1982 by Philips Semiconductor, now NXP Semiconductors.
I2C has multiple features which are also compared in the following table:

  • Synchronous: The output of bits is synchronized to the sampling of bits by a clock signal shared between the master and the slave.
  • Multi-master: You can have multiple masters controlling single, or multiple slaves.
  • Multi-slave: You can connect multiple salves to a single master like SPI.
  • Packet switched: The transferred data is grouped in packages / messages, made of a header and a payload.
  • Single-ended: The data is transferred by a single wire.
  • Serial connection: Data is transferred bit by bit along a single wire

I2C reference design with timing diagram

On your Arduino / ESP8266 you will find two GPIOs (SDA and SCL) for the I2C communication. If you are not sure were to find the corresponding pins, see the following pictures or for the complete pinout you can vitis the following articles:

ESP8266 (NodeMCU)

SDA: D2 (I2C -> Data)
SCL: D1 (I2C -> Clock)

Arduino Nano

SDA: A4
SCL: A5

Arduino Uno

SDA: PIN18
SCL: PIN19
(no label on the PCB front, only visible from the side)

Arduino Mega

SDA: PIN20
SCL: PIN21
(no label on the PCB front, only visible from the side)

The two pins which you need for the I2C communication are the following:

  • SDA (Serial Data): Connection between master and slave to send and receive data.
  • SCL (Serial Clock): Shares the clock signal between the master and the slave, where the master always controls the clock signal.
I2C overview

The Serial Data line and the Serial Clock line are pulled up with resistors. Therefore when there is no transmission of data on the bus, the SDA and SCL are in the HIGH state. Why a resistor is needed, see subsection “Physical layer”. Typical voltages are +5V and +3.3V and devices can communicate at 100 kHz or 400 kHz.
All I2C devices are connected to the bus either with open collector or open drain pins to pull the line LOW. The communication between master and slave occurs by toggling the lines by pulling LOW and releasing HIGH. Also bits are clocked on falling clock edges.

There may be four potential modes of operation for a given bus device, although most devices only use a single role and its two modes:

  • master transmit – master node is sending data to a slave,
  • master receive – master node is receiving data from a slave,
  • slave transmit – slave node is sending data to the master,
  • slave receive – slave node is receiving data from the master.

I2C Message Protocol

The I2C message protocol is divided bit by bit into fixed sections. In the following part of the article, we will take a closer look at the six different sections of the protocol.

I2C start stop condition
  1. Start condition: SDA: HIGH → LOW while SCL is HIGH
  2. Stop condition: SDA: LOW → HIGH while SCL is HIGH

The start and the stop condition are the only two times in the whole I2C communication, where the SDA line changes when the SCL line is HIGH. In all other conditions the SDA line only changes state when the SCL line is LOW.

  1. Address Frame: The address frame is a 7 or 10 bit sequence to identify each slave for the master. The identifier is unique over all slaves. Each slave compares the address sent from the master to its own address. If the address matches, it sends a ACK bit → 0 back to the master. If the address doesn’t match, the slave does nothing and the SDA line remains high.
  2. Read/Write Bit
    • Write: Master is sending data to the slave: 0
    • Read: Master is requesting data from the slave: 1
  3. ACK/NACK Bit. If an address frame or data frame was successfully received, an ACK bit → 0 is returned to the sender from the receiving device.
  4. Data Frame: After the master detects the ACK bit from the slave, the first data frame is ready to be sent. The data frame is always 8 bits long and is followed by an ACK/NACK bit to verify that the frame has been received successfully. The master will continue generating clock pulses at a regular interval.

Because after every Data Frame a ACK /NACK bit is transferred, the I2C communicate protocol has a data overhead.

I2C Write Communication

I2C write

I2C Read Communication

I2C read

Repeated Start Conditions

I2C repeated start

It is possible for the master to exchange several messages in one go, without allowing an other master device take control over the bus. Therefore a repeated start condition is defines as the following:

  1. SCL 0 → 0
  2. SDA 0 → 1
  3. SCL 0 → 1
  4. SDA 1 → 0

Note that there was not Stop condition.

Clock stretching

In some cases the masters data rate will be exceed the slaves ability to provide the requested data. Therefore the slave can hold down the SCL line after the master releases the SCL line. The master will wait for the click line to be released by the slave before proceeding to the next frame.

Physical Layer

Unlike UART or SPI connections, the I2C bus drivers are “open drain”, meaning that they can pull the corresponding signal line low, but cannot drive it high. Therefore, there can be no communication where one device is trying to drive the line high while another tries to pull it low. This architecture avoids errors in the communication.

But how is it possible to pull the signal line high? Each signal line has a pull-up resistor to restore the signal to high when no device is asserting the line to low.
A rule of thumb picking a resistor is 4.7 kΩ. The more devices are connected to the I2C communication the smaller has to be the resistor.

I2C allows for some flexibility in connecting devices with different I/O voltages. For an Arduino board with a voltage level of 5V as master, a slave of 3.3V will be work without problems. But if the voltage of the slave will be lower than 3.3V for example 2.5V there is the possibility to buy a I2C level shifter board.

I2C Speed Modes

  • Low speed mode: 10 kbit/s
  • Standard mode: 100 kbit/s
  • Fast mode: 400 kbit/s
  • Fast mode plus: 1 Mbit/s
  • High speed mode: 3.4 Mbit/s
  • Ultra fast mode: 5 Mbit/s

Advantages and Disadvantages of I2C communication

Advantages

  • Only uses two wires
  • Supports multiple masters and multiple slaves
  • Well known and widely used protocol

Disadvantages

  • Slower data transfer rate than SPI
  • The size of the data frame is limited to 8 bits

I2C Examples

I use the following parts for this tutorial

  • A microcontroller of your choice (Arduino, ESP8266, ESP32)
  • 16×2 or 20×4 LCD Display with I2C

You find all links to the parts on the Components and Parts page

In the next section, we leave the theory behind us and attend to practical examples. Before we can control an I2C device, we first have to find out its HEX address. Therefore, our first example will be an I2C HEX address scanner. After we found out the HEX address of the I2C LCD display, we will control the display accordingly to send messages from the Arduino or NodeMCU via I2C to the LCD display.

The following pictures show the connection between Arduino Uno on the left side and NodeMCU on the right side with the I2C LCD display.

Arduino Uno

GND
5V
PIN18
PIN19

I2C LCD Display

GND
VCC
SDA
SLC

NodeMCU

GND
VIN
D2
D1

I2C LCD Display

GND
VCC
SDA
SLC

How to find the HEX Address?

#include "Wire.h"

The “Wire.h” library allows the microcontroller to communicate with I2C devices. Therefore this library is essential every time you want to use the I2C communication.

void setup(){
  Serial.begin(115200); 
  while(!Serial){} // Waiting for serial connection
 
  Serial.println();
  Serial.println("Start I2C scanner ...");
  Serial.print("\r\n");
  byte count = 0;

This sketch uses only the setup function, because we want only one time to scan all connected devices. First we define the baud rate to 115200 and we will memorize to set the baud rate of the serial monitor to the same value. Than we wait until the serial connection is established that we are able to scan devices. After we define some cool printings on the serial monitor, we define a variable count to zero. The variable will be increased when we find an I2C device and is therefore the sum of connected I2C devices.

  Wire.begin();
  for (byte i = 8; i < 120; i++)
  {
    Wire.beginTransmission(i);
    if (Wire.endTransmission() == 0)
      {
      Serial.print("Found I2C Device: ");
      Serial.print(" (0x");
      Serial.print(i, HEX);
      Serial.println(")");
      count++;
      delay(1);
      }
  }

With Wire.begin() the microcontroller joins the I2C bus as master or slave. If no address is provided in the function like Wire.begin(address), the device joins as master like we want. To scan all possible I2C HEX addresses we use a for loop. To begin the transmission to the possible I2C slave we use the Wire.beginTransmission(address) function. If there is a valid I2C slave we get a 0 by ending the transmission to the slave through Wire.endTransmission(). We print the HEX address of the connected device on the serial monitor. Also if we found an I2C device we increase the counter by 1 and use a little delay before trying to connect to the next device.

  Serial.print("\r\n");
  Serial.println("Finish I2C scanner");
  Serial.print("Found ");
  Serial.print(count, HEX);
  Serial.println(" Device(s).");
}

void loop() {}
At the end of the script we print the total number of found I2C devices. We do not use the loop function.

Feel free to copy the whole script in your Arduino IDE

#include "Wire.h"

void setup(){
  Serial.begin(115200); 
  while(!Serial){} // Waiting for serial connection
 
  Serial.println();
  Serial.println("Start I2C scanner ...");
  Serial.print("\r\n");
  byte count = 0;
  
  Wire.begin();
  for (byte i = 8; i < 120; i++)
  {
    Wire.beginTransmission(i);
    if (Wire.endTransmission() == 0)
      {
      Serial.print("Found I2C Device: ");
      Serial.print(" (0x");
      Serial.print(i, HEX);
      Serial.println(")");
      count++;
      delay(1);
      }
  }
  Serial.print("\r\n");
  Serial.println("Finish I2C scanner");
  Serial.print("Found ");
  Serial.print(count, HEX);
  Serial.println(" Device(s).");
}

void loop() {}

If we look at the serial monitor, the result is the following

I2C Scanner Serial Output
We found the connected I2C LCD display with the HEX address 0x27. We will need this address for our next example.

Control an LCD Display

With the I2C HEX scanner we found out that the HEX address of the LCD display is 0x27. Now we can program the script to communicate with the LCD display. For this sketch we need the LiquidCrystal_I2C library. There is also a separate article about the LCD display with deeper technical informations.

First we have to include the Wire library we know from the I2C HEX scanner code and the new library LiquidCrystal_I2C which takes the huge work to provide us a easy to use interface for the LCD display.
We have to define the HEX address of our display and what display we are using like 16×2 or 20×4. I use the 20×4 display in this example.

In the setup function we use lcd.init() to initialize the connection between the microcontroller and the display. After the initialization we have to turn the backlight on with lcd.backlight().

After the display is setup we can set the cursor to the position (column=1, row=0) and write our first part of the text. Then we set the cursor in the next row and write the rest of the string.

#include "Wire.h"
#include "LiquidCrystal_I2C.h"

// set the LCD address to 0x27 for a 16 chars and 2 line display
LiquidCrystal_I2C lcd(0x27, 20, 4); 

void setup()
{
  lcd.init();

  lcd.backlight();
  lcd.setCursor(1, 0);
  lcd.print("This is");
  lcd.setCursor(1, 1);
  lcd.print("DIYI0T.com");
}

void loop(){}
LCD example

I2C Multiplexer

If you want to connect a lot of sensors to your Arduino and your normal I2C connection comes to a limit? Maybe the sensors have the same I2C address. The solution to your problem is the PCF8574 1-to-8 I2C expander. The PCF8574 Breakout enables communication with multiple I2C devices that have the same address making it simple to interface with them. PCF8574 is an eight-channel I2C expander which allows eight separate I2C devices to be controlled by a single host I2C bus.

The PCF8574 is a 8-bit input/output (I/O) expander for the two-line bidirectional bus (I2C) and designed for operation voltages between 2.5V and 6V. The standby current consumption is very low with 10μA.

The PCF8574 is connected to the Arduino as follows:

VCC -> 5V
GND -> GND
SDA -> PIN18
SCL -> PIN19

P0…P7 are the P-port input/output with push-pull design structure. Here you connect your I2C devices to the PCF8574. We connected the LED on P0.

A0…A2 are address inputs for the PCF8574 itself. We do not need them and therefore connect them with GND.

Click here, if you want to look at the complete datasheet of the PCF8574.

PCF8574

The program code for the scanner and the control of the device is the same as above.

6 Responses

Leave A Comment