2

I had a problem communicating an nodeMCU with an Arduino Pro Mini with I2C.

To find the culprit, I finally wrote an Echo Server in Arduino: it receives a byte and responds the same byte. In the nodeMCU I wrote a test program: it sends bytes to the Echo Server and verifies the response, in addition to any error conditions in the Wire methods.

Both processors are connected GND-GND, VCC-VCC, SDA-SDA and SCL-SCL. SDA and SCL have 10K pull-up resistors. Both are connected via USB to my PC, and I have two serial monitors to trace the execution.

In the tester program line 38 there is a delay(1). If you comment it out, some requestFrom() fails randomly with return value != 1, but the Echo Server still received the byte.

As ATmega328 included TWI in the silicon, I can't understand what is failing here and why delay(1) seems to solve the problem. What I'm missing here?

The tester program

/*
 * Echo Server Test
 * Send one byte to the Echo Server and read it back.
 * Do it for bytes from 0x01 to 0xDD
 */
#include <Wire.h>

#define SLAVE_ADDRESS  42
byte car = 1; // car to be send to the Echo Server.

void setup() {
    Serial.begin(9600);
  // We have a nodeMCU con I2C on pins D5 and D6.
    Wire.begin(D5, D6);
    delay(1000);
  Serial.println("");
    Serial.println(" Echo Server Tester");
    Serial.println("---------------------");
    Serial.println("###  Sent   Received");
    delay(1000);    
}

/*
 * Send one byte in each loop().
 */
void loop() {
    byte status;

    if(car > 0) {
        do {
            Wire.beginTransmission(SLAVE_ADDRESS);
            int written = Wire.write(car);
            status = Wire.endTransmission();

            switch(status) {
            case 0:
        delay(1); // Comment this out and watch requestFrom() fails.
                if(Wire.requestFrom(SLAVE_ADDRESS, 1) == 1) {
                    byte r = Wire.read();
                    printChar(car, r);
                    car++;
                } else {
                    Serial.println("error in return");
                    status = 4;
                }

                break;

            case 1:
                Serial.println("Data too long");
                break;

            case 2:
                Serial.println("NACK on address");
                break;

            case 3:
                Serial.println("NACK on data");
                break;

            case 4:
                Serial.println("Some error");
                break;
            }
        }

        while(status != 0);
    }
}

/*
 * Utility: print in bytes sent and received 
 */
void printChar(byte s, byte r) {
    Serial.print(s);
    Serial.print(" ");
    printBits(s);
    Serial.print(' ');
    printBits(r);

    if(r != s) {
    //  Mark invalid responses.
        Serial.print(" *");
    }

    Serial.println();
}

/*
 * Print a simple byte in binary
 */
void printBits(int car) {
    byte bit = 0x80;

    for(int i = 0; i < 8; i++) {
        int digit = car & bit;
        Serial.print(digit ? '1' : '0');
        bit >>= 1;
    }
}

The Echo Server program

/*
 * I2C Echo Server
 * Receive one byte from Master, echo the same byte to Master.
 */
#define MY_ADDRESS  42

#include <Wire.h>

volatile byte received;
volatile int count = 0;

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

  Wire.begin(MY_ADDRESS);
  Serial.println("--- Echo Server ---");

  Wire.onReceive(receiveCommand);
  Wire.onRequest(sendAnswer);
}

void loop() {
  static int lastCount = 0;

  if (count != lastCount) {
    Serial.print(lastCount + 1); Serial.print(" ");
    printChar(received);
    lastCount++;
  }
}

/*  
 * Receive a byte from Master  
 */
void receiveCommand(int howMany) {
  received = Wire.read();
  count++;
}

/* 
 * Send a byte to Master 
 */
void sendAnswer() {
  Wire.write(received);
}

/*
 * Utility print byte received/sent
 */
void printChar(byte car) {
  Serial.print("Char ");
  byte bit = 0x80;
  for (int i = 0; i < 8; i++) {
    byte digit = car & bit;
    Serial.print(digit ? '1' : '0');
    bit >>= 1;
  }
  Serial.println();
}
2
  • the TWI interface needs to wait a whole cycle before it requests again, otherwise the clock and buffer can get messed up.
    – tuskiomi
    Jul 25, 2017 at 13:41
  • THIS fixed my I2C problems, delay(1) I had not seen this mentioned anywhere previously, so thanks for the question and the answers as I have been going in circles why my first 2 reads worked but not my second read 2, although commenting out the first 2 reads then let the second 2 behave correctly. I will now find out the clock cycle and change delay(1) to a micros delay. Feb 24, 2020 at 4:11

1 Answer 1

2

The 1 millisecond is not needed, try delayMicroseconds with 50 or maybe 20 µs.

The Arduino as a slave is totally different than for example a sensor. The Arduino as a slave uses a combination of hardware and software. The onRequest and onReceive functions are executed while the Arduino keeps the SCL line low. That is called clock pulse stretching.

With an Arduino Uno (ATmega328P microcontroller) connected to an Arduino Uno, this delay problem between a write and read on the I2C bus existed for a long time. A few years ago it was solved. It was a bug, a real bug. That delay is no longer needed.
There are still ways to make it unreliable, for example by disabling the interrupts for too long in the slave.

I read that is still exists with the Arduino Due. With the Due as a master and for example an ATmega328P (at 3.3V) as a slave, the Due still needs a delay.

The nodeMCU and the Raspberry Pi might still have some problems when an Arduino is used as a slave. The Arduino as a slave should at least run at 3.3V or a I2C level shifter should be used.
I don't know if the nodeMCU Wire library supports clock pulse stretching.
That delay might still be needed. Even then, I don't know how reliable the communication will be.

The Wire.endTransmission is the function that transmits the data (not the Wire.write). If that is followed too soon by a Wire.requestFrom and there is a bug in the Wire library, that can cause troubles.