NANO 33 BLE I2C problems (caused in mbed Wire.cpp) - read/write bit on I2C scan

Summary

Mbed Wire library - in contrast to AVR or ESP - implements the endTransmission() function in a way that a read() instead of a write() is performed when transmitBuffer is empty (no data to send). This avoids an ACK response on I2C scans and common device detection which rely on simple beginTransmission/endTransmission method.

I would like to discuss the problem here before opening an issue on the ArduinoCore-mbed Github page.

Initial Problem

Starting point was a MS5611 (pressure) sensor, which was not detected by I2C scanners and MS5611 library by Rob Tillaart, which in turn based on device detection with common beginTransmission/endTransmission. It was assumed that the cause was some kind of internal sleep of the sensor. So we added a write(0) between beginTransmission and endTransmission, which is a defined command for the sensor. This fixed the detection in MS5611 library and in I2C scanner scripts.

Further problems and Analysis

Testing with Adafruit MS8607 sensor (in fact it is two sensors in one: pressure and humidity) ended in the same result. Both sensors were neither detected by the I2C scanner (without write(0)) nor detected by Adafruits example sketches.

Inserting the write(0) again at least resulted in a detection of the pressure sensor on address 0x76, but not in a detection of the humidity sensor on address 0x40 (which doesn't know the 0x00 command).

Further investigations with a Logic Analyzer brought up following insights:

  • beginTransmission/endTransmission (without write() in between) always results in a "setup read" -> rw bit HIGH -> read -> results always in a NAK
  • insertion of write(cmd) results in an ACK only if the cmd is supported by the slave

MS5611 (pressure) on 0x77 - beginTransmission/endTransmission => NAK

MS8607 (pressure) on 0x76 - beginTransmission/endTransmission => NAK

MS8607 (humidity) on 0x40 - beginTransmission/endTransmission => NAK

MS5611 (pressure) on 0x77 - beginTransmission/write(0)/endTransmission => ACK + ACK

MS8607 (pressure) on 0x76 - beginTransmission/write(0)/endTransmission => ACK + ACK

MS8607 (humidity) on 0x40 - beginTransmission/write(0)/endTransmission => ACK + NAK

The problem is caused by the code in mbed Wire.cpp, which is located here (Windows 11, Arduino IDE 2.0.0-rc3) under following path:

C:\Users\username\AppData\Local\Arduino15\packages\arduino\hardware\mbed_nano\2.7.2\libraries\Wire\

The problem resides in endTransmission function, line 71-82. If there is no data to send (usedTxBuffer == 0), it does a master->read(). If there is data to send, it does a master->write().

uint8_t arduino::MbedI2C::endTransmission(bool stopBit) {
	#ifndef TARGET_PORTENTA_H7
	if (usedTxBuffer == 0) {
		// we are scanning, return 0 if the addresed device responds with an ACK
		char buf[1];
		int ret = master->read(_address, buf, 1, !stopBit);
		return ret;
	}
	#endif
	if (master->write(_address, (const char *) txBuffer, usedTxBuffer, !stopBit) == 0) return 0;
	return 2;
}

Conclusions

  • Setting the 8th bit (rw bit) is a matter of the I2C master (NANO 33 BLE). So the problem is not located on the I2C slave side (sensors).
  • It is likely that many "I2C device not found" problems with NANO 33 BLE are related to this code, rather than pullup or frequency issues.
  • Referring to the placed comment (// we are scanning, return 0 if the addresed device responds with an ACK) I guess this was intended as a feature in case of I2C bus scanning. The author maybe didn't have in mind that older platforms (e.g. AVR, ESP) never do a read, but do a write only, and therefore many I2C devices expect the write bit (which also corresponds to an address <= 0x7F, if interpreted as 8-bit address).

For example, Wire.cpp in AVR platform looks like this; no read() - just write():

uint8_t TwoWire::endTransmission(uint8_t sendStop)
{
  // transmit buffer (blocking)
  uint8_t ret = twi_writeTo(txAddress, txBuffer, txBufferLength, 1, sendStop);
  // reset tx buffer iterator vars
  txBufferIndex = 0;
  txBufferLength = 0;
  // indicate that we are done transmitting
  transmitting = 0;
  return ret;
}

Patch / Workarounds

I commentet out the read part. My mbed Wire.cpp looks like this now:

uint8_t arduino::MbedI2C::endTransmission(bool stopBit) {
      // never use read on I2C scanning, since many devices expect write
      // rw bit low (=write) corresponds to an address <= 0x7F, if interpreted as 8-bit address
      /*
      #ifndef TARGET_PORTENTA_H7
      if (usedTxBuffer == 0) {
           // we are scanning, return 0 if the addresed device responds with an ACK
           char buf[1];
           int ret = master->read(_address, buf, 1, !stopBit);
           return ret;
      }
      #endif
      */
      // we need a better handling of return value here
      // always sending 2 is inappropriate
      /*
      if (master->write(_address, (const char *) txBuffer, usedTxBuffer, !stopBit) == 0) return 0;
      return 2;
      */
      // this is still not perfect, because return value of master->write is not evaluated, but works for most cases
      if (master->write(_address, (const char *) txBuffer, usedTxBuffer, !stopBit) == 0) return 0;
      if (usedTxBuffer == 0) return 2;   // received NAK on transmit of address (without data)
      return 3;   // received NAK on transmit of data 
}

This works perfectly, as expected:

MS5611 (pressure) on 0x77 - beginTransmission/endTransmission => ACK

MS8607 (pressure) on 0x76 - beginTransmission/endTransmission => ACK

MS8607 (humidity) on 0x40 - beginTransmission/endTransmission => ACK

Also the Adafruit MS8607 example sketch works as expected now.

This is my I2C scanner script:

// I2C scanner for NANO 33 BLE / TwoWire

#include <Wire.h>     // external I2C bus
#define WireExt Wire  // WireExt = external I2C bus

extern TwoWire Wire1; // internal I2C bus
#define WireInt Wire1 // WireInt = internal I2C bus

void scan(TwoWire * wire, char channel[ ])
{
  
  Serial.print("Scanning I2C addresses on ");
  Serial.print(channel);
  Serial.print(" I2C bus");
  Serial.println();

  byte error, address;
  uint8_t nDevices = 0;   // counter for found devices

  for (address=0; address<128; address++)
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmission to see if
    // a device did acknowledge to the address.

    wire->beginTransmission(address);
    //wire->write(0x00);   // workaround until mbed Wire.cpp lib is fixed
    error = wire->endTransmission(true);

    if (error == 0)   // device found with ACK
    {
      Serial.print("0x");
      if (address<16)
      {
        Serial.print('0');  // add leading zero
      }
      Serial.print(address,HEX);
      nDevices++;
    }
    else if (error == 2)   // received NAK on transmit of address (without data)
    {
      Serial.print("....");
    }
    else   // any else code (should not happen)
    {
      Serial.print("..??");
    }
    Serial.print(' ');   // space after every device
    if ((address&0x0f) == 0x0f)
    {
      Serial.println();   // line break after every 16 addresses
    }
  }
  Serial.print("Scan completed, ");
  Serial.print(nDevices);
  Serial.print(" I2C device(s) found");
  Serial.println();
}


void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("I2C Scanner");
  Serial.print("Waiting for 3 seconds...");
  delay(3000);
  Serial.println();

  Serial.print("Initializing internal I2C bus ...");
  WireInt.begin();
  Serial.println();

  Serial.print("Initializing external I2C bus ...");
  WireExt.begin();
  Serial.println();
}


