ESP32 I2C Corruption

Hello All,

I have been working on a project for a while now where I have 3 ESP32's connected to each other via I2C (1 Master, 2 Slaves)

I am having issues with the slave boards not receiving the full communication (Only receiving 2 digits instead of 3)

I believe it has to do with the Wire.onreceive event getting interrupted by the code getting executed on the other core, however, as I just stated, the actual code itself is being run on the other core so I dont understand why it would be interrupting the onreceive

Instead of receiving "060" from the master, the slaves will rarely receive "06" or "0" (Which will then lead to an invalid data length message)

Here is a rough diagram of how everything is connected:
Untitled

Here are some examples of the code:

Master:

void inputHandler(void* parameter) {
  while (!globals.shutdown) {
    if (digitalRead(PIN1) == 0) {
      if (digitalRead(PIN2) == 1) {
        globals.right.desired = 15;
        globals.left.desired = 1;
      } else if (digitalRead(PIN3) == 1) {
        globals.right.desired = 1;
        globals.left.desired = 15;
      } else {
        globals.right.desired = 1;
        globals.left.desired = 1;
      }
    } else if (digitalRead(PIN1) == 1) { 
      if (digitalRead(PIN2) == 1) {
        globals.right.desired = 25;
        globals.left.desired = 2;
      } else if (digitalRead(PIN3) == 1) {
        globals.right.desired = 2;
        globals.left.desired = 25;
      } else {
        globals.right.desired = 2;
        globals.left.desired = 2;
      }
    }

    String txr = "";
    String txl = "";

    if (globals.web.style < 10) {
      txr += "0";
      txl += "0";
    }

    txr += String(globals.web.style);
    txl += String(globals.web.style);

    switch (globals.right.desired) {
      case 1:
        txr += "0";
        break;
      case 15:
        txr += "1";
        break;
      case 2:
        txr += "2";
        break;
      case 25:
        txr += "3";
        break;
    }

    switch (globals.left.desired) {
      case 1:
        txl += "0";
        break;
      case 15:
        txl += "1";
        break;
      case 2:
        txl += "2";
        break;
      case 25:
        txl += "3";
        break;
    }

    // Perform data validity check
    if (!isValidTransmittedData(txr) || !isValidTransmittedData(txl)) {
      Serial.println("Invalid TX Data");
    } else {
      // Right (Slave 1)
      Wire.beginTransmission(0x0A);
      Wire.write(txr.c_str());
      Wire.endTransmission();

      // Left (Slave 2)
      Wire.beginTransmission(0x1A);
      Wire.write(txl.c_str());
      Wire.endTransmission();
    }

    delay(10);
  }
}

Slave ReceiveEvent:

void receiveEvent(int byteCount) {
  String receivedData = "";  // Variable to store the received data
  
  while (Wire.available()) {
    char c = Wire.read();  // Read the received data as a character
    receivedData += c;  // Concatenate the character to the receivedData string
  }
  
  // Validate the received data
  if (receivedData.length() != 3) {
    Serial.println("Invalid data length");
    return;
  }
  
  for (int i = 0; i < receivedData.length(); i++) {
    if (!isdigit(receivedData[i])) {
      Serial.println("Invalid data format");
      return;
    }
  }
  
  // Extracting individual numbers
  int digit0 = receivedData[0] - '0';
  int digit1 = receivedData[1] - '0';
  int digit2 = receivedData[2] - '0';
  
  int combinedDigits = digit0 * 10 + digit1;
  
  // Processing the extracted numbers
  globals.style = combinedDigits;

  switch (digit2) {
    case 0:
      globals.desired = 10;
      break;
    case 1:
      globals.desired = 15;
      break;
    case 2:
      globals.desired = 20;
      break;
    case 3:
      globals.desired = 25;
      break;
    default:
      Serial.println("Invalid value for digit2");
      return;
  }

   if ((globals.desired != globals.running && globals.running < 3) || globals.style != globals.activestyle) {
    globals.halt = true;
  }

  //Serial.println(receivedData);
  
  delay(5);
}

