I2C communication out of sync, ESP32

Hi, I'm having trouble using two ESP32's to request/send data via I2C. Whenever the master device sends a request it always seems to serial print the previous request from the slave device, as per:

12:36:57.853 -> Send Command: humidity
12:36:57.899 -> Received Data: Temp: 25.30
12:36:57.899 ->
12:36:58.870 -> Send Command: status
12:36:58.948 -> Received Data: Humidity: 60.70
12:36:58.948 ->
12:36:59.935 -> Send Command: temperature
12:36:59.978 -> Received Data: Status: OK

Code for master device:

#include <Wire.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_DEV_ADDR 0x52

void sendCommandAndRequestData(const char* command) {
  Serial.print("Send Command: ");
  Serial.println(command);
  
  // Send command to the slave
  Wire.beginTransmission(I2C_DEV_ADDR);
  Wire.write(command);  
  Wire.endTransmission();
  
  delay(50);  // Allow the slave time to process the command

  // Request data from the slave
  const int requestSize = 32;         // Match the size of the expected data
  char receivedData[requestSize + 1];  // Buffer to store received data
  int index = 0;

  Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request 32 bytes from the slave
  
  while (Wire.available()) {
    receivedData[index++] = Wire.read();
  }
  
  receivedData[index] = '\0';  // Null-terminate the string
  
  // Print the received data
  Serial.print("Received Data: ");
  Serial.println(receivedData);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
}

void loop() {
  // Test commands
  sendCommandAndRequestData("temperature");
  Serial.println("");
  delay(1000);
  sendCommandAndRequestData("humidity");
  Serial.println("");
  delay(1000);
  sendCommandAndRequestData("status");
  Serial.println("");
  delay(1000);
}

Code for the slave device:

#include "Wire.h"

#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_DEV_ADDR 0x52

String currentCommand = "";  // Stores the received command

void onRequest() {
  char buffer[32] = {0};  // Initialize the buffer with null characters
  
  if (currentCommand == "temperature") {
    float temperature = 25.3;  // Example temperature data
    snprintf(buffer, sizeof(buffer), "Temp: %.2f", temperature);
  } else if (currentCommand == "humidity") {
    float humidity = 60.7;  // Example humidity data
    snprintf(buffer, sizeof(buffer), "Humidity: %.2f", humidity);
  } else if (currentCommand == "status") {
    snprintf(buffer, sizeof(buffer), "Status: OK");
  } else {
    snprintf(buffer, sizeof(buffer), "Unknown Command");
  }

  // Pad the remaining buffer with nulls to avoid sending garbage data
  Wire.write((uint8_t*)buffer, 32);  // Send exactly 32 bytes back to the master
}

void onReceive(int numBytes) {
  char receivedCommand[20];  // Buffer to store incoming command

  // Read the command sent by the master
  Wire.readBytes(receivedCommand, numBytes);
  receivedCommand[numBytes] = '\0';  // Null-terminate the string

  // Store the command to use in the onRequest function
  currentCommand = String(receivedCommand);

  Serial.print("Command Received: ");
  Serial.println(currentCommand);
}

void setup() {
  Serial.begin(115200);
  Wire.begin((uint8_t)I2C_DEV_ADDR, (int)SDA_PIN, (int)SCL_PIN, (uint32_t)100000);  //device address, sda, scl, frequency
  Wire.onReceive(onReceive);
  Wire.onRequest(onRequest);
}

void loop() {
}

I've tried a number of things like increasing delays and changing the way requests/repsonses are handled but nothing seems to work. Is there something really obvioius I'm missing here?

Don't print to Serial in the I2C event handlers.

I'm not seeing that making any difference. I still get the same result.

The only way I've got this to work is to repeat the lines for requesting the data with the master device:

  Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request 32 bytes from the slave
  delay(100);
  Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request 32 bytes from the slave
  delay(100);

While this works, it doesn't seem right.

Prepare the answer immediately following the command receive, to prevent a timeout in the onRequestEvent().

I've tried that with...
Master:

#include <Wire.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_DEV_ADDR 0x52

void sendCommandAndRequestData(const char* command) {
  Serial.print("Send Command: ");
  Serial.println(command);
  
  // Send command to the slave
  Wire.beginTransmission(I2C_DEV_ADDR);
  Wire.write(command);  
  Wire.endTransmission();
  
  delay(20);  // Allow the slave time to process the command

  // Request data from the slave
  const int requestSize = 32;         // Match the size of the expected data
  char receivedData[requestSize + 1];  // Buffer to store received data
  memset(receivedData, 0, sizeof(receivedData));  // Clear buffer

  Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request 32 bytes from the slave
  
  int index = 0;
  while (Wire.available() && index < requestSize) {
    receivedData[index++] = Wire.read();
  }
  
  receivedData[index] = '\0';  // Null-terminate the string
  
  // Print the received data
  Serial.print("Received Data: ");
  Serial.println(receivedData);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
}

