Multi Arduino communication

Having multiple Arduino boards communicate with each other, can be done in several ways. Depending on the data content, accuracy and direction the data needs to be sent, one protocol may be better than the other.

Different protocols

There are a few ways you can exchange data between Arduino boards

  • One-to-one wire
    • Digital I/O
    • Analogue I/O
    • PWM
  • Bus protocols
    • RS232
    • SPI
    • I2C
    • Other
  • Own protocol
    • Basically, anything you can image

Data exchange via one-to-one wire

As the title may suggest, one way to exchange data between two (or more) Arduino boards, is by connecting them together through one or more wires, with each wire containing one type of data.

Digital I/O

Probably the simplest way to communicate, and the most limited, is by connecting 1 digital I/O pin between (two) Arduinos. The Arduino with the I/O pin set as OUTPUT will be the sender. The Arduino with the I/O pin set as INPUT will be the receiver. Only 1 bit of data can be send this way (TRUE or FALSE) and only in 1 direction. Using a second I/O pin, a 2 way communication can be achieved.

A practical example of this, is a “digital heartbeat”. This basically is a toggling signal (generated on device Node 0), that is sent to a second device (Node 1), or even multiple devices. The receiving device (Node 1) will then monitor this signal, and if the signal stops toggling, it will know that the sending device (Node 0) is not working anymore (no heartbeat). Adding a second line, the opposite direction can be monitored as well.

Arduino digital heartbeat wiring
Arduino digital heartbeat wiring
#define nodeAddress 1  // Set to 0 or 1

unsigned long lastLocalBeat = 0;
unsigned long lastRemoteBeat = 0;
int pinSendBeat;
int pinReceiveBeat;
bool lastBeat;
int beating = 10;

/****************************************************************
      SETUP
****************************************************************/
void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial
  }

  // Node 0
  if (nodeAddress == 0) {
    pinSendBeat = 3;
    pinReceiveBeat = 4;
  }

  // Node 1
  if (nodeAddress == 1) {
    pinSendBeat = 4;
    pinReceiveBeat = 3;
  }

  pinMode(pinSendBeat, OUTPUT);	         // Send the heartbeat
  pinMode(pinReceiveBeat, INPUT_PULLUP); // Listen for heartbeat
}

/****************************************************************
      LOOP
****************************************************************/
void loop() {

  // Heartbeat every 1s
  if (lastLocalBeat + 1000 < millis()) {
    digitalWrite(pinSendBeat, not(digitalRead(pinSendBeat)));
    lastLocalBeat = millis();
  }

  // Listen every 1/2s
  if (lastRemoteBeat + 500 < millis()) {
    bool currentBeat = digitalRead(pinReceiveBeat);
    if (currentBeat != lastBeat) {
      Serial.println("Still beating ...");
      beating = 10;
    }
    else {
      Serial.println("...");
      beating--;
    }
    if (beating <= 0) {
      Serial.println("No heartbeat detected !");
    }
    lastBeat = currentBeat;
    lastRemoteBeat = millis();
  }
}
// END
Timing diagram digital heartbeat
Timing diagram digital heartbeat
ProsCons
Very simpleOnly 1 bit of data per line
Works on all boardsData in 1 direction only (per line)
AccurateNo sync (no clock)

Analogue I/O

Just as with digital I/O, analogue I/O would only work in 1 direction (per line), and in order to work with a 2 way communication, a seconds line is required.

The advantage to digital I/O is that this way 10 bits of information can be send, by analogue values 0 to 1023 (0000 0011 1111 1111).
Note that some Arduino boards have a 12 bit ADC (0 to 4095).

Arduino analogue wiring
Arduino analogue wiring
// Sender
analogWrite(A0, value);  // MKR, Zero and Due only

// Receiver
analogRead(A0);  // Any analogue pin

The downside here is that many Arduino boards don’t have an analogue output pin.

Another way of using analogue values is to treat them as 10 booleans,
or 12 booleans if using an Arduino board with 12 bit ADC. However, slight differences (4.52V vs 4.51V) will corrupt the data, so basically, this is pretty useless.

// Read analog value from pin A0
int analogValue = analogRead(A0);

// Put each bit in a boolean
bool arrayBooleans[10];
for (b=0; b<10; b++) {
  arrayBooleans[b] = bool(analogValue >> b & 1);
}
ProsCons
SimpleOnly Arduino Zero, Due and MKR series have analogue output pins
More bits available per lineData in 1 direction only (per line)
Not usable for high accuracy
No sync (no clock)

PWM

Like the analogue I/O, PWM (Pulse Width Modulation) signals can hold multiple values, but in stead of 0-1023 with the analogue 10 bit pins, PWM signals (on Arduino boards) can only vary from 0 to 255, representing the 0% to 100% duty cycle.

Arduino PWM wiring
Arduino PWM wiring

