Use I2C for communication between Arduinos

INTRODUCTION
In the few years I have been contributing to the Forum I don't recall any advice about using I2C (rather than Serial) for communication between two Arduinos. I had been assuming that I2C was only suitable for very short range communication of the order of 20cm to 30cm but I recently came across the Megapoints model railway control system which uses I2C over distances of 2 metres or more. Having taken the trouble to learn a little bit about I2C is seems to be easier to use than Serial and is especially useful for communicating between two Unos or nanos when you want to keep the single HardwareSerial port free.

If you are interested Nick Gammon has a lot of technical info about I2C on his website. This Tutorial is just intended to provide some simple example programs for communication between two Arduinos.

I2C SENDS PACKETS OF DATA
I2C is different from Serial because it sends messages as packets with a defined start and end. This makes it easy to send binary data as well as text. With Serial communication you have to provide the start- and end-markers (see for example the 3rd example in Serial Input Basics) which is easy when sending text but not so easy when sending binary data.

Using the Arduino Wire library I2C is limited to sending packets of not more than 32 bytes. I suspect that is sufficient for many situations. Longer messages can obviously be broken down into a series of 32 byte messages but I do not propose to cover that here.

I2C USES TWO WIRES
I2C operates using two wires - one for data and one for the clock. (I2C is also know as TWI - Two Wire Interface). The purpose of the clock is to signal to the slave when it should sample the data to get a valid value. With Serial there is only one wire for each direction and the sampling of bits is based on time set by the baud rate. (Note that both I2C and Serial also need a GND connection). Because it has only one data line (unlike Serial which has Rx and Tx lines) I2C can only operate in one direction at a time. Of course messages only take a few millisecs so it is easy to give the impression of two-way communication.

MODES OF OPERATION
The Wire library has, basically, two modes of operation. In the first case Arduino A can send a message to Arduino B with A acting as master and B acting as slave. Later the roles can be reversed and B (as master) can send a message to A (as slave). This is the basis for my first two examples.

The other mode is when the master requests data from the slave and this is used in my third example. In all cases the clock signal is provided by the master so, when the slave is requested to provide data it must be ready to do so immediately while the master is supplying the clock signal. Also the maximum number of bytes to be transferred is determined by the master.

I2C IS A BUS AND DEVICES HAVE ADDRESSES
I2C is a bus system which means that several devices can be connected at the same time and each device is identified by its address. (This is another difference from Serial which is a one-to-one system.) For the Wire library the addresses are a single byte in the range 8 to 119 allowing for 112 different devices. (The other values are reserved for special purposes which I will not cover here.) My example program has only been tested with two Arduinos but could probably be extended to several. My programs can also be used in parallel with other devices (such as sensors) connected to the I2C bus. Just ensure that every device has a separate address. The Wire library waits until the bus is free before starting a transmission so data collisions are avoided unless two devices start transmitting at precisely the same microsecond - which is unlikely. (Note that data collisions are not a problem with Serial as it is a one-to-one system with separate Rx and Tx wires).

I2C IS INTERRUPT DRIVEN
Unless it is actually sending a message the I2C system is always listening for a new message. When a message arrives the I2C systems makes a call to a function that you will have included in your program. The code in that function should be as short as possible to allow the function to complete quickly so that the interrupt can be completed and normal processing resumed. There should be no Print statements in the function. In the examples I have named the functions receiveEvent() and requestEvent(), however you can use any names you like for these functions.

THE EXAMPLE PROGRAMS
I have included three example programs. In the first pair the master only sends data to the slave. In the second example the master and slave can swap roles to provide two-way communication. In the third pair the master sends data to the slave and also requests data from the slave. This is also two-way communication but strictly controlled by the master.

Continues in next Post

1 Like

THE FIRST EXAMPLE - master sending to slave

The general principles can be understood from this example.

I have created a struct to hold the data that is to be sent. This is a convenient way to allow for any type of data in any combination as long as the total size does not exceed 32 bytes. In the example I have padded out the struct to exactly 32 bytes to make the point as clear as possible, but any size not exceeding 32 bytes can be sent. Note that the struct will be perfectly happy containing a single variable if that’s all you need.