void loop() {
  // Test commands
  sendCommandAndRequestData("temperature");
  Serial.println("");
  delay(1000);
  sendCommandAndRequestData("humidity");
  Serial.println("");
  delay(1000);
  sendCommandAndRequestData("status");
  Serial.println("");
  delay(1000);
}

Slave:

#include "Wire.h"

#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_DEV_ADDR 0x52

String currentCommand = "";  // Stores the received command
char buffer[32] = {0};  // Initialize the buffer with null characters

void onRequest() {
  Wire.write((uint8_t*)buffer, sizeof(buffer));  // Send exactly 32 bytes back to the master
}

void onReceive(int numBytes) {
  char receivedCommand[20];  // Buffer to store incoming command

  // Read the command sent by the master
  Wire.readBytes(receivedCommand, numBytes);
  receivedCommand[numBytes] = '\0';  // Null-terminate the string

  // Store the command to use in the onRequest function
  currentCommand = String(receivedCommand);

  Serial.print("Command Received: ");
  Serial.println(currentCommand);

  memset(buffer, 0, sizeof(buffer));  // Clear the buffer

  // Populate the buffer based on the command  
  if (currentCommand == "temperature") {
    float temperature = 25.3;  // Example temperature data
    snprintf(buffer, sizeof(buffer), "Temp: %.2f", temperature);
  } else if (currentCommand == "humidity") {
    float humidity = 60.7;  // Example humidity data
    snprintf(buffer, sizeof(buffer), "Humidity: %.2f", humidity);
  } else if (currentCommand == "status") {
    snprintf(buffer, sizeof(buffer), "Status: OK");
  } else {
    snprintf(buffer, sizeof(buffer), "Unknown Command");
  }
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Wire.begin((uint8_t)I2C_DEV_ADDR, (int)SDA_PIN, (int)SCL_PIN, (uint32_t)100000);  //device address, sda, scl, frequency
  Wire.onReceive(onReceive);
  Wire.onRequest(onRequest);
}

void loop() {
  // The slave will respond to requests based on the command received
}

But that still didn't work unless in the Master code i repeat the Wire.requestFrom:

Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request 32 bytes from the slave
  delay(20);
  Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request 32 bytes from the slave

I spent so much time trying to figure this out, so for now I'll just stick with this solution.

In your slave code, did you try making your buffer volatile since it is being accessed in two different ISRs?

I guess ESP32 is the I2C Master. Which Arduin is the I2C Slave? What type of sensor you are using and with Arduino (Master or Slave) it is connected? My intention is to reproduce the fault and suggest corrections.

Unfortunately my knowledge of coding is quite limited which is why I'm probably struggling to solve this. The code provided is all I've come up with. I'm not fimilar with making a buffer volatile, but will look into this and see if it helps. Thanks for the input.

I have two ESP32's, one being the slave and the other the master. At the moment there are no sensors connected to them. This is just the first step in trying to figure out how to request/send different data strings from one to another.
Any help would be really appreciated.
If I do find a solution I'll make sure to post the results as I'm sure this will be helpful to others. I've found no similar examples of sending a receiving specific data strings.

Please, provide an example of a data item like 0x1234 or 12.75 or "Forum" that you want to exchange between Master and Slave using I2C Bus.

That's a very untypical usage of I2C. Usually commands are sent as numbers and results returned as binary values as well. That's why you won't find serious examples of sending strings via I2C.

so as an example the master would send a request for data from different sensors, e.g. a particle counter:

void loop() {
  sendCommandAndRequestData("particle");
  delay(1000);
}

void sendCommandAndRequestData(const char* command) {
  Serial.print("REQUEST COMMAND: ");
  Serial.println(command);

  Wire.beginTransmission(I2C_DEV_ADDR);
  Wire.write(command);  // Send command to the slave
  Wire.endTransmission();

  delay(200);  // Give slave time to process the command

  // Request data from the slave
  const int requestSize = 100;         // Match the size of the expected data
  char receivedData[requestSize + 1];  // Buffer to store received data
  int index = 0;

  Wire.requestFrom(I2C_DEV_ADDR, requestSize);  // Request data from the slave
  delay(50);

  while (Wire.available()) {
    receivedData[index++] = Wire.read();
  }
  receivedData[index] = '\0';  // Null-terminate the string

  // Print the received data
  Serial.print("Received Data: ");
  Serial.println(receivedData);

  parseData(receivedData, command);
}

