Designating 2 Arduinos as both Master and Slave

Is it possible to write code such that two Arduinos communicating to each other could both make use of the Master AND the Slave functionality?

Setup: I am using two Arduinos because I have a lot of equipment that requires being monitored and operated and with more than one Arduino multitasking is much simpler.

Things I want to take advantage of:

Wire.onRequest()
Wire.onRecieve()

If I can have these functions properly working on both devices, it enables them to send and receive data at will (depending on what they hear from xyz sensor/button/device), which would be EXTREMELY useful.

The only place I can see there being issues is with Wire.begin() as I do not fully yet understand how it works.

Thanks,

cypher

*edit, I realize the Wire.begin() function may require an address in the case of the slave, and in the examples, the Wire.begin() is listed in the setup(), could I not use this function outside of setup() when I am changing the direction of these commands / requests?

*edit 2, Is what I'm trying to due possibly completely superfluous and can be done in the manner that is already intended with the Master/Slave relationship?

Wire.begin() initializes the I2C in Master mode.
Wire.begin(0xA1) initializes the I2C in Master and Slave mode.

If you use the parameter for a slave address, every possible combinations of Master, Slave, onReceive and onRequest is possible.
The only problem is how to prevent that two Arduinos want to become Master at the same time.

Well, there will be some extensive code that will order who is Master and when. Here is an example

Arduino 1 is the Main Controller
Arduino 2 has all the sensors and equipment attached

Arduino 1 tells Arduino 2 to do something and wants to hear back.
Arduino 2 communicates while Arduino 1 listens.

So far, all of this could probably be done using the normal infrastructure.

However, if there is any problem, and Arduino 2 needs to communicate this to Arduino 1, how could it request to send information if it is a Slave?

Also, if Arduino 1 needs to be doing other things while it waits for Arduino 2 to send the information, then it would be nice to be able to utilize the onReceive rather than some infinite while loop that waits for the information.

There are few different ways to implement this.

(1)
My favorite: Arduino as a Slave behaves like a sensor.
With simulated 'registers' and a interrupt to the Master.
The Master can request data from the slave, after an interrupt or if Master wants data.
The Slave executes that request with onRequest()
The Master waits during that I2C session for the data.

(2)
Faster and often used: The Slave becomes the Master.
The Masters tells the Slave to send something, that command is received by the Slave with the onReceive function.
The Slave becomes Master and sends the data to the Main Controller.
The Main Controller accepts it with its own OnReceive.
Both the Master and the Slave have their own slave address, which was set with "Wire.begin(address);"

You have to take care what kind of code can be executed in the interrupt routine.
For example the "Slave becoming the Master" should be done the "loop()" function. Therefor some polling is needed if a command is received, and after that the data can be sent to the Main Controller.

Every 'Slave' can be a Master. Just use the I2C functions to transmit data and it will work.

I tried the method where "the Master becomes the Slave" or the "Slave becomes the Master", same thing, and I couldn't really figure out how to get it to work. Maybe there was an error in my code. Here is a sample of what I am running below (I tried to chop out all the unnecessary parts unrelated to this exercise):

Arduino 1:

#include <EEPROM.h>
#include <SD.h>
#include <Wire.h>

#define pinSelect (10)
#define checker_button (2)

File Errors;
File Status;

//setup pins and/or communication lines
void setup() {
  Wire.begin(1);
  Wire.onRequest(requestEvent);
  Wire.onReceive(receiveEvent);
  Serial.begin(9600);
  pinMode(pinSelect, OUTPUT);
  pinMode(checker_button, INPUT);
  Serial.print("Initializing SD card...");
  if (!SD.begin(10)) {
    Serial.println("initialization failed!");
  }
  Serial.println("initialization done.");
    
}

//communicate with other Arduino, check buttons?
void loop() {
  button_check();
  delay(100);
}

void button_check() {
  if (digitalRead(checker_button)) {
    while (digitalRead(checker_button)) {
      
    }
    initiateGC();
  }
}

void initiateGC() {
  Wire.beginTransmission(2);
  Wire.write("GC");
  Wire.endTransmission();
}

