Sending struct over i2c

im trying to send a struct from an esp8266 to an arduino nano via i2c. i have slightly modified an example to send a struct as bytes but when the master requests data the slave sends data struct 1 time then freezes.

if i remove Wire.onRequest(requestEvent); then the slave continues to receive data from the master without freezing but i cant send data. and ideas what im doing wrong?

i2c master node mcu,

#include <Wire.h>

struct {
  int gpios[8] {3, 4, 5, 6, 7, 8, 9};
  bool pins[8];
} pinData;

void setup() {
  Serial.begin(9600); /* begin serial for debug */
  Wire.begin(D1, D2); /* join i2c bus with SDA=D1 and SCL=D2 of NodeMCU */
  pinData.gpios[0]=999;
    Serial.print( pinData.gpios[0] );
}

void loop() {
  Wire.beginTransmission(8); /* begin with device address 8 */
  Wire.write("T1START");  /* sends hello string */
  Wire.endTransmission();    /* stop transmitting */

  Wire.requestFrom(8, sizeof(pinData)); /* request & read data of size 13 from slave */
  while (Wire.available()) {
    Wire.readBytes((byte*)&pinData, sizeof(pinData));
    Serial.print( pinData.gpios[0] );
  }
  Serial.println();
  delay(1000);
}

i2c slave arduino nano,

#include <Wire.h>

String readString;

unsigned long now = 0;



struct {
  int gpios[8] {3, 4, 5, 6, 7, 8, 9};
  bool pins[8];
} pinData;

void setup() {
  Wire.begin(8);                /* join i2c bus with address 8 */
  Wire.onReceive(receiveEvent); /* register receive event */
  Wire.onRequest(requestEvent); /* register request event */
  Serial.begin(9600);           /* start serial for debug */
  Serial.println("hi");
  
}

void loop() {

  if (millis() - now >= 500) {
    for (int i; i <= 8; i++) {
      pinData.pins[i] = digitalRead(pinData.gpios[i]);
      Serial.println(pinData.pins[i]);
    }
    now = millis();
  }
}

// function that executes whenever data is received from master
void receiveEvent(int howMany) {
  while (0 < Wire.available()) {
    char c = Wire.read();      /* receive byte as a character */
    readString += c;
  }
  if ( readString, "T1START") {
    Serial.println("it worked");
  }
  readString = "";

}

// function that executes whenever data is requested from master
void requestEvent() {
 Serial.println("writing to master");
  Wire.write((byte *)&pinData, sizeof(pinData));  
}

also the single time i do receive the struct from the slave it is incorrect values. is this an endian issue?

i'm curious how arbitrary length data is received via I2C.

does a master need to specify the amount of data being read? does it "poll" a slave for data?

could a poll respond with a fixed amount of data indicating the type of data and length which is then received in a follow-up request?

Okay i didnt realize i was using ISR and i had to remove the serial print now it has stopped freezing, however im getting incorrect data.

Serial.print( pinData.gpios[0] ); should be 3 on the master but instead after receiving the struct from the slave it says 262147.

What going on now?

== 0x40003

i got it working by explicitly defining variable sizes,

struct {
  uint16_t gpios[8] {3, 4, 5, 6, 7, 8, 9};
  uint8_t  pins[8];
} pinData;

instead of

struct {
  int gpios[8] {3, 4, 5, 6, 7, 8, 9};
  bool  pins[8];
} pinData;

the answer was in another question i asked here lol,

Data type int is 16 bits on a nano, 32 bits on the esp8266.

On the nano, declare the struct as volatile since it is being used in an ISR.

There really is no reason to define gpios as int or uint16_t - use uint8_t instead (unless you somehow expect the nano to suddenly have over 255 i/o pins). That will avoid problems with reading a multi-byte volatile variable.

sorry forgive my knowledge but i thought that was only necessary to do on a esp processor?

or was it the variables that are handled by the ISR function?

