A theoretical I2C Question

Hello,

in my previous post I had the problem that my I2C communication wouldn't work.

Although the struct was declared the same the difference in processor_type made it not work but with the help I got there (great!) it now works.

What I do not understand though is this:

On my receiver side I set up the sketch like default:
in the setup I write Wire.onReceive(receiveEvent); so when receiving it jumps to this routine.

For debugging purposes and trying to understand what was going on in this routine I made a 'flag' being a println "("In here") or a led that goes on for a small amount of time.

However....none of these flags did get active. It didn't matter if I put it before or after the Wire.read command.

So ok, because the structures have different sizes I expect it would have become a (data-) mess but why is the jump to the receiveEvent not made when there is data being send to the receiver?

Thanks!

1 Like

Please post the entire, formatted code. Use code tags.

1 Like

I wouldn't bother the code as it is much more then the question is about but here you go....

BTW: now that the Struct's are the same size and all is working I do see my led blink shortly when receiving the message.

/*
 I2C comm tussen Nano (master) en een Wemos (slave)

  Nano: A4=SDA  A5=SCL
  Wemos: D2=SDA D1=SCL

*/


# define disp_intens 7
# define loop_delay 1000
# define Slave_Addr 9          //I2C adres van de Wemos

#include <Wire.h>



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

I2cRxStruct rxData;


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

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

} //end setup

void loop() {
  
  // put your main code here, to run repeatedly:

} //einde loop


void receiveEvent(int numBytesReceived) {
 Serial.println("In receiveEvent");
 Wire.readBytes( (byte*) &rxData, numBytesReceived);
 Read_waarde = rxData.valC;
 Serial.println (Read_waarde);
 digitalWrite(Led_pin, HIGH);
 delay(500);
 digitalWrite(Led_pin, LOW);
 
} //end receiveEvent



Never attempt to print in an interrupt routine, as interrupts are turned off and printing depends on interrupts.

Instead, declare a flag as volatile, set the flag in the interrupt routine, exit and let normal program flow check the flag and do whatever is required.

2 Likes

Data bytes being transmitted by the Master from within beginTransmission() and endTransmission() methods are received by the Slave byte-by-byte on interrupt basis and are saved in a FIFO type unseen buffer.

At the end of the arrival of all the data bytes (say: 5 bytes), the control goes to the routine receiveEvent(int howMany); where, the variable howMany is equal to 5. The slave MCU reads 5-byte data from the buffer into a user defined array and then returns back to the loop() function to process the data.

Some people in the forum are seen to call the receiveEvent() routine an iterrupt context. It is my curiosity to know why they call it so?

1 Like

Thank you for your explanation.

But it only confirms that the receiveEvent should be entered but in my different-size-structure test it is not.

To clarify:

The sender is sending a structure. In total this structure contains let's say 30 bytes.

The receiver although having the same structure, due to different byte sizes of variables it is let's say 28 bytes.

So the receiver is receiving these 30 bytes and an end-of-transmission.

At this point is doesn't know the structures do not match so I would expect it will enter the receiverEventand may-be make a mess or it is stuck in there because it expects 2 more bytes but, as my flags indicate, it never enters this routine.

Now....may-be I'm getting a bit to much theoretical here because now I do have the structures exactly the same size it does work however when I tried before knowing the answer and trying to see what the problem was I would expect that it would enter this receiveEvent (so it would confirm that my sender was ok) when at least 1 byte (with an end of transmission) was being send.

Tell me about the types of your sender (UNO3 or ESP32) and receiver (UNOR3 or ESP32). I show you how to send a structure type data, receive it and re-construct the structure correctly.

But your test is not valid. You are doing a print and delay in your call back receiveEvent function which can result in unpredictable code behavior.

1 Like

We know that the MCU clears the global interrupt flag before entering an ISR, ensuring that the current ISR is not interrupted until it is completed.

Is receiveEvent() an ISR routine? If it is, then theoretically the Serial.print() method should not produce any output on the Serial Monitor; because, it requires enabled interrupt logic which has already been disabled by the calling program. Practically, the print() method does show output on Serial Monitor -- why?

From @jim-p's post in #8, I understand that the print() method is not recommended to be executed inside the receiveEvent() routine; instead, it should be executed within the loop() function.

volatile bool flag = false;
void receiveEvent(int numBytesReceived) 
{
  Wire.readBytes( (byte*) &rxData, numBytesReceived);
  flag = true;
}

void loop()
{
  if(flag == true)
  {
    Read_waarde = rxData.valC;
    Serial.println (Read_waarde);
    digitalWrite(Led_pin, HIGH);
    delay(500);
    digitalWrite(Led_pin, LOW);
    flag = false;
 }
}
1 Like

Ahaa...