Writing and reading PWM with an Arduino is not that ideal.
For writing a PWM, the function analogWrite(<pin>, <value>) can be used, with <value> being the value 0-255 representing a duty cycle of 0-100%
For reading, function pulseIn() can be used, however the return value wil be the time in µs. Both HIGH and LOW time of the pulse must be known, to calculate the duty cycle.

duty_cycle = high_time / (high_time + low_time) * 100

#define nodeAddress 0  // Set to 0 or 1

int pinSendPWM;
int pinReceivePWM;
unsigned long lastWrite;
unsigned long lastRead;

/****************************************************************
      SETUP
****************************************************************/
void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial
  }

  // Node 0
  if (nodeAddress == 0) {
    pinSendPWM = 3;
    pinReceivePWM = 4;
  }

  // Node 1
  if (nodeAddress == 1) {
    pinSendPWM = 3;
    pinReceivePWM = 4;
  }

  pinMode(pinSendPWM, OUTPUT);	         // Generate PWM signal (use PWM pins !)
  pinMode(pinReceivePWM, INPUT_PULLUP);  // Read PWM signal

  lastWrite = millis();
  lastRead = millis();
}

/****************************************************************
      LOOP
****************************************************************/
void loop() {
  // Write every 10ms
  if (lastWrite + 10 < millis()) {
    // Send PWM random duty cycle (20..80%)
    writePWM( int(random(20,80)) );
    lastWrite = millis();
  }

  // Read every 1s
  if (lastRead + 1000 < millis()) {
    // Check the incoming PWM
    Serial.println("Reading duty cycle of " + String(readPWM()) + "%");
    lastRead = millis();
  }
}

// Write PWM to pin
void writePWM(int DutyCycle) {
  byte sendPWM;
  sendPWM = byte( float(DutyCycle) / 100 * 255 );  // Calculate pin value
  analogWrite(pinSendPWM, sendPWM);              // Write to pin
}

// Read PWM from pin
int readPWM() {
  int PWM_High_Time = pulseIn(pinReceivePWM, HIGH);   // Read HIGH time (µs)
  int PWM_Low_Time = pulseIn(pinReceivePWM, LOW);     // Read LOW time (µs)
  return int(float(PWM_High_Time) / (PWM_High_Time + PWM_Low_Time) * 100); // Calculate Duty Cycle (%)
}
// END

Looking at the code, you can image this is far from an ideal solution.

Timing diagram PWM
Timing diagram PWM
ProsCons
Works on all boardsMore complex
Analogue valuesData in 1 direction only (per line)
Not usable for high accuracy
No sync (no clock)

Bus protocols

Much more interesting are the bus protocols. Generally, they allow big chunks of data to be sent between devices, at (relatively) high speeds, in any form possible (depending on the protocol used). The guarantee much more accurate data than analogue values.

RS232

You could argue that RS232 isn’t a true bus protocol, since it basically can only be used to have 2 devices communicate with each other (although there are some tricks to work around this). The big advantage of RS232 is that the devices can both send and receive data.

Arduino RS232 wiring
Arduino RS232 wiring
#define nodeAddress 0   //Set to 0 for sender, 1 for receiver

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial
  }
}

void loop() {
  // Sender
  if (nodeAddress == 0) {
    Serial.write("ABC");
    delay(1000);  // Wait a second
  }

  // Receiver
  if (nodeAddress == 1) {
    String receiveData;
    while (Serial.available() > 0) {
      Serial.print("Receiving: ");
      receiveData = Serial.readString();
      Serial.println(receiveData);
    }
  }
}

The sample code above will send the string “ABC” to the receiver. Characters A, B and C have ASCII values 0x41, 0x42 and 0x43. Taking a look at the timing diagram, the string “ABC” is sent to the receiver with 8 databits, no parity and 1 stopbit.

RS232 timing diagram
RS232 timing diagram
ProsCons
Works on all boardsOnly between 2 devices
2 way communication
Widely supported

SPI

A bit more interesting when multiple devices are used is the SPI protocol, since it’s also possible to work in a 2 way communication. Downside with this protocol however, is that for every receiver (or slave) a separate line is required to select the slave (SS or Slave Select) hat the master (sender) wants to talk to. Slaves cannot communicate directly with each other.

Arduino SPI wiring
Arduino SPI wiring
PinWireSignal
9BlueSS(2) (Slave Select 2)
10OrangeSS(1) (Slave Select 1)
11GreenMOSI (Master Out Slave In)
12YellowMISO (Master In Slave Out)
13PurpleSCK (Clock)
// Include SPI library
#include <SPI.h>

//Set to 0 for master, 1 or 2 for slaves
#define nodeAddress 0

// Slave
char recData[50];                     // array for receiving data
volatile byte idx = 0;                // index
volatile boolean bDataReady = false;  //  Flag for data ready

