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?