void loop()
{
  scan(&WireInt, "internal");
  delay(100);
  scan(&WireExt, "external");
  Serial.println("Waiting for 5 seconds...");
  delay(5000);
}

Discussion on a general solution

How to detect an I2C device if you don't know which device it is, on which address it resides and which commands are supported? MS8607 humidity sensor (0x40) is a good example: With the current implementation of endTransmission in mbed Wire.cpp it returns a NAK on read() and a NAK on write(0). One would need to test all possible commands from 0x00 to 0xFF an all possible addresses, getting slow and not knowing which effects they have each, worst case writing unwanted data to registers. It would be much better to get an ACK in the beginTransmission/endTransmission sequence.

In my opinion it's inadequate to hack workarounds in I2C scanners and in countless devices libraries. We need a write() in case there is no data to send, as in Wire.cpp of other platforms (e.g. AVR, ESP).

Any thoughts on this?

2 Likes

First of all many thanks for your extensive investigation :slight_smile:

There is no reason to perform any unwanted transmissions. A circumvention of problems in some broken hardware should be restricted to that hardware.

Read requests never can result in NACK because ACK is generated by the master which, in this case, receives data whether or not a slave provides any. A NACK only can be generated from I2C address transmission that is evaluated by a scanner program.

A well behaved I2C master implementation should abort any transmission if no slave responds to the address byte(s), except for a broadcast ("general call").