/****************************************************************
      SETUP
****************************************************************/
void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial
  }

  // Master - Setup SPI
  if (nodeAddress == 0) {
    pinMode(10, OUTPUT);    // Slave Select 1 as output
    pinMode(9, OUTPUT);     // Slave Select 2 as output
    digitalWrite(10, HIGH); // Slave select arduino 1 off
    digitalWrite(9, HIGH);  // Slave select arduino 2 off
    SPI.begin();
    SPI.setClockDivider(SPI_CLOCK_DIV128);  // Set clock divider to 128
  }

  // Slave - Setup SPI
  if (nodeAddress == 1 || nodeAddress == 2) {
    pinMode(12, OUTPUT);    // MISO pin as output on slave
    pinMode(10, INPUT);     // Set SS as input
    SPCR |= _BV(SPE);       // Slave mode (SPI control register)
    SPI.attachInterrupt();  // Interrupt on SPI: ON
  }
}

/****************************************************************
      LOOP
****************************************************************/
void loop() {
  // Master
  if (nodeAddress == 0) {
    char c;
    // Send "one\r" to slave 1
    digitalWrite(10, LOW); // Slave 1 select ON (active low)
    for (const char * p = "one\r" ; c = *p; p++)
    {
      SPI.transfer (c);   // Send over SPI
      Serial.print(c);    // Show on serial monitor
    }
    digitalWrite(10, HIGH); // Slave 1 select OFF

    // Send "two\r" to slave 2
    digitalWrite(9, LOW);  // Slave 2 select ON (active low)
    for (const char * p = "two\r" ; c = *p; p++)
    {
      SPI.transfer (c);   // Send over SPI
      Serial.print(c);    // Show on serial monitor
    }
    digitalWrite(9, HIGH); // Slave 2 select 2 OFF

    delay(1000);  // Wait a second
  }

  // Slave
  if (nodeAddress == 1 || nodeAddress == 2) {
    if (bDataReady) {
      bDataReady = false;        // Reset data ready flag
      Serial.println (recData);  // Show output on serial monitor
      idx = 0; // Reset index
    }
  }
}

/****************************************************************
      SPI interrupt routine
****************************************************************/
ISR (SPI_STC_vect)
{
  byte c = SPDR;   // Read 1 byte from the SPI data register
  if (idx < sizeof recData) {
    recData[idx++] = c;   // Add byte to data array
    if (c == '\r') {      // Cariage return means end of data
      bDataReady = true;
    }
  }
}
// END

The code above sends the message “one\r” (that is “one” and a carriage return) to slave 1, and “two\r” to slave 2 (which is not connected). In the timing diagram, since slave 2 is not present, it will not send a response.
Note that characters “one” have ASCII values 0x6F, 0x6E and 0x65 and carriage return has ASCII value 0x0D (13).

SPI timing diagram
SPI timing diagram
ProsCons
Works on all boards1 extra line per (slave) device required
2 way communicationBit more complex to program
Multiple devices
Synchronised
Widely supported

I2C

When communicating with multiple devices, I2C may be the most interesting one, since every device has its own “address” and therefore sending and receiving data to and from can be selected by this address. (compared to SS line in SPI protocol)

Arduino I2C Wiring
Arduino I2C Wiring
PinWireSignal
SCL / A5YellowSCL (Clock)
SDA / A4GreenSDA (Data)
// I2C library
#include <Wire.h>

// Set the node address (0 = master, 2 and 3 are slaves)
#define nodeAddress 0

void setup() {
  // Master or Slave
  if (nodeAddress == 0) {
    // Master
    Wire.begin();  // INIT I2C
  }
  else {
    // Slave
    Wire.begin(nodeAddress);      // INIT I2C as device with address <nodeAddress>
    Wire.onReceive(i2cReceive);   // Function for receiving data
  }

  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial
  }
  Serial.println("--- Serial monitor started ---");
}

void loop() {
  // Only master sends messages
  if (nodeAddress == 0) {
    // Send to device #2
    Wire.beginTransmission(2);    // Slave is address 2
    Wire.write("22");
    Wire.endTransmission();       // End of transmission

    // Send to device #3
    Wire.beginTransmission(3);    // Slave is address 3
    Wire.write("333");
    Wire.endTransmission();       // End of transmission

    delay(1000);  // Wait a second
  }
}


void i2cReceive() {
  Serial.print("Message received: ");
  while (Wire.available()) {   // As long as data is availbale
    char c = Wire.read();      // Read char by char
    Serial.print(c);           // And print on the serial monitor
  }
  Serial.println(""); // Next line
}

The sample code above will send “22” to slave with address 2 and “333” to slave with address 3. Characters 2 and 3 have ASCII values 0x32 and 0x33. Taking a look at the timing diagram, message “22” is sent to slave 2 which is connected to the master and replies with “ACK” (ACKnowledge), message “333” is NOT sent to slave 3 since it is not connected, and a “NACK” (Not ACKnowledge”) is set in stead.

