0

I am communicating two arduinos using the I2C protocol and sending from the slave to the master the integer value of UTC to update the time in the RTC module. When the master receives the UTC value from I2C and tries to set the time by the RTC.set () command the Arduino hangs and needs to be restarted, however, if I use RTC.set () in the loop () method it works perfectly. I do not understand why the system for when it is updated by I2c, is there any conflict?

This is the code with the loop() and Wire.onReceive():

#include <Thread.h>
#include <ThreadController.h>
#include <SoftwareSerial.h>
#include <LinkedList.h>
#include <SPI.h>
#include <TimeLib.h>
#include <DS3232RTC.h>
#include <Wire.h>

void setup() {
  Serial.begin(9600);  

  Wire.begin(1); 
  Wire.onReceive(receiveEvent);    

  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  setSyncProvider(RTC.get);   
  if (timeStatus() != timeSet) 
     Serial.println("Unable to sync with the RTC");
  else
     Serial.println("RTC has set the system time");

}

void loop() {


  digitalClockDisplay();
  delay(2000);

  // test only
  /*time_t t = processUTCFromI2c("1560810413");

  if (t != 0) {
    RTC.set(t);   // it's works
    setTime(t);          
  }*/

}

// ================================ INIT ONRECEIVE ===========================================

void receiveEvent(int howMany) {
  char* vectorAction;
  char c;
  Serial.println(howMany);


  LinkedList<char> myLinkedList = LinkedList<char>();

  //while (1 < Wire.available()) { // loop through all but the last
  for (int i = 0; i < howMany; i++){  
    c = Wire.read(); // receive byte as a character
    myLinkedList.add(c);
  }

  vectorAction = new char[myLinkedList.size()+1];

  for(int i=0; i < myLinkedList.size(); i++) {
    vectorAction[i] = myLinkedList.get(i);
  }

  vectorAction[myLinkedList.size()] = '\0';

  getParamsAndValues(vectorAction);

}

void getParamsAndValues(char* i2cValueString) {

  char* getValue; 
  char* getParam;
  char* endValue; 

  getValue = strchr (i2cValueString, '=');
  getParam = strchr (i2cValueString, '@');
  endValue = strchr (i2cValueString, '#');

  const size_t maxBuffLength = 15;

  char valueBuffer[maxBuffLength+1]; // allocate local buffer with space for trailing null char
  char paramBuffer[maxBuffLength+1];

  valueBuffer[0] = '\0';

  while(getValue && getParam && endValue && (getValue > getParam) && (endValue > getValue)) {
    Serial.println(i2cValueString);

    if (getValue && getParam && (getValue > getParam)) {
      size_t paramLength = getValue-getParam-1;
      valueBuffer[0] = '\0';  

      if (paramLength <= maxBuffLength) {
        strncpy(paramBuffer, getParam+1, paramLength);// http://www.cplusplus.com/reference/cstring/strncpy/
        paramBuffer[paramLength] = '\0'; // properly terminate the c-string
      }
    } else {
      // handle Wrong Input 
    }

    if (getValue && endValue && (endValue > getValue)) {
      size_t valueLength = endValue-getValue-1;
      valueBuffer[0] = '\0';

      if (valueLength <= maxBuffLength) {
        strncpy(valueBuffer, getValue+1, valueLength);// http://www.cplusplus.com/reference/cstring/strncpy/
        valueBuffer[valueLength] = '\0'; // properly terminate the c-string
      } else {
        // handle buffer too small
      }
    } else {
      // handle Wrong Input 
    }

    getValue = strchr (getValue+1, '=');
    getParam = strchr (getParam+1, '@');
    endValue = strchr (endValue+1, '#');

    i2cActionReceived(paramBuffer, valueBuffer);
  }

}

void i2cActionReceived(char* param, char* value) {
  if(strcmp(param, "setTime") == 0) {
    time_t t = processUTCFromI2c(value);

    if (t != 0) {
      RTC.set(t);   // It's not works
      setTime(t);          
    }
  }
}

// ================================ END ONRECEIVE ===========================================


// ================================ CONVERT UTC char* TO long ===============================
unsigned long processUTCFromI2c(char* utc) {
  unsigned long auxTime = 0L;

  if (strlen(utc)>0) {
    auxTime = atol(utc);
  } 

  return auxTime;
}

The slave sends a string through I2C in the format @setTime=1560810413#.

In the loop() this commented the code that works onorata and on i2cActionReceived (char* param, char* value) is where the update fails by onReceive.

1
  • you are not using consistent data in your debugging effort ... in the first instance you use "1560810413" ... in the second instance you use a parameter in a function
    – jsotola
    Commented Jun 26, 2019 at 3:28

2 Answers 2

1

The symptom you are describing is: The RTC.set() method blocks in the onReceive callback of the Wire library, but not when used in the loop() function.

That sounds like an interrupt issue. The onReceive callback function is called from an ISR (Interrupt Service Routine) of the I2C hardware. It is no good to use any code, which utilizes interrupts, inside an ISR, until you know, what you are doing.

