ESP32 slave I2C responds master request with previous value

As the title says, I encountered a strange issue while doing a PoC using an Arduino UNO R2 as the I2C master and an ESP32 as the I2C slave.

I initially found ESP32 documentation suggesting the use of Wire.slaveWrite, but the sample code they provided seemed certified retarded. I replaced all Wire.write calls with Wire.slaveWrite in my Wire.onRequest handler, but it did not solve the issue.

The order of events shown in the slave’s serial console debug output looked correct. However, the master’s serial console—and even the I2C oscilloscope—showed the previous response from the slave.

Interestingly, when I moved the Wire.onRequest logic to execute immediately after Wire.onReceive, it magically fixed the stale slave response seen by the master. Now I’m scratching my head trying to understand why.

AI suggestions didn’t help either—it proposed many incorrect solutions, and none of them fixed the problem.

#include <Wire.h>

#define I2C_SLAVE_ADDR 0x55
#define SDA_PIN 16
#define SCL_PIN 17

uint8_t volatile receivedValue = 0;

void setup() {
  Serial.begin(9600);
  Wire.begin(I2C_SLAVE_ADDR, SDA_PIN, SCL_PIN, 0);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
  Serial.println("Simple I2C Slave Ready");
}

void loop() {
  delay(100);
}

void receiveEvent(int numBytes) {
  if (Wire.available()) {
    receivedValue = Wire.read();
    Serial.print("[SLAVE] Received: 0x");
    Serial.println(receivedValue, HEX);

    //
    // Move from requestEvent
    //
    uint8_t responseValue = receivedValue + 0x50;
    //Wire.write(responseValue);
    Wire.slaveWrite(&responseValue, 1);
    Serial.print("[SLAVE] Sending back: 0x");
    Serial.print(receivedValue, HEX);
    Serial.print(" + 0x50 = 0x");
    Serial.println(responseValue, HEX);
    
    // Clear any remaining bytes
    while (Wire.available()) {
      Wire.read();
    }
  }
}

void requestEvent() {
// move to receiveEvent!!!
}

Boring I2C master in UNO R2

#include <Wire.h>

// Arduino UNO I2C pins:
// SDA: Pin A4
// SCL: Pin A5

const int SLAVE_ADDRESS = 0x55;
int counter = 1;

void setup() {
  Serial.begin(9600);
  Wire.begin(); // Initialize as I2C master

  Serial.println("Arduino UNO I2C Master Started");
  Serial.println("Sending counter 1-100 to slave at address 0x55");
  Serial.println("--------------------------------------------");
}

void loop() {
  if (counter <= 100) {
    // Send counter value to slave
    Wire.beginTransmission(SLAVE_ADDRESS);
    Wire.write(counter);
    Wire.endTransmission();

    delay(50); // Small delay to allow slave to process

    // Request echo back from slave
    Wire.requestFrom(SLAVE_ADDRESS, 1);

    if (Wire.available()) {
      int echo = Wire.read();
      Serial.print("[MASTER] Sent: 0x");
      Serial.print(counter, HEX);
      Serial.print(" | [SLAVE] Echo: 0x");
      Serial.println(echo, HEX);
    } else {
      Serial.print("[MASTER] Sent: 0x");
      Serial.print(counter, HEX);
      Serial.println(" | [SLAVE] No response");
    }

    counter++;
    
  } else {
    // Reset counter after reaching 100
    counter = 1;
    Serial.println("--------------------------------------------");
    Serial.println("Counter reset to 1");
    Serial.println("--------------------------------------------");
  }
  delay(100); // Wait 500ms between transmissions
}

1. This is the format shown below in whcih I write sketches for the Master and Slave using 2C Bus. Master sends: 1 2 3 ..... 100 to Slave at 1sec interval. Master expects to receive 1 + 0x5 (81), 82, 83, ..... 180 from Slave. Note that I2C is a half-duplex nework; so, answer from Slave will be delayed for the time Slave takes to receive 1 2 3, .....100.

Master Sketch: (untested)

#include<Wire.h>
#define slaveAddrs 0x55
byte counter = 1;

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

void loop()
{
    while(counter <= 100)
    {
        Wire.beginTransmission(slaveAddrs);
        Wire.write(counter);
        Wire.write(0x20);        //ASCII code for space
        Wire.endTransmission();
    } 

    counter = 1;
    delay(1000);
    //-----------------------
    Wire.requestFrom(slaveAddrs, 199);  //100 + 99 spaces
    for(int i = 0; i < 199; i++)
    {
        byte y = Wire.read();
        Serial.print(y)
    }   
    Serial.println()
}

Slave Sketch: (untested)

#include<Wire.h>
#define slaveAddrs 0x55
#define SDA_PIN 16
#define SCL_PIN 17
byte myData[250];
volatile bool flag = false;

void setup()
{
   Serial.begin(9600);
   Wire.begin(slaveAddrs, SDA_PIN, SCL_PIN, 0);
   Wire.onReceive(receiveEvent);
   Wire.onRequest(sendEvent);
 }

void loop()
{
     if(flag == true)
     {
         for(int i = 0; i < 199; i++)
         {
            Serial.print(myData[i]);
        }   
        flag = false;
     }
}

void receiveEvent(int howMany) //howMany = 199
{
     for(int i = 0; i < howMany; i++)
     {
        myData[i]  y = Wire.read();
     }   
     flag = true;    //do printing in the loop() function and NOT here
}

void sendEvent()
{
    for(int i = 0; i < 199; i++)
    {
        byte y = myData[i] + 0x50;
        Wire.write(y);
     }   
}

2. Why is it Arduino UNO R2 and NOT Arduino UNO R3?

This is a completely normal behavior. A onReceive is a callback, called automatically when a request comes in from the master. It sends to the master a value that is ALREADY in the buffer—that is, the value from the previous callback call.
The code you execute in the receiveEvent() doesn't send the data to the master, it only prepares the value for the next request and places it in the buffer. See the documentation about the Wire.slaveWrite() method - it doesn't send anything; it only places the value in the buffer. The value is sent on the next call.

The receiveEvent is the argument in the OnReceive() function in the Slave sketch. As I understand, when all the data bytes within begin-end/Transmission() session at the Master side arrive to the Slave's buffer, the Slave goes to the receiveEvent() sub routine and reads the data bytes from the buffer into variables and process them.

When a request comes from Master to Slave to get data from Slave, the Slave goes to the requestEvent() sub routine and stores data into buffer for onward transmission to the Master over the SDA line of the I2C bus.

First, you make the problem more complicated. Why returns a vector rather than a scalar value. Second, the sample you wrote is exactly the problem where the master got the stale value.

I attached the decode signal from my oscilloscope: while the master writes 0x4A, I would expect the master reads 0x9A. If you do in your way, you will get the previous value, instead.

What is the next call in your context? Wire.onRequest?

From the ESP-32 arduino doc on slaveWrite, I guess it means the master request the slave for response. This is odd. I'd think Wire.onRequest is the place where you write your response. Now I don't see the point to do anything there. Is it ESP32 specific quirk? The ESP32 doc suggests it only applies to ESP 32.