Thanks for your reply.

Which piece of hardware do you see as broken in this case? Master (NANO 33 BLE) or slaves?

What is your suggestion on this problem? Are you saying that modifying endTransmission() function in the suggested way is not sufficient or not the right place to solve the problem?

The current implementation leads to countless non working combinations of hardware or their libraries respectively.

I don't know. Can you tell hardware that requires a deviation from the original Arduino implementation?

If no such broken hardware exists then the modification of endTransmission() has to be reverted.

This issue is even broader than this.

My Raspberry Pi Pico in Arduino mode on top of Mbed using a Wokwi simulation with a I2C Scanner worked before, but no more.

I read that scanning for I2C addresses is a Arduino thing, but it is not. A Slave should respond to its address, regardless for reading or writing.
When the Master sets the read mode, the Slave is getting a byte ready. So the write mode seems better.

The AVR Wire library has a bug with a I2C Scanner in read mode (Sorry, I can't find the Issue that I made on Github). [EDIT] Found it: https://github.com/arduino/ArduinoCore-avr/issues/421.
I tried that, because some touch sensors have the write mode not implemented in hardware.

This pull requests added the read for Mbed, and excluded the Portia. That was to fix the I2C Scanner :face_with_spiral_eyes:

Are you saying that writing a I2C address with write mode with no data is not supported by Mbed ?

Exactly.

Issue #414 / PR #415 opened on ArduinoCore-mbed Github page.

2 Likes

I agree, this issue is even broader. After modification of Wire.endTransmission() (to do a write instead of read) all present sensors are correctly detected and communication works without glitches.

But since the modification my I2C scanner script (using beginTransmission-endTransmission method) detects ghost addresses, randomly distributed - without recognizable pattern, where there are no devices. This happens both on the external and on the internal I2C bus. The number of ghost addresses corresponds with the clock speed (very few on 50 kHz, many on 400 kHz). Regardless from clock speed, reading with a Logic Analyzer in parallel does not detect any ghost addresses.

I cross-checked with same MCUs, with another cables. The ghost addresses occur even without any external sensors/cables connected to the I2C pins! Weird...

That's normal behaviour without or too high pullup resistors on SCL/SDA.

Are you experienced with pullups on NANO 33 BLE?

I'm working throughout with 3,3 V and 100 kHz on the I2C bus.
The schematics file shows 2x 4k7 on SDA1/SCL1 (internal I2C bus).
For me it's not clear, if the external I2C bus has built-in pullups. From schematics I would say no, multimeter reading shows a resistance of around 10M.

Not sure if pinMode is applicable on analog pins, so I tried this (without any effect noticable):

pinMode(A4, INPUT_PULLUP);   // SDA
pinMode(A5, INPUT_PULLUP);   // SCL

I tried with external resistors (2x 4k7 and 2x 2k2) on SDA/SCL, without any effect.

I found two more hints here

and here

but I'm not sure about how to apply.

pinMode(PIN_ENABLE_I2C_PULLUP, OUTPUT);
P1_0 pin, which is the pullup for the I2C bus

If ghost addresses are an indicator for non present or too high pullup resistors on SCL/SDA, what else could I try if 2x 2k2 did not help? Just ignoring, because communication with present I2C devices is possible without glitches?

You should use a scope to view the exact timing of the I2C signals.

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