The address that this Arduino is listening on is identified in setup() with the line

Wire.begin(thisAddress);

where the variable thisAddress holds the number for the address.

Sending a message is achieved with these three lines

Wire.beginTransmission(otherAddress);
Wire.write((byte*) &sendData, sizeof(sendData));
Wire.endTransmission();

The first line identifies the device for which the message is intended. The second line identifies the data to be sent and the last line actually sends the message

The way I have written the program it updates the data in the struct at regular intervals and sets the variable newSendData to true to signal that there is something to be transmitted. You can choose any other basis for updating the struct.

For the slave the function to be called when an I2C message arrives is established with the line

Wire.onReceive(receiveEvent);

and the data is copied to the receiving struct with this line

Wire.readBytes( (byte*) &rxData, numBytesReceived);

For reasons that I hope are obvious the program receiving the message must save it to a struct that is identical to the struct of the sending message.

Note that Wire.readbytes() works although it is not mentioned in the Wire library documentation.

Note, also, how the use of a struct eliminates the need to parse the incoming data - the data automatically gets put in the right places in the struct.

Code for Master to Slave communication
Master

//===================
// Using I2C to send and receive structs between two Arduinos
//   SDA is the data connection and SCL is the clock connection
//   On an Uno  SDA is A4 and SCL is A5
//   On an Mega SDA is 20 and SCL is 21
//   GNDs must also be connected
//===================


        // data to be sent
struct I2cTxStruct {
    char textA[16];         // 16 bytes
    int valA;               //  2
    unsigned long valB;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

I2cTxStruct txData = {"xxx", 236, 0};

bool newTxData = false;


        // I2C control stuff
#include <Wire.h>

const byte thisAddress = 8; // these need to be swapped for the other Arduino
const byte otherAddress = 9;


        // timing variables
unsigned long prevUpdateTime = 0;
unsigned long updateInterval = 500;

//=================================

void setup() {
    Serial.begin(115200);
    Serial.println("\nStarting I2C Master demo\n");

        // set up I2C
    Wire.begin(thisAddress); // join i2c bus

}

//============

void loop() {

        // this function updates the data in txData
    updateDataToSend();
        // this function sends the data if one is ready to be sent
    transmitData();
}

//============

void updateDataToSend() {

    if (millis() - prevUpdateTime >= updateInterval) {
        prevUpdateTime = millis();
        if (newTxData == false) { // ensure previous message has been sent

            char sText[] = "SendA";
            strcpy(txData.textA, sText);
            txData.valA += 10;
            if (txData.valA > 300) {
                txData.valA = 236;
            }
            txData.valB = millis();
            newTxData = true;
        }
    }
}

//============

void transmitData() {

    if (newTxData == true) {
        Wire.beginTransmission(otherAddress);
        Wire.write((byte*) &txData, sizeof(txData));
        Wire.endTransmission();    // this is what actually sends the data

            // for demo show the data that as been sent
        Serial.print("Sent ");
        Serial.print(txData.textA);
        Serial.print(' ');
        Serial.print(txData.valA);
        Serial.print(' ');
        Serial.println(txData.valB);

        newTxData = false;
    }
}

Slave

//===================
// Using I2C to send and receive structs between two Arduinos
//   SDA is the data connection and SCL is the clock connection
//   On an Uno  SDA is A4 and SCL is A5
//   On an Mega SDA is 20 and SCL is 21
//   GNDs must also be connected
//===================


        // data to be received

struct I2cRxStruct {
    char textB[16];         // 16 bytes
    int valC;               //  2
    unsigned long valD;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

I2cRxStruct rxData;

bool newRxData = false;


        // I2C control stuff
#include <Wire.h>

const byte thisAddress = 9; // these need to be swapped for the other Arduino
const byte otherAddress = 8;



//=================================

void setup() {
    Serial.begin(115200);
    Serial.println("\nStarting I2C Slave demo\n");

    // set up I2C
    Wire.begin(thisAddress); // join i2c bus
    Wire.onReceive(receiveEvent); // register event
}

//============

void loop() {

        // this bit checks if a message has been received
    if (newRxData == true) {
        showNewData();
        newRxData = false;
    }
}


//=============

void showNewData() {

    Serial.print("This just in    ");
    Serial.print(rxData.textB);
    Serial.print(' ');
    Serial.print(rxData.valC);
    Serial.print(' ');
    Serial.println(rxData.valD);
}

//============