//Sends the command to timestamp a specific code to the external SD Card Reader/Writer
void timeStamp(String codeA, int codeB) {
  if (codeB) {
    Status = SD.open("Status.txt", FILE_WRITE);
    Status.print(codeA);
    Status.print(" - ");
    Status.println(millis());
    while (Status.available()) {
      Serial.write(Status.read());
    }
    Status.close();
  } else {
    Errors = SD.open("Errors.txt", FILE_WRITE);
    Errors.print(codeA);
    Errors.print(" - ");
    Errors.println(millis());
    while (Errors.available()) {
      Serial.write(Errors.read());
    }
    Errors.close();
  }
}

//Currently unused
void requestEvent() {
  Wire.write("Nothing to do here. ");                       
}

//Depending on what code is recieved, different things are timestamped.
void receiveEvent(int howMany) {
  int code;
  String stuff;
  while(Wire.available()) 
  {
    int code = Wire.read();
  }
  if (code = 0) {
    stuff = "Who knows";
    timeStamp(stuff, 0);
  } else if (code = 1) {
    stuff = "GD";
    timeStamp(stuff, 1);
  } else if (code = 2) {
    stuff = "GP";
    timeStamp(stuff, 1);
  }
}

Arduino 2:

#include <EEPROM.h>
#include <SD.h>
#include <Wire.h>

//setup pins and/or communication lines
void setup() {
  Wire.begin(2);
  Wire.onRequest(requestEvent);
  Wire.onReceive(receiveEvent);  
}

void loop() {
  delay(100);
}

void requestEvent()
{
  Wire.write("Blank space.");
}

void receiveEvent(int howMany) {
  String command;
  int code;
  while(Wire.available())
  {
    char c = Wire.read(); // receive byte as a character
    command += c;
  }
  if (command == "GC") {
    code = 1;
    Wire.beginTransmission(1);
    Wire.write(code);
    Wire.endTransmission();
    code = 2;
    Wire.beginTransmission(1);
    Wire.write(code);
    Wire.endTransmission();
  } else {
    code = 0;
    Wire.beginTransmission(1);
    Wire.write(code);
    Wire.endTransmission();
  }  
}

Note:
Would it be better to simply use the TX/RX pins for Serial communication? I only need these 2 Arduinos (for now) to be communicating with each other. Are there any advantages to I2C or Serial over the other?

I only had a glance at the sketch, but in an interrupt routine, you can not do a lot.

Arduino 2 (the Slave), function "receiveEvent()".
That is an interrupt routine.
You can set variables, and not much else.
Using the String class takes a lot of time, it is possible to avoid that ?
You can not start a new I2C session, and you can for example also not send messages to the serial port.

My suggestion:

Create a global volatile variable in the Slave : volatile byte command;
The Master sends a binary number, not "GC", but a single byte with a number.
The Slave detects it in "receiveEvent()" and sets the global variable.
In the "loop()" the global variable is checked, if it is set, that global variable is cleared and something is sent to the Master.

Brilliant, I broke down the I2C to a simpler form and well, short form is, I got done what I needed to get done. Thanks for the help!

However, I still need this last question answered if possible. As previously indicated, I also tested the use of the RX/TX pins for serial communication and it worked well. For the use of simply 2 Arduinos communicating (only 2, both devices Arduinos), should I use I2C or the Serial Pins? I tried to find some info online and although most people said "they both have pros and cons", I couldn't find anyone ever choosing Serial Communication.

Thanks

Serial communication is also a good choice.
However, the Arduino Uno has only 1 serial port and that is used for sketch uploading and the serial monitor.
If some messages to the serial monitor are needed, that port is in use on the Arduino Uno.

The Arduino Mega has 4 and the Leonardo has 2 (not really two serial ports, but one free to use). And the SoftwareSerial library can add extra serial ports on any pin.

You can also communicate using SPI interface, or you can write your own pin toggle software. You could even make a 8-bit wide databus with clock signals.

For myself, I would prefer I2C. But I can't tell why, I just happen to like I2C.

Bah, this is ridiculous! Still having minor problems...

Here's the setup, when I flip a switch, Arduino 1 sends Arduino 2 some information, of which Arduino 2 thinks about it, and then sends something similar back to Arduino 1 (it sends 2 different transmissions about 1 second apart). With this setup alone, it works fine.