In this case the RTC.set() function (which you can find here at line 152) will call the method RTC.write(), which will do multiple I2C transactions (both sending and receiving as master). Doing these inside an ISR may already block the code from executing further.

But there is another problem. You have 2 Arduinos connected by I2C. The master Arduino talks only to the slave Arduino, while the slave Arduino is also a master for the RTC (You initialize this Arduino as slave, but it will become a master for the time, when you try to do master transmissions). Your slave code and the RTC library both use the Wire library, meaning the hardware I2C port of the Uno. So you have 2 masters on the same I2C port.

While the I2C standard supports multi-master setups via bus arbitration, the Wire library does not. In the case, when both masters start to transmit at the same time, it will block the bus (due to some blocking loops inside the Wire library, that cannot handle bus arbitration). So the setup, that you use is either not going to work at all or being very risky, depending on the amount of send data (When the bus is filled with more data, it get's more probable, that a collision between the masters happens).

With I2C on Arduino you should stick to single-master setups. For example you can set the first Arduino as slave, keeping the second Arduino as single master on the bus, which handles both getting UTC from the first Arduino and communicating with the RTC. On the first Arduino you would set up an onRequest callback, that fills the I2C buffer with the current UTC time, which then get's transmitted to the master Arduino, so that it can update the RTC's time with it.


EDIT:

I2C has a fundamental master-slave principle. The master generates the clock signal and initiates/stops any transactions on the bus. A slave cannot send anything over the bus, when the master didn't request data from it, including "calling" the master. Any communication start's with the master.

The Wire library gives you the possibility to initialize the Arduino as slave and then do master transactions. For this the I2C hardware is configured as master for the time of the transactions. So you can initialize both Arduinos as slaves with an address and let them change to master mode for any master transaction, that you need. That is the way, that you are talking about in your comment and what you use in the code in your question.

BUT you get a big problem, when both Arduinos try to send at the same time. That is called data collision. The I2C specification has the bus arbitration for this (basically the master that first sends a 1 has to back down from the transaction), so that collisions can gracefully be resolved. The Wire library doesn't implement this, so it will block in case of a collision. Then you have to reset the Arduino to get it going again.

You should again analyse your requirements and check, if you really need I2C for communication between the Arduinos, or if you really need individual bidirectional communication. That is often also a design choice.

If you really need individual 2 way communication between the Arduinos, I see 2 possibilities:

  1. You can do it the hacky way and add an extra line between both Arduinos. You would set the Arduino with the RTC as I2C master and the other Arduino as slave. The slave Arduino can pull the extra line with a digital output pin to LOW, so that the master Arduino can sense this as signal at one of it's digital input pins (configured with internal pullup resistor). When the master Arduino senses, that the extra line goes to LOW, it can request data from the slave. So you are using the extra line for signaling the master, that new data is available at the slave.

  2. If you are not bound to I2C, other communication interfaces might be better suited. UART (in the Arduino framework called Serial) implements bidirectional bytewise communication. No master/slave relationship. That would be a lot easier.

4
  • Thanks for the explanation and the hint of how to solve the problem! Because of the project specification I will not be able to set one of the Arduinos as master, the two need to be slave and communicate between them. To solve I set the variable time_t as global and another variable boolean also as global and when it arrives a value by I2C I assign the value to time_t and true to the other variable and do the update inside the loop(). Commented Jun 27, 2019 at 1:34
  • I don't quite understand the project specification. In I2C communication there is always 1 master during a transmission. In a multi-master setup this role will just change between the devices. And the Wire library does not support multi-master. Why do you need multi-master? What exactly states the project specification about this?
    – chrisl
    Commented Jun 27, 2019 at 6:34
  • I do not know much about the Wire library, but as far as I understand it, it is not possible for the slave to call the master because it does not have an address, so my "master" is at address 1 (Wire.begin (1);) and the slave in address 2 (Wire.begin (2);) and in this way one can call the other. Is there a way for the slave to call the master since it does not have an address? Commented Jun 27, 2019 at 12:03
  • @user2831852 I added more explanations at the end of my answer.
    – chrisl
    Commented Jun 27, 2019 at 14:11
0

As explained by chrisl, the fact that the Arduinos communicate with I2c and the update of the RTC is by I2C, the update fails and locks the system. To resolve this I declare the global variables boolean updateRTC = false; and time_t setUTC; and in the method that processes UTC I assign the value true to updateRTC and the numeric value of UTC tosetUTC and do the verification in void loop().

// global vars
boolean updateRTC = false;
time_t setUTC;

// RTC update inside loop()
void loop() {
  digitalClockDisplay();
  delay(1000);

  if (setUTC > 0 && updateRTC == true) {
    updateRTC = false;
    RTC.set(setUTC);
    setTime(setUTC);
    setUTC = 0;
  }

}

// Method executed at the end of the onReceive() event
void i2cActionReceived(char* param, char* value) {
  if(strcmp(param, "setTime") == 0) {
    setUTC = processUTCFromI2c(value);

    updateRTC = true;
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.