Communicating With I2C Devices in Your Arduino Project

Communicating With I2C Devices in Your Arduino Project

Often with an Arduino project, you will need to communicate with many devices and sensors. Many protocols were invented to limit the inputs required to communicate with a device and make the communication more standard so you don’t have to figure out a proprietary format each time.

One very popular protocol to do with is the I2C (Inter-Integrated Circuit) protocol. It only takes two pins to communicate with a I2C device: one pin is for data (SDA) and the other is for the clock (SCL). Each device has a specific address, so many devices can be added to the same bus without using up extra pins. The logic level of the communication (generally 3.3V or 5V) depends on the master, so each slave device must be able to work at that level.

Everything is driven by the master node, which generates the clock and manages the communication with all the slave devices. There are theoretically 127 addresses available for slave devices, but you can realistically connect around 6-10 devices. BlueDot has an excellent explanation of what really limits the number of devices at https://www.bluedot.space/tutorials/how-many-devices-can-you-connect-on-i2c-bus/.

If you purchase a sensor and it supports I2C, it will already have a default slave address (the master doesn’t have one.). Since each address must be unique, if you want to use more than one of the same device on a bus you must make sure that it can be changed by a jumper (or another similar option). You also need to be careful that the devices you want to use don’t communicate using the same address.

Adafruit maintains a list of slave addresses for the most popular devices at https://learn.adafruit.com/i2c-addresses/the-list. You can also generally find it in the documentation for the device, and they often already have an Arduino library that abstracts the messy communication part including the default address to use.

I2C Communication: The Hardware

For this tutorial, I’m going to make two Arduino communicate together to show how to program both sides of the communication. This would not be possible with a slave device that already has a programming and an assigned address. The slave is going to read the value of a potentiometer on the analog pin when the value is requested by the master.

Please note that the reverse is possible: a master can push data to a specific slave so it reacts to this data.

For the Arduinos, the wiring is pretty simple: just wire the SDA, SDL and GND pins of both microcontrollers together. The I2C bus requires pullup resistors, but they are included inside the Atmega chip of the Arduino and will be turned on automatically by the Wire Arduino library that we will use so you don’t need to add any.

You also need to write the potentiometer to the A0 analog pin of the slave Arduino so you can read its value.

Here is the complete wiring diagram (the potentiometer I used has a maximum value of 1k ohms):

I2C Communication: The Software

First, we’re going to focus on the slave (the Arduino to the right on the schematic). to get started, we need to initialize the I2C communication by specifying the slave address our slave Arduino will have (any value from 0 to 127).

After this, we need to do the setup so the slave will answer when the master request data. The onRequest call in the setup() method specifies which method will be called, in that case the respondToMasterEvent method.

When data is requested, the respondToMasterEvent reads the current value of the analog pin, which can be modified by the potentiometer, and writes it to the buffer so the master can retrieve it. Since the IC2 protocols uses bytes that are 8-bit long, but the analog pin of the Arduino returns a 10 bits value, this value is split in two so it can be sent as 2 bytes.

#include <Wire.h>
// Slave address of this arduino on the bus.
const int SLAVE_ADDRESS = 0x10; 
// Analog pin A0 for the potentiometer that will act as an analog sensor in this example.
const int SENSOR_ANALOG_PIN = 14; 
// Variable for the current value of the analog pin (declared here to declare it only once).
int analogPinValue;
const int BYTES_TO_SEND = 2;
uint8_t sendBuffer[BYTES_TO_SEND];

void setup() {
  // Enable I2C on the Arduino, including pull-up resistors and join the bus as a slave.
  Wire.begin(SLAVE_ADDRESS);
  
  // Register event to respond to request from the master.
  Wire.onRequest(respondToMasterEvent); 
  
  // Enable serial for debug information.
  Serial.begin(115200);
}

// This function is called when the master requests data from the slave.
void respondToMasterEvent() {
  // The analog pin can return values from 0 to 1023 (10 bits).
  analogPinValue = analogRead(SENSOR_ANALOG_PIN);
  // Put the value in two uint8_t to send in 8bit format over I2C
  sendBuffer[0] = analogPinValue >> 8;
  sendBuffer[1] = analogPinValue & 0xff; 
  Wire.write(sendBuffer, BYTES_TO_SEND); 
  Serial.println(analogPinValue);
}

void loop() {
  // Receiving the I2C message has priority over the code in the loop (it works with 
  // interrupts), but nothing happens in the loop in this sketch. 
  delay(1000);
}

On the master side, we only need to enable the I2C communication. After this, each 5 seconds the master will request the current value of the slave using the slave address. On receiving the value, it will reassemble it from the 2 bytes it received in a single integer and send it to the serial port so you can see it on the Serial Monitor of the Arduino IDE.

#include <Wire.h>
// Address of the slave on the bus.
const int SLAVE_ADDRESS = 0x10; 
// Number of bytes to request from the slave
const int BYTES_TO_REQUEST = 2;
// Last value received from the slave
int slaveIncomingValue = 0;

void setup() {
  // Enable I2C on the Arduino, including pull-up resistors.
  Wire.begin();
  // Enable serial for debug information
  Serial.begin(115200);
}

void loop() {
  // Request 4 bytes from slave device (slave may send less)
  Wire.requestFrom(SLAVE_ADDRESS, BYTES_TO_REQUEST);    
  // Read the bytes on the bus, two at a time
  while (Wire.available() >= BYTES_TO_REQUEST) { 
    slaveIncomingValue = Wire.read() << 8 | Wire.read(); 
  }
  // Print the complete value received.
  Serial.println(slaveIncomingValue);   
   
  // Read again from the slave in 5 seconds.
  delay(5000);
}

That’s it: the master can now request data from the slave, and you can see for yourself that the value is updated by changing the value of the potentiometer.

If you wish to download the code example and try to for yourself, it’s available on my GitHub at https://github.com/CindyPotvin/I2C-Tutorial-Arduino

Please follow and like us:
error