Communication Between Arduinos Using I²C

There are many great resources for describing what I²C is, and how to read data from an I²C device with the Arduino. However, I couldn’t find any suitable page for how to use I²C for communication between two Arduinos. This is for version 1.0.3.

Hardware

The hardware setup is the same as any other I²C setup. Connect each Arduino’s A4 inputs to each other, and connect the A5 inputs together. They will need a pull-up resistor, however the Wire library enables the internal pull-up resistors when begin() is called.

This may be adequate for your uses, but as is discussed in Effects of Varying I²C Pull-Up Resistors, it’s a case of YMMV. If internal pull-up resistors do cause a problem, then you’ll have to comment out the following lines from the Wire/utility/twi.c soruce file.

// activate internal pullups for twi.
digitalWrite(SDA, 1);
digitalWrite(SCL, 1);

Software Overview

The structure of the master code and the slave code are very different, but neither is complicated. Because I²C does not define a data protocol, you’ll have to roll your own. It’s common to use a model where devices are manipulated through writing to registers, and reading values back.

The Master

The master’s code is synchronous, and wraps its communication between beginTransmission() and endTransmission() calls. It sends with the write() methods, and receives with the requestFrom() method.

Setup

Setup requires only one command, begin().

void setup()
{
  // Start I²C bus as master
  Wire.begin();
}

Sending Data to a Slave

Sending data to a slave is done either by individual bytes, or in bulk. Here is a somewhat contrived example that sends the value of an analogue input pin to a slave every second. It writes bytes one at a time rather than using a buffer.

void loop()
{
  int input = analogRead(AnalogueInputPin);

  // Send two bytes to slave.
  Wire.beginTransmission(SlaveDeviceId);
  Wire.write(input >> 8);
  Wire.write(input & 255);
  Wire.endTransmission();

  delay(1000);
}

Receiving Data from a Slave

Receiving data from a slave is done either by individual bytes, or in bulk. Here is another contrived example that receives the value in the prior section.

void loop()
{
  // Request data from slave.
  Wire.beginTransmission(SlaveDeviceId);
  int available = Wire.requestFrom(SlaveDeviceId, (uint8_t)2);

  if(available == 2)
  {
    int receivedValue = Wire.read() << 8 | Wire.read();
    Serial.println(receivedValue);
  }
  else
  {
    Serial.print("Unexpected number of bytes received: ");
    Serial.println(available);
  }

  Wire.endTransmission();

  delay(1000);
}

The Slave

The slave controller uses asynchronous callback functions for its communication. Slave mode is strictly reactionary — it receives or sends data only at the best of the master. The callbacks are set in the setup() function, the loop() function is empty, and everything happens in the callbacks.

Setup

Setup requires two commands. The first is to call begin() with the slave’s ID number.

const byte SlaveDeviceId = 1;

void setup()
{
  // Start I²C bus as a slave
  Wire.begin(SlaveDeviceId);
...

The second is to set up the callbacks. Details are below.

Receiving Data from the Master

Data coming in to the slave from the master is handled in a single callback of the form

void callbackName(int numberOfBytesBeingTransmitted)

This is registered in the setup() function by calling the onReceive() method.

#include

const byte SlaveDeviceId = 1;

void setup()
{
  // Start I²C bus as a slave
  Wire.begin(SlaveDeviceId);
  // Set the callback to call when data is received.
  Wire.onReceive(receiveCallback);

  // For demonstration purposes.
  Serial.begin(9600);
}

void loop()
{
}

// aCount is the number of bytes received.
void receiveCallback(int aCount)
{
  if(aCount == 2)
  {
    int receivedValue  = Wire.read() << 8;
    receivedValue |= Wire.read();
    Serial.println(receivedValue);
  }
  else
  {
    Serial.print("Unexpected number of bytes received: ");
    Serial.println(aCount);
  }
}

Sending Data to the Master

Sending data to the master is also done asynchronously using a callback. When a request for data is received from a master, a callback is invoked. The callback must compose its reply in a buffer, then send the buffer all at once with a single call to write(). Note that the Write class performs no buffering for outgoing transmissions; any call to write() transmits the entire reply immediately.

Data coming in to the slave from the master is handled in a single callback of the form

void callbackName()

This is registered in the setup() function by calling the onRequest() method.

#include

const byte AnalogueInputPin = 0;
const byte SlaveDeviceId = 1;

void setup()
{
  // Start I²C bus as a slave
  Wire.begin(SlaveDeviceId);
  // Set the callback to call when data is requested.
  Wire.onRequest(requestCallback);
}

void loop()
{
}

void requestCallback()
{
  // Contrived example - transmit a value from an analogue pin.
  int input = analogRead(AnalogueInputPin);

  // To send multiple bytes from the slave,
  // you have to fill your own buffer and send it all at once.
  uint8_t buffer[2];
  buffer[0] = input >> 8;
  buffer[1] = input & 0xff;
  Wire.write(buffer, 2);
}

Sending and Receiving on the Same Slave

A single slave can both send and receive data by registering two callbacks. The appropriate callback is invoked automatically.

void setup()
{
  Wire.begin(SlaveDeviceId);
  Wire.onReceive(receiveCallback);
  Wire.onRequest(requestCallback);
}

“Gotchas”

The Wire library provides a convenient high-level wrapper for I²C. It has some oddities that will cause trouble if you’re not aware of them. Here are some gotchas.

Small Buffer

The Wire object‘s internal buffer is fixed at 32 bytes. You cannot send larger packets without modifying the library.

Asymmetric Behaviour

The write() method’s behaviour is different depending on whether the class is a master or a slave. See the sections on master and slave for details.

Type Coercion

The requestFrom() method’s overloads may confuse the compiler. If you get compiler errors, try coercing the parameters, for example:

const byte SlaveDeviceId = 1;
...
// Request data from slave.
Wire.beginTransmission(SlaveDeviceId);
int bytesReceived = Wire.requestFrom(SlaveDeviceId, (uint8_t)2);

No Clock Stretching

The current version of the library does not support clock stretching.

Collisions Between Multiple Masters

I’m still looking into this more, but it appears that there is no mechanism for a master to wait for for a busy bus to clear. The transmission simply fails.

Code

I put some some demonstration code on GitHub.

Updates

16 Feb 2013
Update pull-up resistor information. Update “gotchas” section.
This entry was posted in Micro & Hardware and tagged , , , , , , , , . Bookmark the permalink.

4 Responses to Communication Between Arduinos Using I²C

  1. Ben says:

    Thanks mate,
    I think this is the best explanation of the Wire library I’ve come across. I didn’t know you needed a buffer, when writing multiple bytes from the slave to the master. Now my code is finally working! Cheers, Ben

  2. Ignas says:

    Thanks for elegant way sending integers!
    Maybe You can add some info how to ask from slave (requestFrom ?) specific value from multiple sensors. Lets say, slave have three sensors (X, Y & Z) and three return functions for each sensor. If slave gets request from master it just have to do one function. It have no info what data it is requested, as no additional info is passed. I think that first master has to transmit some value (command number) to slave. Than slave has to respond accordingly. But how slave transmits to master after master’s request is a little bit tricky part for me.
    Thank You.

  3. Ignas says:

    Thank You one more time. Sorry for bothering again. I figured it out. Here is my simple Arduino I2C protocol for multiple slave functions http://www.berryjam.eu/2014/07/advanced-arduino-i2c-communication/

  4. I’m glad you figured it out, Ignas. Thanks for sharing your solution.

Leave a Reply