The slave device would read the particle counter (via rs485 modbus) and send all the values, comma separated:

void onRequest() {
    char buffer[100] = {0};

  if (currentCommand == "particle") {

    i4 = random(0, 241) / 10.0;
    i6 = random(0, 241) / 10.0;
    i14 = random(0, 241) / 10.0;
    i21 = random(0, 241) / 10.0;
    n5 = random(-1, 13);
    n15 = random(-1, 13);
    n25 = random(-1, 13);
    n50 = random(-1, 13);
    n100 = random(-1, 13);
    s4 = random(-2, 13);
    s6 = random(-2, 13);
    s14 = random(-2, 13);
    s21 = random(-2, 13);
    s38 = random(-2, 13);
    s70 = random(-2, 13);
    g4 = random(-2, 13);
    g6 = random(-2, 13);
    g14 = random(-2, 13);
    g21 = random(-2, 13);
    g38 = random(-2, 13);
    g70 = random(-2, 13);

    snprintf(buffer, sizeof(buffer),
             "Particle: %04.1f,%04.1f,%04.1f,%04.1f,"     // i4, i6, i14, i21
             "%02d,%02d,%02d,%02d,%02d,"        // n5, n15, n25, n50, n100
             "%02d,%02d,%02d,%02d,%02d,%02d,"   // s4, s6, s14, s21, s38, s70
             "%02d,%02d,%02d,%02d,%02d,%02d",  // g4, g6, g14, g21, g38, g70
             i4, i6, i14, i21,                  // Floats: i4, i6, i14, i21
             n5, n15, n25, n50, n100,           // Integers: n5, n15, n25, n50, n100
             s4, s6, s14, s21, s38, s70,        // Integers: s4, s6, s14, s21, s38, s70
             g4, g6, g14, g21, g38, g70         // Integers: g4, g6, g14, g21, g38, g70
    );

  } else {
    snprintf(buffer, sizeof(buffer), "Unknown Command");
  }

  // Send data back to master
//  Wire.write(buffer);
  Wire.slaveWrite((uint8_t*)buffer, sizeof(buffer));  // Send exactly 32 bytes back to the master

  Serial.println("Sent:");
  Serial.println(buffer);
}

void onReceive(int numBytes) {
  char receivedCommand[20];  // Buffer to store incoming command

  // Read the command sent by the master
  Wire.readBytes(receivedCommand, numBytes);
  receivedCommand[numBytes] = '\0';  // Null-terminate the string

  // Store the command to use in the onRequest function
  currentCommand = String(receivedCommand);

  Serial.print("Command Received: ");
  Serial.println(currentCommand);
}

The master device would get this data and parse it so all the comma separated values are then stored in the relevant variables.
Is this possibly the wrong way of using I2C and there's a more suitable way of getting all these values from the various sensors I may be working with?

For the ESP32 you need to use slaveWrite. It is used in the onReceive function not the onRequest
See post #7 in this topic

Thank you so much, I seem to be having some luck with this.
I'll refine and simplify things and post the results when I've got a working example.

No hurry

FYI:
The problem with I2C and the ESP32 is that the ESP32 does not do I2C clock stretching.
When a master requests data and the slave doesn't have the data ready to send, it can pull the SCL line low (stretching the clock) to signal to the master that it is not ready. When it has the data ready, it will release the SCL line and the master can read the data.

However, since the ESP32 does not do clock stretching, the master will immediately read whatever is the the I2C transmit buffer. So if the buffer was not preloaded with the expected data the master will read invalid or old data, hence the need for the slaveWrite() function.

I think I have understood what you want to achieve.

Let us assume that after receiving request from Master, the Slave will send the following data stream to Master over the I2C Bus. Master will collect those data form the I2C bus and will save them into appropriate variables.

23 1234 56.78 Forum

Output:

Received y1: 23
Received y2: 1234
Received y3: 56.78
Received str: Forum
====================================

Master Sketch: (tested using 30-pin ESP32s)

#include <Wire.h>

const int I2C_SLAVE_ADDRESS = 8;  // Slave address

// Define the same struct on the master
struct myData 
{
  byte y1;
  int y2;
  float y3;
  char str[10];
};

myData receivedData;  // Create an instance to store received data

void setup() {
  // Initialize I2C as master
  Wire.begin();
  Serial.begin(115200);
  delay(1000);  // Give some time for initialization
}

