A nightmare with I2C serial link, especially with onReceive

Apparently I2C is easy to use! Well unfortunately it's not for me!

I have a NANO as a slave, which has 4 variables in it (Hrs, Min, etc gained by reading a GPS unit). That all works OK.
I have a UNO as the master which would like to be able to read those 4 variables (all under 255) and then do something with them. Preferably by receiving one variable at a time as and when required, but receiving all 4 variables together might be OK.

I can send a Request from master to slave and the slave will reply instantly with which ever variable is in the Write command. However this obviously only returns one variable, not the four that the slave holds.

I believe a way to do this might be to get the master to send a value and then the slave will return a different variable depending on the value received from the master. If this is the way to go then I think I need to use Wire.onReceive and that's when the problems begin! As soon as I put Wire.onReceive in the 'setup' the program refuses to compile and says "'receiveEvent' was not declared in this scope", I have no idea what this means.

Below is the sketch for the slave which works fine (for one variable). The bits that are commented out are the bits I think I need to add, but can't.
Any help will be massively appreciated.

//////////////SLAVE////////////
#include <SoftwareSerial.h>
#include <Wire.h>

int Hrs =23; // The value (from GPS) that will be sent to master (UNO)
int Mins =59; // The value (from GPS) that will be sent to master (UNO)
int infofromUNO = 0; // The info I'm trying to get from the master - but can't.

void setup()
{

// Wire.onReceive(receiveEvent); //receive data from master
Wire.onRequest(requestEvent); //master requests data from slave

Wire.begin(9);
Serial.begin(9600);
}

//void receiveEvent(x);
// {
// if (Wire.available())
// {
// infofromUNO = Wire.read();
// }
//}

void requestEvent()
{
Wire.write(Mins); //data sent to master when requested.
}

void loop()
{

}

Sorry, I missed a crucial bit. It doesn't help you didn't use code tags to post your code.

Note that 'int' is not the correct receive type; Wire transfers go byte by byte. So use 'byte' or 'uint8_t'. If you want to send an 'int' (2 bytes), break it up into two bytes and send them separately.

Your basic approach to the problem of transferring 4 variables is indeed correct: have the master send a code to the slave specifying which variable it wants to receive in return. On the slave side, you can make for instance a 'switch-case' block that checks which code was received and hence which variable to send.

Your solution would be very similar / identical to the use of so-called 'registers' that are common in I2C devices such as sensors. I've used this approach a lot too and it works like a charm. Here's a bit of code from a project I'm working on atm:

void I2C_receive_event (int quantity) 
{
  if (Wire.available() > 0) 
  {
    i2c_register = Wire.read(); //This is the 'code' the master sends telling the slave what kind of information it wants to send
    if (quantity > 1)
       process_wire_input(i2c_register, Wire.read()); //This line calls a function that processes the incoming data from the master
  }
}

void I2C_request_event () 
{
  switch (i2c_register)  //i2c_register is defined elsewhere; it's the 'code' the master sends to the slave to ask for a particular bit of information...
  {
    case REG_STATUS: //...such as the contents of the status register...
    {
      Wire.write(reg_status_v); //...which consists of 1 byte of data...
    }
    break;
    case REG_ITIME: //..or a variable that is actually of an int type, so 2 bytes...
    {
      uint8_t *ptr = (uint8_t*)(void*)reg_itime_v; //...so we use a pointer to that variable and then use it to write 2 bytes back to the master
      for (uint8_t i = 0; i < 2; i ++)
      {
        Wire.write(*ptr);
        ptr ++;
      }
    }
  }
}

The above is just an excerpt that I edited a bit for brevity. It won't compile as it is, but it shows how you could approach this. There are several ways to skin this cat. The use of a pointer to write 2 bytes of an integer is perhaps not what everyone prefers and indeed it can be done differently as well, but this is fairly efficient also if it needs to be scaled up to e.g. 4 bytes (e.g. for a float).

You have

If you get rid of the semi-colon and change (x) to int(x) it will compile but you'll get orange notes that something is still amiss.

Koraks - Apologies for not putting the code into a grey box, how do I do that? Thanks so much for your very detailed reply, although unfortunately a lot of it has gone over my head.
I have removed the stray ';' and added 'int' as pointed out by 'runaway_pancake' and it now compiles so I now have something to work with. I will now persevere with your information to sort out the 4 variables, so thanks again for your time. Although I might be back again if I can't sort them out!!
'runaway_pancake' - Thanks for pointing out my 2 mistakes, I should have noticed the stray ';' but would not have found the missing 'int' without your help, It now compiles OK with no messages, so again thanks for your help.

Great, good luck and see if you can get this to work.

About the grey block thing: just on top of the post textbox, there's this little icon that says '</>'. You can paste your code, select it and then press that icon. The code will then be displayed (more or less) correctly in terms of formatting.