I2C timing diagram
I2C timing diagram
ProsCons
Works on all boards
2 way communication
Multiple devices
Synchronised
Widely supported

The code below shows how the master can request information from the slaves.

// I2C library
#include <Wire.h>

// Set the node address (0 = master ; >0 = slave)
#define nodeAddress 3
bool volatile bDataReady = false;
char cData[32];
int volatile idx = 0;

/****************************************************************
      SETUP
****************************************************************/
void setup() {
  // Master or Slave
  if (nodeAddress == 0) {
    // Master
    Wire.begin();  // INIT I2C
  }
  else {
    // Slave
    Wire.begin(nodeAddress);      // INIT I2C as device with address <nodeAddress>
    Wire.onReceive(i2cReceive);   // ISR on data receive
    Wire.onRequest(i2cRequest);   // ISR on data request
  }

  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial
  }
  Serial.println("--- Serial monitor started ---");
}

/****************************************************************
      LOOP
****************************************************************/
void loop() {
  // Only master sends messages
  if (nodeAddress == 0) {
    // Build string ; MAX 32 bytes by default !!!
    String data = "Uptime is " + String(millis()) + " ms";

    // Convert to char
    int dataLength = data.length() + 1; // +1, receiver cuts last byte
    char cData[dataLength];
    data.toCharArray(cData, dataLength);

    // Send to device #2
    Wire.beginTransmission(2);    // Slave is address 2
    Wire.write(cData, dataLength);
    Wire.endTransmission();       // End of transmission
    Serial.print("Message sent - ");
    Serial.print(cData);
    Serial.println(" - to slave 2");

    // Send to device #3
    Wire.beginTransmission(3);    // Slave is address 3
    Wire.write(cData, dataLength);
    Wire.endTransmission();       // End of transmission
    Serial.print("Message sent - ");
    Serial.print(cData);
    Serial.println(" - to slave 3");

    // Send request for data
    Wire.requestFrom(2, 32);
    i2cReceive();
    showMessage();
    Wire.requestFrom(3, 32);
    i2cReceive();
    showMessage();

    delay(500);  // Wait a second
  }
  // Slave part
  else {
    showMessage();
  }
}

/****************************************************************
      Show received data on serial monitor
****************************************************************/
void showMessage() {
  if (bDataReady) {
    bDataReady = false; // Reset data ready flag
    Serial.print("Message received: ");
    for (int b = 0; b < idx; b++) {
      Serial.print(cData[b]);  // Convert char array to string auto.
    }
    Serial.println("");
    idx = 0;
  }
}

/****************************************************************
      Interrupt call for receiving data
****************************************************************/
void i2cReceive() {
  char c;
  while (Wire.available()) {    // As long as data is availbale
    char c = Wire.read();            // Read char by char
    cData[idx] = c;
    idx++;
  }
  bDataReady = true;        // Set data ready flag is char is carriage return
}

/****************************************************************
      Interrupt call for receiving data
****************************************************************/
void i2cRequest() {
  // Build string ; MAX 32 bytes by default !!!
  String data = "Uptime is " + String(millis()) + " ms";

  // Convert to char
  int dataLength = data.length() + 1; // +1, receiver cuts last byte
  char cData[dataLength];
  data.toCharArray(cData, dataLength);

  // Send
  Wire.write(cData, dataLength);
  Serial.print("Replying: ");
  Serial.println(cData);
}
//END
I2C timing diagram: request data
I2C timing diagram: request data

Other protocols

Software simulated protocols

Besides the previously mentioned protocols, there are dozens of others: Profibus, EtherCAT, RS422, RS485, Ethernet, Modbus, Devicenet, CC-Link, … to name a few. By default, these are not supported by Arduino boards, but some of them can be simulated through software, and libraries are very likely available, like for example RS485.

ProsCons
Officially available protocols are well documented and widely supportedSoftware may be buggy
Mostly 2 way communicationMay be harder to diagnose
Almost all support multiple devices

Converters

Note that for Arduino, some breakout boards are available that support protocols that are by default not supported by Arduino.

It’s also possible to convert protocols with external hardware, for example, RS232 to RS485 converters. This way, the Arduino board could communicate over RS232, by default supported, to a device with RS485 protocol and vice versa.

Own protocol

Writing your own protocol can be very fun and basically can be anything you want from digital I/O with a simple clock signal, to complex serial data transfer.

ProsCons
Anything is possibleVery likely no boards support this
Can become very complex

Conclusion

When transferring data that is larger than one bit, or bi-directional traffic is required, no doubt bus protocols are the big winner here. Writing your own protocol can be challenging and fun, but what device could it possible support (except your own maybe?).