        // this function is called by the Wire library when a message is received
void receiveEvent(int numBytesReceived) {

    if (newRxData == false) {
            // copy the data to rxData
        Wire.readBytes( (byte*) &rxData, numBytesReceived);
        newRxData = true;
    }
    else {
            // dump the data
        while(Wire.available() > 0) {
            byte c = Wire.read();
        }
    }
}

Continues in next Post

THE SECOND EXAMPLE - two-way communication by swapping roles

The second example allows both Arduinos to swap roles so that at one time A is the master and B is the slave and at other times B is the master and A is the slave. In effect both programs contain the combined code from the master and slave in the first example. The only difference between the programs is that the addresses be swapped.

For convenience I have used the same struct for sending data in both directions but they could be very different. Just ensure that the struct which receives data is the same as the struct that sends the data.

Code for two-way commincation by swapping roles.
Use the same code for both Arduinos - just swap the addresses in one of them

//===================
// Using I2C to send and receive structs between two Arduinos
//   SDA is the data connection and SCL is the clock connection
//   On an Uno  SDA is A4 and SCL is A5
//   On an Mega SDA is 20 and SCL is 21
//   GNDs must also be connected
//===================


        // data to be sent and received
struct I2cTxStruct {
    char textA[16];         // 16 bytes
    int valA;               //  2
    unsigned long valB;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

struct I2cRxStruct {
    char textB[16];         // 16 bytes
    int valC;               //  2
    unsigned long valD;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

I2cTxStruct txData = {"xxx", 236, 0};
I2cRxStruct rxData;

bool newTxData = false;
bool newRxData = false;


        // I2C control stuff
#include <Wire.h>

const byte thisAddress = 8; // these need to be swapped for the other Arduino
const byte otherAddress = 9;


        // timing variables
unsigned long prevUpdateTime = 0;
unsigned long updateInterval = 500;

//=================================

void setup() {
    Serial.begin(115200);
    Serial.println("\nStarting I2C SwapRoles demo\n");

        // set up I2C
    Wire.begin(thisAddress); // join i2c bus
    Wire.onReceive(receiveEvent); // register function to be called when a message arrives

}

//============

void loop() {

        // this bit checks if a message has been received
    if (newRxData == true) {
        showNewData();
        newRxData = false;
    }


        // this function updates the data in txData
    updateDataToSend();
        // this function sends the data if one is ready to be sent
    transmitData();
}

//============

void updateDataToSend() {

    if (millis() - prevUpdateTime >= updateInterval) {
        prevUpdateTime = millis();
        if (newTxData == false) { // ensure previous message has been sent

            char sText[] = "SendA";
            strcpy(txData.textA, sText);
            txData.valA += 10;
            if (txData.valA > 300) {
                txData.valA = 236;
            }
            txData.valB = millis();
            newTxData = true;
        }
    }
}

//============

void transmitData() {

    if (newTxData == true) {
        Wire.beginTransmission(otherAddress);
        Wire.write((byte*) &txData, sizeof(txData));
        Wire.endTransmission();    // this is what actually sends the data

            // for demo show the data that as been sent
        Serial.print("Sent ");
        Serial.print(txData.textA);
        Serial.print(' ');
        Serial.print(txData.valA);
        Serial.print(' ');
        Serial.println(txData.valB);

        newTxData = false;
    }
}

//=============

void showNewData() {

    Serial.print("This just in    ");
    Serial.print(rxData.textB);
    Serial.print(' ');
    Serial.print(rxData.valC);
    Serial.print(' ');
    Serial.println(rxData.valD);
}

//============