Show the full sketch for the Master and Slave.
If it is part of a larger project, then make two small sketches, to test the I2C communication.
Please don't include SoftwareSerial.

Who told you that ? I have written pages to discourage new users for using the I2C bus between Arduino boards: Reliable I2C bus and Arduino in Slave mode.

It usually is, if you use I2C for the design purpose, which is communication between chips on a single PCB.

@ngee
1. I2C handles data one byte at a time; accordingly, your requestEvent() function should be as follows:

void requestEvent()
{
  Wire.write(highByte(Hrs));
  Wire.write(lowByte(Hrs));
  Wire.write(highByte(Mins));
  Wire.write(lowByte(Mins));//data sent to master when requested.
}

2. At the Master side, you need to execute the following request command:

Wire.reaquestFrom(9, 4);
byte Hrh = Wire.read();
byte Hrl = Wire.read();
int Hrs = Hrh<<8|Hrl;
//---------------------------
1 Like

I have seen hundreds of young learners liking the logic of the I2C Protocol!

I have to admit I find I2C quite convenient. Yes, it helps to keep distances (very) short, use appropriately sized pullups and don't try to use it to transmit vast amounts of data. But other than that, I personally find it one of the easiest ways to interface two microcontrollers. Easier than Serial, and much easier than OneWire (although that works quite nicely over longer distances).

1 Like

Thank you all again for your help. I now have a fully working 'GPS to main board' program. It may not be very neat and I'm sure it's not 'good' programming, but it works!! If anyone else is struggling and comes across this thread then here is my final version. Which might even come out in little grey boxes!!

/////////MASTER////////
#include <Wire.h>
#include <SoftwareSerial.h>

 int infofromNANO = 0;
 int selectGPSinfo = 2; // 1 is Month, 2 is Hrs, 3 is Mins, 4 is secs
						 // This variable will be changed, as required, 
						 // by the rest of the program.
  void setup()
{  
   Serial.begin(9600);
   Wire.begin (); 
}

void loop()
{ 
   Wire.beginTransmission(9);
   Wire.write(selectGPSinfo); // send 1,2,3, or 4 to slave to get required variable
   Wire.endTransmission();

   Wire.requestFrom(9,1);
     if (Wire.available())
       {
         infofromNANO = Wire.read();  // recieve GPS data from slave
         Serial.println (infofromNANO);
       }
}

//////////////SLAVE////////////
#include <SoftwareSerial.h>
#include <Wire.h>

int Month = 4;  //These values will be read from the GPS
int Hrs  = 23;  // and then will be sent to master (UNO)
int Mins = 59;
int Secs = 45;
int infofromUNO = 0;
int sendGPS = 0;


void setup()
{ 
 
   Wire.onReceive(receiveEvent);  //receive data from master
   Wire.onRequest(requestEvent);  //master requests data from slave  
   Wire.begin(9);
   Serial.begin(9600);
}


void receiveEvent(int quantity)
  {  
     if (Wire.available()>0)
        {
           infofromUNO = Wire.read();          
        }      
   }

void requestEvent()
   {   
       Wire.write(sendGPS);     //GPS data sent to master when requested. 	 
   }

void loop()
   {
      Serial.println (infofromUNO);
      if (infofromUNO == 1) sendGPS = Month;
      if (infofromUNO == 2) sendGPS = Hrs;
      if (infofromUNO == 3) sendGPS = Mins;
      if (infofromUNO == 4) sendGPS = Secs;
   }

It is short and simple, but there are a few things that can be improved.

The Slave is constantly hammered with I2C sessions. If the Slave is a simple 16MHz Arduino board (such as you Nano), then it needs "time to breathe". Even a 'delay(1);' of 1 millisecond in the loop() of the Master is enough.
I prefer that you define how often that variable needs to be updated. Is twice per second enough, then make the delay 500ms.

In the Slave, you should start with 'Wire.begin(9);', and after that, do other Wire functions, such as Wire.onRequest() and Wire.onReceive().

Some improvements can be made in the overall working of the code:

  • It is easier to use just one I2C sessions that transfers all the 4 bytes.
  • Variables that are used in the loop() and in interrupts should be 'volatile'.
  • The Slave loop() sets the variable 'sendGPS' over and over again. It needs to be set only once after a command from via the I2C bus.

Anyone that says I2C is easy and simple has not had the pleasure of writing a driver for one. There are so many things that can go wrong in an I2C transaction, and without the proper driver support, you almost always end up in a "locked-up" state.

Koepel - I have modified the code to most of your recommendations. The delay in the master loop was particularly useful info as when I programmed up 2 real boards, instead of using the simulation program I had been using, it wouldn't work without the delay. I have now incorporated the I2C part into the main project and it's all working fine.
So, thanks again to all who've helped me, I would never have done it without your help.