void loop() {
  // Request data from the slave
  Wire.requestFrom(I2C_SLAVE_ADDRESS, sizeof(receivedData));  // Request the size of the struct

  // Read the data into the struct
  if (Wire.available() == sizeof(receivedData))
  {
    Wire.readBytes((byte*)&receivedData, sizeof(receivedData));
  }

  // Print the received data
  Serial.print("Received y1: ");
  byte z1 = receivedData.y1;
  Serial.println(z1, DEC);

  Serial.print("Received y2: ");
  int z2 = receivedData.y2;
  Serial.println(z2, DEC);

  Serial.print("Received y3: ");
  float z3 = receivedData.y3;
  Serial.println(z3, 2);

  Serial.print("Received str: ");
  Serial.println(receivedData.str);  // Print the received string
  Serial.println("====================================");
  delay(1000);  //test interval
}

Slave Sketch:

#include <Wire.h>
#define slaveAddress 8

// Define the struct to hole conglomerate type data
struct myData
{
  byte y1;
  int y2;
  float y3;
  char str[10];
};

myData dataToSend;

void setup()
{
  Serial.begin(115200);
  // Initialize the I2C bus as a slave
  Wire.begin(slaveAddress);
  Wire.onRequest(requestEvent);   // Register the function to send data when requested



  // Prepare data for the struct
  dataToSend.y1 = 23;
  dataToSend.y2 = 1234;
  dataToSend.y3 = 56.78;
  strcpy(dataToSend.str, "Forum");

  Serial.println("I2C Slave ready");
}

void loop()
{

}

void requestEvent()
{
  Wire.write((byte*)&dataToSend, sizeof(dataToSend));  // Send the entire struct as bytes
}
1 Like

Hi, @ftlab

What distance do you want to have this I2C link?

Why I2C?

What is your project?

Thanks.. Tom.. :smiley: :+1: :coffee: :australia:

Thanks to everyone for all your input and help on this. I've got a working solution below so hopefully that'll be of help to someone in the future. I'm sure this could be significantly improved, I'm still a novice.

Some background on the project... I'll be using an ESP32 LCD display (the yellow PCB version) to display data, but has limited spare pins, so I'll be using a second PCB that does all the data collection from sensors (RS485, RS232, ADC, GPIO expanders, etc). I was initally going to try and send data over SPI, but as the screen is always using this it initally seemed problematic, so I opted for I2C. @TomGeorge At the moment the I2C lines are about 20cm, but will hopefully be <5cm in the final board to board connection.

This example modified from @Cardansan and help from @GolamMostafa shows examples of messages and comma separated data strings being returned to the master, with any extra garbage bytes removed, along with requests for structed data.

Master:

#include <Wire.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_DEV_ADDR 0x52

struct myData {
  byte y1 = 0;
  int y2 = 0;
  float y3 = 0.0;
  char str[10];
};
myData receivedData;  // Create an instance to store received data

// Write & send message to the slave
void sendtoSlave(String message, int slaveAddress) {
  Wire.beginTransmission(I2C_DEV_ADDR);        // Address the slave
  Wire.print(message);                         // Add data to buffer
  uint8_t error = Wire.endTransmission(true);  // Send buffered data
  if (error != 0)
    Serial.printf("endTransmission error: %u\n", error);  // Prints if there's an actual error
}

// Request data from the slave
String requestData(int slaveAddress, int messageLength) {
  String data = "";
  int bytesReceived = Wire.requestFrom(slaveAddress, messageLength);  // it makes the request here
  Serial.printf("request bytes received: %d\n", bytesReceived);

  if (bytesReceived > 0) {
    // Read bytes into a buffer
    char buffer[messageLength + 1];  // Add 1 for null terminator
    int index = 0;

    while (Wire.available() && index < messageLength) {
      buffer[index++] = Wire.read();
    }

    buffer[index] = '\0';  // Null-terminate the string

    data = String(buffer);  // Convert buffer to String
    return data;
  }
  return "failed request";
}

void requestDataStruc() {
  Wire.requestFrom(I2C_DEV_ADDR, sizeof(receivedData));  // Request the size of the struct

  // Read the data into the struct
  if (Wire.available() == sizeof(receivedData)) {
    Wire.readBytes((byte*)&receivedData, sizeof(receivedData));
  }

  // Print the received data
  Serial.print("Received y1: ");
  byte z1 = receivedData.y1;
  Serial.println(z1, DEC);

  Serial.print("Received y2: ");
  int z2 = receivedData.y2;
  Serial.println(z2, DEC);

  Serial.print("Received y3: ");
  float z3 = receivedData.y3;
  Serial.println(z3, 2);

  Serial.print("Received str: ");
  Serial.println(receivedData.str);  // Print the received string
  Serial.println("====================================");
  delay(1000);  //test interval
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Wire.begin(SDA_PIN, SCL_PIN);
  Serial.println("I'm the master.");
  delay(2000);
}