what I will do for a test: I wil make my I2C communication non-functional again and I will do the test-flag test in the loop and see if the flag gets changed despite both Structures contain a different number of bytes (because 1 program runs on an esp8266 and the other on a nano. (more info see the post I mention in the OP).

Will come back with the results but today out so later may-be tomorrow.

Thanks for the suggestions!

I assume ESP8266 is sender and NANO is receiver -- correct? Give me the structure with ntial vlues for the members that ESP8266 wants to send to NANO.

1 Like

Let's put it this way, it's called from the TwoWire ISR.

2 Likes

If receiveEvent() is not being triggered at all, it usually means the I2C interrupt never fired, not just that the data was wrong. The most common reason is that the slave never detected a valid transmission, even though the master was sending something. This can happen if the I2C address doesn’t match, the wiring isn’t perfect (SDA/SCL), or the master never actually sent a STOP condition for the slave to register the data. Even if your struct sizes were different and the data became garbage, the callback should still trigger, wrong data does NOT stop receiveEvent() from being called. So if your LED or println didn’t run, it means the slave never saw the incoming message as a proper I2C packet, probably because the processor types handled the I2C hardware differently. Once the struct mismatch was fixed and the processors matched, the transmission became valid and the interrupt finally triggered. In short: the issue wasn’t the data, it was that the receiver never recognized the incoming I2C message as valid, so the code never jumped into receiveEvent() at all.

1 Like

Ok...did some testing....here we go.

First...this will be a long post (I guess as I have to write it) read it if you want, skip it if you don't want.

Now, first some environment setup 'painting':

As I2C sender a ESP8266 as master (as it can not do slave).
As I2C receiver a Nano on address 9

It's been discussed that you should not do this, all kind of limitations on either ESP or Nano side and/or different voltages/architectures etc. We'll skip that for the moment as:

I2C communication between these 2 modules IS working.

When sending a standard transmission with a byte it all works fine.

When incrementing the sended byte value (byte++) the receiving side shows it perfectly. So: although not perfect for all kind of reasons: communication is there and working flawlessly.

The problem arises when sending Structures and especially when....(cliffhanger...will come back to that later :grin: )

Working Structure send/receive:

Followed the instructions from you all above and made a volatile flag (in case I2C receive is an interrupt driven function. If not it wouldn't harm to do so).

OK: working code for the ESP8266 as sender.
Very basic, no bells/whistles just to do the basic test.

/*
  I2C comm tussen Nano (master) en een Wemos (slave)

  Nano: A4=SDA  A5=SCL
  Wemos: D2=SDA D1=SCL
  
  
*/

#define Slave_Addr 9          //I2C adres van de ontvanger (nano)
#define loop_delay 1000

#include <Wire.h>



//struct I2cTxStruct {  //NOT working
struct __attribute__((__packed__)) I2cTxStruct {    //this structure works
    char textA[16];          
    int16_t valA;            
    uint32_t valB;           
    byte padding[10];                               
}; //end struct

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


void setup() {
  Serial.begin (9600);
  Wire.begin();                 //open comm as Master

  Serial.print("sizeof(I2cTxStruct) = ");
  Serial.println(sizeof(struct I2cTxStruct));
} //end setup


void loop() {
  transmitData();  // this function sends the data if one is ready to be sent
  delay (loop_delay);
} //end loop


void transmitData() {
  Wire.beginTransmission(Slave_Addr);
  Wire.write((byte*) &txData, sizeof(txData));
  Wire.endTransmission();    // this is what actually sends the data
}

The receiver code for the Nano:

/*
  I2C comm tussen Nano (master) en een Wemos (slave)

  Nano: A4=SDA  A5=SCL
  Wemos: D2=SDA D1=SCL
*/

# define loop_delay 1000
# define Slave_Addr 9          //I2C adres van de Wemos

#include <Wire.h>

//struct I2cRxStruct {        //this works size of struct = 32 bytes
struct __attribute__((__packed__)) I2cRxStruct {  //this also works
    char textA[16];         
    int16_t valA;            
    uint32_t valB;           
    byte padding[10];                                 
}; //end struct

I2cRxStruct rxData;

volatile bool was_in_RXloop = false;


void setup() {
  Serial.begin(9600);
  
  Wire.begin(Slave_Addr);
  Wire.onReceive(receiveEvent);
  
  Serial.print("sizeof(I2cRxStruct) = ");
  Serial.println(sizeof(I2cRxStruct));
  delay (3000);
  
} //end setup

void loop() {
 // 
  if (was_in_RXloop==1) {
    Serial.println ("We did get into the receive loop!");
    was_in_RXloop=0;
  }
  //else Serial.println ("No");
} //end loop


void receiveEvent(int numBytesReceived) {
  was_in_RXloop = 1;
  Wire.readBytes( (byte*) &rxData, numBytesReceived);
} //end receiveEvent


And the outcome when I run this:

17:33:04.698 -> sizeof(I2cRxStruct) = 32
17:33:07.712 -> We did get into the receive loop!
17:33:08.300 -> We did get into the receive loop!
17:33:09.296 -> We did get into the receive loop!
17:33:10.285 -> We did get into the receive loop!
17:33:11.294 -> We did get into the receive loop!
17:33:12.298 -> We did get into the receive loop!

As you can see: appr. every second (that's the delay in the send sketch) we are into the receive event as expected.

Not in this code but I'm also able to read all the sended variables so all ok.

Now the interesting part:

There are 2 variables, on both the sender and receiver so that gives 4 possibilities.

It's all in the definition of the Structure:

A) //struct I2cRxStruct {        
B) struct __attribute__((__packed__)) I2cRxStruct { 
   

When running these structures on the Nano with the given definition the outcome for both is 32bytes (the absolute length is not important, it's 32 bytes for this test).

No difference for both declaration.

However.....

On the ESP8266 it's different.

The A) structure is 36 bytes where the B) structure is the same as the Nano: 32 bytes.

When running the program: When the sender is using structure B) I can change the structure definition in the Nano to either A or B, both work (as the both generate a struct of the same size).