volatile is needed whenever a variable is used in both an ISR and somewhere else in the code. That lets the compiler know that the value of the variable may change at any time, so that it will not make any optimizations with the expectation that the value remains unchanged because it is not altered in the code itself. On the nano, any time the variable is larger than a single byte, it should not be accessed outside the ISR without temporarily disabling interrupts, otherwise an interrupt may occur between accessing each byte of a multi-byte variable because the processor can only access memory a single byte at a time.

1 Like

so far the communication seems pretty reliable but.. i will be sending requests to the slave pretty often

request
100ms
request
100
request
100ms
request
10000ms
repeat*

since these requests will be changing the state of the gpios i want to make sure that the request gets through and handles the gpio states. I dont want to say, send a request to set a gpio LOW only for it not to arrive for whatever reason. what are my options here?

im trying to wrap my mind around some sort of ack or checksum that both MCU can agree the request was parsed and is correct

how would the code differentiate between receiving different types of messages?

well if i wanted to write gpio 1 high i would send request, wire.write("gpio1HIGH); but how am i to make sure the request is resent if it did not make it to the other side. do i request a response from the slave containing the gpio states and see if the state actually change and if not send the request again? i read i2c is suppose to have error checking "whatever that means" but im not confident that every single request/response will make it through to the slave vice versa

an i2c gpio expander probably wouldn't be much more reliable right? only upside of the gpio expander is its easier to read the states of the gpios but i think that might be arguable.

im just filling big buckets of water and i dont want to spend all day mopping a thousand gallons of water from the floor lol. maybe ill just put the code that handles the pins on the slave and control the time values though i2c. its less risky but i would like to just use the salve as expander

i asked how would you receive an arbitrary message?

if the master sends an arbitrary request, the receiver can send data associated with that request or an acknowledgement. if the master doesn't receive a response to the request it can send it again. 3 failures, reset the system

but after requestFrom() Wire.avaliable() needs to return true to parse the incoming response. what if wire.avaliable never returns any data?

and forgive me im not exactly sure what you mean.

what happens if i receive random request? im not sure how to answer that

 if (Wire.available()) {

capture a timestamp and check if no response w/in some timeout period

so should i

while(!Wire.avaliable())
//if timer expires resend

&&
if time expires and some checksum != what it suppose to, resend request

or just the latter?

conventional code practice would separate sending and receiving messages. the code may also need to monitor the serial interface for commands, as well as check for a timeout

loop ()
{
    if (Serial.available ()) {
        ...
    }

    if (Wire.available ()) {
        ...
    }

    if (timingSomething && (msec - msecLst) > TIMEOUT) {
        ...
    }
}

there would be separate sub-functions to send messages. that code can keep track of what it expects in response. since you haven't answered my question, i'll assume the code always knows what to expect (i.e. # bytes)

ahh yes the struct is a known size of bytes. so what happens if i send a message with arbitrary size, would this be okay as long as receiver gets message no more than but maybe less than the amount of byte i specify?

i could add a variable to the struct that is changed by the receiver of a message and sent back to the sender with an expected value. this might solve the size problem without sending multiple messages of different sizes

should i send a response back to the sender with an expected value or should i just see if i got a response back at all. i know either way is not perfect but what would you do. would you check a variable for a specific value to know if it was successful or just see if i got a response at all from the receiver

as i mentioned before, you can use the TLV approach where each message is preceded by a type and length.

if the type and length are bytes, the receiver first reads 2 bytes and then reads the message now knowing the length.

these values would be passed to the sending routine along with a byte pointer to the data

an ACK could be of type ACK and length zero. a struct would have a non-zero length

why does receiver read first 2 bytes then the data. are the first two byte the length?

so then you mean send the request and the receiver respond with the length of bytes it got from the sender. why the bytepointer to the received data?

my limited knowledge i think your saying, send the request, the receiver respond and say the length of that data was """ and the data itself was ""

i think im probably wrong but i want to ask because i want to know