        // this function is called by the Wire library when a message is received
void receiveEvent(int numBytesReceived) {

    if (newRxData == false) {
            // copy the data to rxData
        Wire.readBytes( (byte*) &rxData, numBytesReceived);
        newRxData = true;
    }
    else {
            // dump the data
        while(Wire.available() > 0) {
            byte c = Wire.read();
        }
    }
}

Continues in next Post

THE THIRD EXAMPLE - master requests data from slave

The third example is somewhat different. In it the master sends data to the slave (as in the first example) but it also requests data from the slave. When the slave detects a request it calls the requestEvent() function and must immediately send data back to the master. Generally speaking that means that the data for the response must be in the struct before the request arrives. In the example the data for the response is updated immediately after the response is sent. That way it is ready in plenty of time for the next request.

Note also that the way the master deals with the response is different. The data in the response is not picked up by a receiveEvent() function, it is simply read immediately after the request is made.

Code for Master requesting data from Slave
Master

//===================
// Using I2C to send and receive structs between two Arduinos
//   SDA is the data connection and SCL is the clock connection
//   On an Uno  SDA is A4 and SCL is A5
//   On an Mega SDA is 20 and SCL is 21
//   GNDs must also be connected
//===================


        // data to be sent and received
struct I2cTxStruct {
    char textA[16];         // 16 bytes
    int valA;               //  2
    unsigned long valB;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

struct I2cRxStruct {
    char textB[16];         // 16 bytes
    int valC;               //  2
    unsigned long valD;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

I2cTxStruct txData = {"xxx", 236, 0};
I2cRxStruct rxData;

bool newTxData = false;
bool newRxData = false;
bool rqData = false;


        // I2C control stuff
#include <Wire.h>

const byte thisAddress = 8; // these need to be swapped for the other Arduino
const byte otherAddress = 9;


        // timing variables
unsigned long prevUpdateTime = 0;
unsigned long updateInterval = 500;

//=================================

void setup() {
    Serial.begin(115200);
    Serial.println("\nStarting I2C MasterRequest demo\n");

        // set up I2C
    Wire.begin(thisAddress); // join i2c bus
    //~ Wire.onReceive(receiveEvent); // register function to be called when a message arrives

}

//============

void loop() {

        // this bit checks if a message has been received
    if (newRxData == true) {
        showNewData();
        newRxData = false;
    }


        // this function updates the data in txData
    updateDataToSend();
        // this function sends the data if one is ready to be sent
    transmitData();
    requestData();
}

//============

void updateDataToSend() {

    if (millis() - prevUpdateTime >= updateInterval) {
        prevUpdateTime = millis();
        if (newTxData == false) { // ensure previous message has been sent

            char sText[] = "SendA";
            strcpy(txData.textA, sText);
            txData.valA += 10;
            if (txData.valA > 300) {
                txData.valA = 236;
            }
            txData.valB = millis();
            newTxData = true;
        }
    }
}

//============

void transmitData() {

    if (newTxData == true) {
        Wire.beginTransmission(otherAddress);
        Wire.write((byte*) &txData, sizeof(txData));
        Wire.endTransmission();    // this is what actually sends the data

            // for demo show the data that as been sent
        Serial.print("Sent ");
        Serial.print(txData.textA);
        Serial.print(' ');
        Serial.print(txData.valA);
        Serial.print(' ');
        Serial.println(txData.valB);

        newTxData = false;
        rqData = true;
    }
}

//=============

void requestData() {
    if (rqData == true) { // just one request following every Tx
        byte stop = true;
        byte numBytes = 32;
        Wire.requestFrom(otherAddress, numBytes, stop);
            // the request is immediately followed by the read for the response
        Wire.readBytes( (byte*) &rxData, numBytes);
        newRxData = true;
        rqData = false;
    }
}

//=============

void showNewData() {

    Serial.print("This just in    ");
    Serial.print(rxData.textB);
    Serial.print(' ');
    Serial.print(rxData.valC);
    Serial.print(' ');
    Serial.println(rxData.valD);
}

Slave

//===================
// Using I2C to send and receive structs between two Arduinos
//   SDA is the data connection and SCL is the clock connection
//   On an Uno  SDA is A4 and SCL is A5
//   On an Mega SDA is 20 and SCL is 21
//   GNDs must also be connected
//===================