void loop() {
  String results = "";
  //Send specific string to Slave, so it prepares data for the Request
  sendtoSlave("Temperature", I2C_DEV_ADDR);  // Sending message to Slave
  Serial.println("Sending \"Temperature\"");
  delay(50);
  //Now that the data is prepared, here it polls it from the Slave
  results = requestData(I2C_DEV_ADDR, 16);  // Receive data from Slave
  Serial.print("Slave said: ");
  Serial.println(results);
  delay(5000);


  //Send specific string to Slave, so it prepares data for the Request
  sendtoSlave("DataString", I2C_DEV_ADDR);  // Sending message to Slave
  Serial.println("Sending \"DataString\"");
  delay(50);
  //Now that the data is prepared, here it polls it from the Slave
  results = requestData(I2C_DEV_ADDR, 24);  // Receive data from Slave
  Serial.print("Slave said: ");
  Serial.println(results);
  delay(5000);
  

  //Send specific string to Slave, so it prepares data for the Request
  sendtoSlave("DataStruc", I2C_DEV_ADDR);  // Sending message to Slave
  Serial.println("Sending \"DataStruc\"");
  delay(50);
  //Request the structured data
  requestDataStruc();
  delay(5000);
}

Slave:

#include "Wire.h"

#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_DEV_ADDR 0x52

struct myData {
  byte y1;
  int y2;
  float y3;
  char str[10];
};
myData dataToSend;

void onRequest()  //Anwsers to Master's "requestFrom"
{
  Serial.println("Request received. Sending buffered data.");  //Sending happens in the background
}

void onReceive(int len)  //Anwsers to Master's "transmissions"
{
  char buffer[50] = { 0 };      // Adjust buffer size as needed
  String requestResponse = "";  // to generate the request reply content
  String masterMessage = "";    // to save Master's message

  Serial.printf("received message[%d]: ", len);
  while (Wire.available())  // If there are bytes through I2C
  {
    masterMessage.concat((char)Wire.read());  //make a string out of the bytes
  }
  Serial.println(masterMessage);

  if (masterMessage == "Temperature")  //Filter Master messages
  {
    Serial.println("Preparing Temperature");
    requestResponse = "Getting hot";
    sendChar(requestResponse);
  } else if (masterMessage == "DataString") {
    Serial.println("Preparing Data");
    float rh = random(0, 1000) / 10.0;
    float tempC = random(150, 250) / 10.0;
    float tempF = random(600, 800) / 10.0;
    int ppm = random(0, 300);

    snprintf(buffer, sizeof(buffer),
             "%05.1f,%05.1f,%05.1f,%04d",  // rh, tempC, tempF, ppm
             rh, tempC, tempF, ppm         // Floats: rh, tempC, tempF
    );
    // Assign the formatted string to requestResponse
    requestResponse = String(buffer);  // Convert the buffer (char array) to a String
    sendChar(requestResponse);

  } else if (masterMessage == "DataStruc") {
    // Prepare data for the struct
    dataToSend.y1 = 1;
    dataToSend.y2 = 2;
    dataToSend.y3 = 3.4;
    strcpy(dataToSend.str, "Forum");

    Wire.slaveWrite((byte *)&dataToSend, sizeof(dataToSend));  // Send the entire struct as bytes
  } else {
    requestResponse = "message error";
    Serial.println("Master message not recognized.");
  }
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Wire.onReceive(onReceive);
  Wire.onRequest(onRequest);
  Wire.begin((uint8_t)I2C_DEV_ADDR, (int)SDA_PIN, (int)SCL_PIN, (uint32_t)100000);  //device address, sda, scl, frequency
  Serial.println("I'm the slave.");
  delay(2000);
}

void loop() {
}


void sendChar(String requestResponse) {
  requestResponse += "\n";  //string terminator

  //convert string to char[]
  int str_len = requestResponse.length();
  char char_array[str_len];
  requestResponse.toCharArray(char_array, str_len);

  Wire.slaveWrite((uint8_t *)char_array, str_len);  //Adds the string to Slave buffer, sent on Request
  Serial.println("Buffer ready. Data is:");
  Serial.println(char_array);
}

Glad you have it working.
You should explain why slaveWrite() is necessary for the ESP32 and not on other processors.