Slave Running Function:

void anims::style6::mode10() {
  //bool isFirstInstance = true;
  while (!globals.shutdown) {
    fadeInOut(8, 0, true, 30);
    if (checkWithDelay(1)) { return; }
    fadeInOut(7, 8, false, 20);
    if (checkWithDelay(1)) { return; }
    fadeInOut(6, 7, false, 15);
    if (checkWithDelay(1)) { return; }
    fadeInOut(5, 6, false, 15);
    if (checkWithDelay(1)) { return; }
    fadeInOut(4, 5, false, 15);
    if (checkWithDelay(1)) { return; }
    fadeInOut(3, 4, false, 15);
    if (checkWithDelay(1)) { return; }
    fadeInOut(2, 3, false, 10);
    if (checkWithDelay(1)) { return; }
    fadeInOut(1, 2, false, 10);
    if (checkWithDelay(1)) { return; }
    fadeInOut(0, 1, false, 10);
    if (checkWithDelay(1)) { return; }
    fadeOut(0, 60);
    if (checkWithDelay(190)) {
      return;
    }
    //isFirstInstance = false;
  }
}

void fadeInOut(int ring, int prev, bool isFirstInstance, int speed) {
  // Number of steps for fading
  int numSteps = speed; // Adjust the number of steps for faster fading

  // Fade in and fade out
  for (int i = 0; i < numSteps; i++)
  {
    for (const int & value: it.LEDS_BOTH(ring, ring, ring, ring, 1)) {
      int brightness = map(i, 0, numSteps - 1, 0, 255 - (31.9 * ring));
      globals.leds[value] = CRGB(brightness, brightness, brightness);
    }

    // Fade out the previous ring, excluding the first instance
    if (!isFirstInstance)
    {
      for (const int & value: it.LEDS_BOTH(prev, prev, prev, prev, 1)) {
        int brightness = map(numSteps - i - 1, 0, numSteps - 1, 0, 255 - (31.9 * ring));
        globals.leds[value] = CRGB(brightness, brightness, brightness);
      }
    }

    FastLED.show();
  }
}

int checkWithDelay(int time) {
  for (int i = 0; i <= time; i++) {
    delay(5);
    if (globals.halt) {
      globals.halt = false;
      return 1;
    }
  }
  return 0;
}

I know I have kinda just dumped this, I just cannot figure out why I am getting interruptions to the receive event running on the other core.
For reference, I have a different "Running Function" which (almost) never gives me data corruption, So it seems to be an issue with this function.

Any help would be greatly appreciated
Cheers

I2C needs pull-up resistors. Not resistors in series with the SDA / SLC lines!!!

2 Likes

Hello OM k4hvh

Take a view to gain the knowledge.

1 Like

Thanks for your response!

Could you explain the difference and how I would need to fix it.
I was under the impression that you only needed 1 set of resistors

Thanks

That's correct. But they need to be pull up resistors, not inline. The document @paulpaulson provided explains it all. Have you read it?

Yes I did try to read it haha. But ngl, don't really have a clue what I'm looking at.
This is my first project doing this sorta stuff so am not entirely sure of everything going on

Surely you can see the difference in resistor placement between this diagram and yours?

Oh ok, So I should have the resistors connected to a power supply, then attach that in between components.

Thanks for your help bro

PS: Quick question about that, do you have to use a seperate resitors for each "segment" of the circuit? Or can the same pair be used for all "segments"?

Your setup only requires two resistors. One on SDA and one on SCL. The exact placement location isn't critical for reasonable bus lengths.

Welcome to the forum.

Do you know a working example where the ESP32 is a Slave ? If you can not find that, then perhaps you should avoid it.

Did you know that the ESP32 has something special ? https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/i2c.html#i2c-slave-mode
But even with that, I don't know how well the ESP32 works as a I2C Slave.