        // data to be sent and received
struct I2cTxStruct {
    char textA[16];         // 16 bytes
    int valA;               //  2
    unsigned long valB;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

struct I2cRxStruct {
    char textB[16];         // 16 bytes
    int valC;               //  2
    unsigned long valD;     //  4
    byte padding[10];       // 10
                            //------
                            // 32
};

I2cTxStruct txData = {"xxx", 236, 0};
I2cRxStruct rxData;

bool newTxData = false;
bool newRxData = false;
bool rqSent = false;


        // I2C control stuff
#include <Wire.h>

const byte thisAddress = 9; // these need to be swapped for the other Arduino
const byte otherAddress = 8;



//=================================

void setup() {
    Serial.begin(115200);
    Serial.println("\nStarting I2C SlaveRespond demo\n");

        // set up I2C
    Wire.begin(thisAddress); // join i2c bus
    Wire.onReceive(receiveEvent); // register function to be called when a message arrives
    Wire.onRequest(requestEvent); // register function to be called when a request arrives

}

//============

void loop() {

        // this bit checks if a message has been received
    if (newRxData == true) {
        showNewData();
        newRxData = false;
    }


        // this function updates the data in txData
    updateDataToSend();
        // this function sends the data if one is ready to be sent

}

//============

void updateDataToSend() {

        // update the data after the previous message has been
        //    sent in response to the request
        // this ensures the new data will ready when the next request arrives
    if (rqSent == true) {
        rqSent = false;

        char sText[] = "SendB";
        strcpy(txData.textA, sText);
        txData.valA += 10;
        if (txData.valA > 300) {
            txData.valA = 236;
        }
        txData.valB = millis();

    }
}

//=========

void showTxData() {

            // for demo show the data that as been sent
        Serial.print("Sent ");
        Serial.print(txData.textA);
        Serial.print(' ');
        Serial.print(txData.valA);
        Serial.print(' ');
        Serial.println(txData.valB);

}

//=============

void showNewData() {

    Serial.print("This just in    ");
    Serial.print(rxData.textB);
    Serial.print(' ');
    Serial.print(rxData.valC);
    Serial.print(' ');
    Serial.println(rxData.valD);
}

//============