Here's the second setup, using an external MicroSD Storage Read/Write Board, I have a function that, when given a string and an int, stores the string and the current millis() in a file designated by the int. With this setup alone, it works fine.

However....when I combine them, something simply fails. I have debugging Serial.println() statements everywhere so I can see what is happening when, and those don't even get printed.

I have the switch so that it does not do anything until I switch it off again, so I pretty much get, "Button ON, Button OFF," on my Serial Monitor, and then, it transmits data to Arduino 2, then waits in a loop, then should, after receiving data from Arduino 2, print out the data received and what that corresponds to in my books. Finally, it will open the SD files, print to them, then close and reopen the SD files to read everything I have written.

As I said before, both segments of code (the I2C communication and the SD write/read communication) work fine separately, but together, something fails miserably after I flip that switch back...

SD uses SPI and your communication is I2C, that should all mix together without problem.
You use pin 10 for ChipSelect and for nothing else ?

You could upload the whole sketch (both).
I don't understand what is going wrong at what point and in which Arduino.

Is there still some Serial.print() in an interrupt routine (like receiveRequest or receiveEvent). I you have one of those, you will get that result.

Yes, pin 10 is for ChipSelect, I got all my information from the adafruit tutorial.

There is still a Serial.print() in my interrupt routine, but it normally works.

Let me explain the two tests that I ran, because the problem arose from trying to read back the information stored in the SD Card.

  1. My first tests (where I was unaware of a certain problem). This tested my intended code, and everything worked except the reading back of the data stored within the SD Card.

  2. My second test excluded the I2C communication and simply tested the timeStamp() functionality. This test can be seen by the commented out line " //timeStamp("chicken", 2);" in the buttonCheck() function. When I tested it, it performed perfectly as intended, it stored data properly as well as retrieved the entire contents of the files that I requested. Doing this test, I noted that, while my previous test had not been able to retrieve the data stored within the SD Card, it was indeed storing data properly.

In my attempt to fix the first problem, you may see the 3 lines of greyed out code in the timeStamp() function where I close and reopen the SD File (I figured this was necessary as the initial file was opened with the FILE_WRITE argument, also it was this way in the examples). Instead of fixing this problem, it somehow destroys the entire program (even parts that come before it). Even though there are Serial.print() functions that are called before timeStamp() is ever called, no such things are printed to the Serial Monitor, this can be seen with the following outputs (Test 2 is not listed as it is not necessary):

Original Test 1 Output:
Initializing SD card...initialization done.
Button ON
Button OFF
1
Reading Over.
GD
Opening Status.txt
2
Reading Over.
GP
Opening Status.txt

Note: If I throw the switch again here the output is repeated, as it should be.

Proposed Revision to Test 1:
Initializing SD card...initialization done.
Button ON
Button OFF

Note: The device seems to be stuck somewhere, because if I throw the switch again, nothing happens.

As you can see, even the Serial.print() contained within the receiveEvent() work "normally", but when I thought I "fixed" the code, for some reason those Serial.print() statements cease to function, even though the timeStamp() function is not called until AFTER those print statements...what the heck. My code for the two Arduinos is as follows:

Arduino 1:

#include <Wire.h>
#include <SD.h>

#define pinSelect (10)
#define checker_button (2)

File Errors;
File Status;
int code = 3;
String stuff;

void setup() {
  Wire.begin(1);       
  Wire.onReceive(receiveEvent);
  Serial.begin(9600);  
  pinMode(pinSelect, OUTPUT);
  pinMode(checker_button, INPUT);
  Serial.print("Initializing SD card...");
  if (!SD.begin(10)) {
    Serial.println("initialization failed!");
  }
  Serial.println("initialization done.");
}

byte x = 0;

//does nothing but wait for the switch to be thrown.
void loop() {
  button_check();  
  delay(100);
}