We don't send text over the I2C bus. The I2C bus works best with small packages of a fixed size of binary data.

You use a String object in the receiveEvent(), but the String object allocates and frees memory from the heap. Avoid such things in the receiveEvent().

You also use Serial function in the receiveEvent(). Avoid that as well.

The I2C bus is not a good bus to communicate between processors. It is to get a few bytes from a sensor.

Can you tell why you have decided to use multiple ESP32 boards ?
If you can do your project with a single ESP32, that would be so much better. The ESP32 runs FreeRTOS, so you can create tasks with only a little extra code.
Whatever your three ESP32 boards are doing, you can do the same thing with three tasks.

Hey mate thanks for the response.

Using xTaskCreatePinnedToCore() with the FastLED library causes "Thread Duplication" in my testing.
Even if I only create 1 thread for an animation, there appears to be 2 of that thread running (This can be seen with the attached leds as 2 seperate animations appear to overlap each other)

If you would like I can code up and example and show

What does any of that have to do with the concerns raised by @Koepel ... actual need for multiple processors? The appropriateness of using I2C for this application?

I was explaning why I can't just use a single processor for the application, instead having to seperate it onto seperate boards (IRT his last point) (And hoping someone might know of this issue)

So your real question is how to run multiple tasks with FastLED ?
That is a single problem that we should try to solve. Using the I2C bus creates a dozen of new problems and each of those problems create a dozen more :scream:

Should the FastLED with multiple tasks somehow fail, then the ESP32 has Serial2. So you always have something to fall back to (multiple boards via Serial/UART).

On the ESP32, all the Wifi things run on Core0 and all the Arduino things run on Core1.
Let Core0 do what Core0 is doing, and use only Core1.
You only need the normal 'xTaskCreate()'.

Can you tell more about your project ? Please give us a broader view.
How many ledstrips do you want to control independently ?

Clarification:

I'm skeptical of the need to have FastLED running in multiple tasks to update the LED strip(s). If you're using WS2812B-type LEDs, their timing requires FastLED to disable interrupts while the data is pumped out to the LEDs. Thus, task switching won't take place during that time. So, no advantage to putting different strips in different tasks over sequentially in the same task.

1 Like

I2C Bus is a byte oriented network/protocol. It has no idea if the submitted data is an ASCII coded character or a natural binary coded byte. It will transmit/received faithfully as long as it is an 8-bit data and it can handle as many as 32-byte data in one GO. For example:

ESP32S-I2CMaster Sketch:

#include<Wire.h>

void setup()
{
  Serial.begin(9600);
  Wire.begin();  //defaulr SDA = D21, SCL = D22; 2.2k pull-up with each line
  //--------------

}

void loop()
{
  Wire.beginTransmission(0x23);
  Wire.print("Hello ESP32S Slave!");
  Wire.endTransmission();
  delay(1000);
}

ESP32S-I2CSlve Sketch:

#include<Wire.h>
char myMsg[10];
int i = 0;
volatile bool flag = false;

void setup()
{
  Serial.begin(9600);
  Wire.begin(0x23);
  Wire.onReceive(receiveEvent);
}

void loop()
{
  if(flag == true)
  {
    Serial.println(myMsg);
    flag = false;
  }
}

void receiveEvent(int howMany)
{
  for(i = 0; i < howMany; i++)
  {
    myMsg[i] = Wire.read();
  }
  myMsg[i] = '\0';  //null charcater
  flag = true;
}

Slave Screen

Hello ESP32S Slave!
Hello ESP32S Slave!
Hello ESP32S Slave!
Hello ESP32S Slave!
Hello ESP32S Slave!

ESP32S-Master/ESP32S-Slave are behaving exactly same as UNO-Master/UNO-Slave. Then, what is the advantage of using ESP32S instead of UNO?

ESP32 has built-in hardware RMT that can send data to NeoPixels without CPU intervention,
but RMT hardware access has to be synchronized between tasks, so it does not solve all problems.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.