But when I use the B) structure on the 8266 non of the A or B structures on the Nano will make it work....but

Also: when the sender is sending a structure of 36 bytes the receiveEvent will NOT get triggered.

Not once, not multiple times...just not.

And this I think is strange were as the receiver doesn't know upfront that there are more bytes being send then should.

My thought whould be that at the first byte it would trigger the Event and finally I don't know what would happen because there are more bytes being send then would fit in the structure but the event is not being called at all.

Now...

That's my contribution / outcome of the experiment and although I do not understand it I think the reason behind it is somewhere in the deep dungeons of the 8266 and better leave it there as it is as using the --packed-- structure does work fine.

If you take a peek at the Wire code, you will notice this in the header

#define BUFFER_LENGTH 32

You can NOT transmit more than 32 bytes in a single transaction.

1 Like

THAT's IT!!

Perfect.

I outcommented those 10 padding bytes and changed the Struct to the (previous) non-working version and now it IS getting into the Receive event!

In total the 8266 Structure is sending 24bytes while the Arduino Struct is 22 bytes so may-be there is something not getting right but that was not the concern, it was that it wouldn't go into the Receive event which it does now!

That's the solution, great, thanks!

You can take help of the following sketches (tested between two UNOR3s) to print the received byes at Slave side and do your all kinds of experiments.

Master Sketch:

#include<Wire.h>
#define slaveAdr 0x13

struct i2cTxStruct
{
  char textB[4];         // 4 bytes
  int valC;               //  2
  unsigned long valD;     //  4
  float num;              // 4
};                      //------
                        // 14

i2cTxStruct txData;

void setup()
{
  Serial.begin(115200);
  Wire.begin();
  //-----------------
  strncpy(txData.textB, "AUST", sizeof(txData.textB)); 
  txData.valC = 0x1234;
  txData.valD = 0x5678ABCD;
  txData.num = 13.57;
}

void loop()
{
  Wire.beginTransmission(slaveAdr);
  Wire.write((uint8_t*)&txData, sizeof(txData));
  Wire.endTransmission();
  delay(1000);
}

Slave Sketch:

#include<Wire.h>
#define slaveAdr 0x13
volatile bool flag = false;

struct i2cRxStruct
{
  char textB[4];         // 4 bytes
  int valC;               //  2
  unsigned long valD;     //  4
  float num;              // 4
};                      //------
// 14

int counter;
char myData[5];

i2cRxStruct rxData;

void setup()
{
  Serial.begin(115200);
  Wire.begin(slaveAdr);
  Wire.onReceive(receiveEvent);
  //-----------------
}

void loop()
{
  delay(50);
}

void receiveEvent(int numBytesReceived)
{
  Serial.println(numBytesReceived);
  Wire.readBytes((byte*) &rxData, numBytesReceived);
  strncpy(myData, rxData.textB, sizeof(rxData.textB));
  myData[4] = '\0';
  Serial.println(myData);
  Serial.println(rxData.valC, HEX);
  Serial.println(rxData.valD, HEX);
  Serial.println(rxData.num, 2);
  Serial.println("===================");
}

Output on Slave Monitor

14
AUST
1234
5678ABCD
13.57
===================
14
AUST
1234
5678ABCD
13.57
===================

You may read this thread.

1 Like

If I change the BUFFER_LENGTH to 64 in the Library, then can 64 bytes be transmitted in one transaction?

Thanks for your posting.

Any link to that thread??