//On receiving data, data is read, then a timeStamp command is sent
//timeStamp command is sent based on what code is received.
void receiveEvent(int howMany) {
  stuff = "";
  while(Wire.available()) {
    code = Wire.read();
    Serial.println(code);
  }
  delay(100);
  Serial.println("Reading Over.");
  if (code == 0) {
    stuff = "Who knows";
    Serial.println(stuff);
    timeStamp(stuff, 0);
  } else if (code == 1) {
    stuff = "GD";
    Serial.println(stuff);
    timeStamp(stuff, 1);
  } else if (code == 2) {
    stuff = "GP";
    Serial.println(stuff);
    timeStamp(stuff, 1);
  }
  delay(200);
}

//checks to see if the switch has been thrown 
//waits to act until the switch is reset.
void button_check() {
  if (digitalRead(checker_button)) {
    Serial.println("Button ON");
    while (digitalRead(checker_button)) {
      
    }
    Serial.println("Button OFF");
    //timeStamp("chicken", 2);
    initiateGC();
  }
}

//simply sends a command to the second Arduino
void initiateGC() {
  Wire.beginTransmission(2);
  Wire.write("GC");
  Wire.endTransmission();
}

//Depending on what codeB is given, either Status.txt. or Errors.txt is chosen
//codeA is then printed to said .txt file as well as a millis()
//after writing data, all data contained within said file is supposed to be read back
void timeStamp(String codeA, int codeB) {
  if (codeB == 1) {
    Status = SD.open("Status2.txt", FILE_WRITE);
    if (Status) {
      Serial.println("Opening Status.txt"); 
      Status.print(codeA);
      Status.print(" - ");
      Status.println(millis());
      //Status.close();
      //delay(2);
      //Status = SD.open("Status2.txt");
      while (Status.available()) {
        Serial.write(Status.read());
      }
      Status.close();
    } else {
      Serial.println("Error Opening Status.txt");
      Status.close();
    }
  } else {
    Errors = SD.open("Errors2.txt", FILE_WRITE);
    if (Errors) {
      Serial.println("Opening Errors.txt"); 
      Errors.print(codeA);
      Errors.print(" - ");
      Errors.println(millis());
      Errors.close();
      delay(2);
      Errors = SD.open("Errors2.txt");
      while (Errors.available()) {
        Serial.write(Errors.read());
      }
      Errors.close();
    } else {
      Serial.println("Error Opening Status.txt");
      Errors.close();
    }
  }
}

Arduino 2:

#include <Wire.h>

String tester;
String command = "";
byte code;
int x;

void setup() {
  Wire.begin(2);                
  Wire.onReceive(receiveEvent); 
}

//As to your suggestion, the "replies" are in loop() and not in the interrupt.
//two codes are sent 5 seconds apart (I chose this number during later tests,
//but the original delay was very short, say 100ms)
void loop() {
    if (command == "GC") {
    code = 1;
    Wire.beginTransmission(1);
    Wire.write(code);
    Wire.endTransmission();
    delay(5000);
    code = 2;
    Wire.beginTransmission(1);
    Wire.write(code);
    Wire.endTransmission();
    delay(5000);
  }
  command = "";
  delay(100);
}

//reads in the command and stores it.
void receiveEvent(int howMany) {
  command = "";
  code = 3;
  while(Wire.available())
  {
    char c = Wire.read();
    command += c;
  }
  delay(500);
}

Please remove the Serial.print from the interrupt routines. Both in the Slave and the Master.

Sometimes it will work just once. Sometimes it will work okay. Sometimes it won't work at all. Sometimes when calling Serial.print from within "loop()" causes the buffer to get full and the Serial.print in the interrupt routine waits for the buffer to get empty, but that doesn't happen since it is in an interrupt routine itself. Sometimes ....

So there are a lot of possibilities, and that includes the problem you have.

cypherrage:
Is it possible to write code such that two Arduinos communicating to each other could both make use of the Master AND the Slave functionality?

Sure, but it becomes very complicated when two devices try to become master at exactly the same time.

If you're using two Arduinos it's much simpler to connect an extra wire so the slave can call the master's attention when it needs it.

I took out all the print statements and it seems to work, I just have no idea why it would work with the "bugged" first version and not the "correct" second version. Well, I say work, but then again it wasn't having to print out all the data from the SD Card. I guess it seem to work with small amounts of print statements but not large ones? I guess that was the overflow you were talking about.