        // this function is called by the Wire library when a message is received
void receiveEvent(int numBytesReceived) {

    if (newRxData == false) {
            // copy the data to rxData
        Wire.readBytes( (byte*) &rxData, numBytesReceived);
        newRxData = true;
    }
    else {
            // dump the data
        while(Wire.available() > 0) {
            byte c = Wire.read();
        }
    }
}

//===========

void requestEvent() {
    Wire.write((byte*) &txData, sizeof(txData));
    rqSent = true;
}

… END

…R

Reserved for future use

Very nice tutorial 8)

The variables that are used in the loop() and in interrupts should be 'volatile', the bool flags and the structs with data as well.

The Due has a number of troubles with the I2C and one was a double receiveEvent() for a single package of data, one valid and one with no data.
I therefor use this:

void receiveEvent( int howMany) 
{
  if( howMany > 0)
  {
    ...
  }
}

or when the size of the struct is fixed:

void receiveEvent( int howMany) 
{
  if( howMany == sizeof( rxData))
  {
    ...
  }
}

In an other thread you mentioned that the maximum of 50cm length for the I2C bus is a myth. Well, yes, it is. I want to call it a rule of thumb. The 50cm is because some do three or four things wrong (voltage mismatch, too much or too little pullup, no GND, wires along wires with current peaks for motors, and so on).

1 Like

Koepel:
The variables that are used in the loop() and in interrupts should be 'volatile', the bool flags and the structs with data as well.

It seems to work fine as it is. Obviously it would do not harm to make them volatile.

As far as I can see the only purpose of "volatile" is to tell the compiler not to optimise away a variable that it thinks is never used.

I have no experience with a DUE so your suggestions are welcome.

...R

@Koepel
1. flag is being changed in the interrupt context; so, it is to be declared volatile.

2. struct database is solely handled in loop() function; why should it be declared volatile?

@Robin2, I forgot to mention that there is no need to read/dump the unused data in the receiveEvent(). It is always a package of data, and the buffers are cleared.

@GolamMostafa, the receiveEvent() fills the struct with Wire.readBytes(). In the loop(), the compiler could decide to keep (a part) of the struct in registers, then the loop() does not have the latest data.

Thanks for the comments.

I'll wait a few more days to see if there are any more comments before I consider updating the code.

...R

Koepel:
In the loop(), the compiler could decide to keep (a part) of the struct in registers,

Is that likely when the struct is a global variable?

...R

Robin2:
I'll wait a few more days to see if there are any more comments before I consider updating the code.

If you have a plan to update the codes for the beginners who are hobbyists or project builders (in fact, there are many of this group), I would like to suggest--

1. In the FIRST EXAMPLE, you give up (discard) the idea of struct database. Give emphasis that the I2C Bus handles data byte-by-byte and accordingly present codes to demonstrate how two Arduinos exchange 1-byte, 2-byte, multi-byte integer, float, and text.

2. In the FIRST EXAMPLE, you may use Wire.read() under for() loop instead of Wire.readBytes() while handling multiple data bytes.

3. Your style/structure of coding is very similar to your UART Tutorials; think if it could be different. For example: we call a subroutine (when it contains a good number of lines) to keep the MLP tidy. After the Wire.requestFrom() command, there are only 2/3 lines to grab the data from the Buffer; think of putting in line codes rather than a SUR call.

4. You may check if this thread and this PDF file are helpful. This link also contains additional links to good literatures on I2C Bus.

1 Like

Robin2:
Is that likely when the struct is a global variable?

You never know. Of course, there has been problems that were solved with 'volatile', but I think that in 90% it is more theoretical. The AVR chips have 32 register, but the GCC compiler uses a few of them as "work" registers and does not use the 32 registers to the full extend. Anyway, the 'volatile' should be used, it is good practice.

volatile Keyword/modifier:
"volatile keyword is a qualifier that is applied to a variable when it is declared. It tells the compiler that the value of the variable may change at any time--without any action being taken by the code the compiler finds nearby. A variable should be declared volatile whenever its value could change unexpectedly."

When the value of a variable is updated/modified by an interrupt subroutine, it is an unexpected change. Therefore, the compiler should be informed to refrain from optimizing the said variable. This is done by appending the word volatile (the modifier) before the data type of the variable.

Code Optimization: It refers to minimizing the (i) execution time by placing the variable in the processor register and (ii) usage of memory space by reducing the number of code lines.

Once again great work !

Thank you RobinTwo.

Could be in Tutorials.

larryd:
Once again great work !

Would it not be good for the readers and poster if you would add some critical comments as every work has strength and weakness?

Well, I think it would be better disseminated if it was in PDF format like your work is.

However, people here don’t seem to like PDFs :frowning:

larryd:
Could be in Tutorials.

If the section was called simply "Tutorials" then that is where I would have put this.

However it is called "Introductory Tutorials" which I take to mean the very first things a newbie should read when he takes his first Arduino out of the box. And I don't consider that I2C between Arduinos is essential reading for a person at that early stage in their Arduino experience.

If you would like to campaign to have the word "Introductory" removed from the name of that section I will support you.

...R

Robin2:
If you would like to campaign to have the word ‚ÄúIntroductory‚ÄĚ removed from the name of that section I will support you.

Introductory does imply ‚Äėto introduce‚Äô something which could be anything of any level. Basic Tutorials are different from Introductory Tutorials. So, if you request the Moderator to shift your thread under Introductory Tutorials Section, there is a good possibility that the thread will receive much more attentions of the Forum members/readers than the current few.

GolamMostafa:
Introductory does imply 'to introduce' something which could be anything of any level. Basic Tutorials are different from Introductory Tutorials. So, if you request the Moderator to shift your thread under Introductory Tutorials Section, there is a good possibility that the thread will receive much more attentions of the Forum members/readers than the current few.

I have already made my position clear.

And it is a view that I expressed when the Introductory Tutorials section was first created. That section has so much stuff in it now that I don't see how anyone new can find anything useful in it more easily than they can find things anywhere else on the Forum.

...R