Getting rid of delay in I2C with multiple slaves

I have multiple i2c slaves that I am polling with an Arduino. A GPS, a barometer, and a combo accelerometer/gyroscope/magnometer. The Accel and GPS both play nicely where I send out the request and then the program is free to do other things. The Barometer, however, has a hard coded 10 ms delay where the entire program stops while a conversion is taking place in the barometer.

How can I remove this delay? I tried doing some rough testing where I manually sent the command to calculate the pressure then used a millis to calculate when 10 ms had passed (to simulate the delay(10) ) and then called the read function, but all I get it zeros now.

The original code was:

Pressure = =read_adc(MS5xxx_CMD_ADC_D2+MS5xxx_CMD_ADC_4096); // read_adc(0x18)

uint32_t MS5xxx::read_adc(unsigned char aCMD)
{
  unsigned long value=0;
  unsigned long c=0;
  
  send_cmd(MS5xxx_CMD_ADC_CONV+aCMD); // start DAQ and conversion of ADC data send_cmd(0x58)
   switch (aCMD & 0x0f)
  {
    case MS5xxx_CMD_ADC_256 : delayMicroseconds(900);
    break;
    case MS5xxx_CMD_ADC_512 : delay(3);
    break;
    case MS5xxx_CMD_ADC_1024: delay(4);
    break;
    case MS5xxx_CMD_ADC_2048: delay(6);
    break;
    case MS5xxx_CMD_ADC_4096: delay(10);
    break;
  } 
  send_cmd(0x00); // read out values
  _Wire->requestFrom(i2caddr, 3);
  c = _Wire->read();
  value = (c<<16);
  c = _Wire->read();
  value += (c<<8);
  c = _Wire->read();
  value += c;
  _Wire->endTransmission(true);
 
  return value;
}

byte MS5xxx::send_cmd(byte aCMD)
{
  _Wire->beginTransmission(i2caddr);
  _Wire->write(aCMD);
  uint8_t ret=_Wire->endTransmission(true);
  return ret;
}

I changed it to

if (baroStep == 0) {
  barometer.D1 = 0;
  barometer.send_cmd(0x58); // 0x58 is the command to read Pressure with
  pressPrevRead = currMillis; // CurMillis is variable to hold millis()
  baroStep = 1;
}

if (currMillis - pressPrevRead >= 10) {
  barometer.D1 = barometer.read_adc();
}

uint32_t MS5xxx::read_adc()
{
  unsigned long value=0;
  unsigned long c=0;
  
  send_cmd(0x00); // read out values
  _Wire->requestFrom(i2caddr, 3);
  c = _Wire->read();
  value = (c<<16);
  c = _Wire->read();
  value += (c<<8);
  c = _Wire->read();
  value += c;
  _Wire->endTransmission(true);
 
  return value;
}

As you can see, the only thing I changed was the hard delay of 10 ms into a timer waiting 10 ms but when using the timer I get no data back. During that time, I do poll the accel/gryoscope, but it has a different i2c address so the barometer should be ignoring that activity on the i2c bus, right?

For reference, the barometer is the MS5611 and it has to finish it's calculation prior to you issuing a read request or else the data will come back as 0. I've tried changing my timer from 0 ms up to 20 ms, but nothing works. Any ideas why this might be the case?

Get the barometer part to asynchronously collect its data. Say at intervals of 5 minutes, as a guess, and store the result so, when the I2C request comes in, it simply hands over the stored result. That is instead of calculating the result at the time of the I2C request.

1 Like

How about switching the MS5611 from I²C to SPI since it supports both protocols?

I tried that and I still get zero. I know this device really hates getting a second command while it's processing the first, maybe it considers any activity on the i2c bus as additional commands regardless of who it is addressed to? That might explain while a delay of 10 ms then immediately asking for the code words, but a timer where other devices communicate fails?

That is an interesting idea, but I think in the end I'd still have a situation where my program halted because of the delay 10. I was looking at getting an ESP32 and then splitting my code into two sections, a section where there are no delay commands, and a section without delays and then assigning each section to a dedicated core. It doesn't really solve the underlying issue of why I can't get rid of the delay, but it at least gets it out of the way.

Solving a problem by making it harder is the wrong solution.

I agree with @6v6gt
Use the Blink Without Delay for a timer for 1 or 5 minutes or so.
In one timer tick, start the sensor. In the next timer tick collect the data. And so on.
Store the data in a global variable, and return that to the Master when requested.

If you have a few sensors that needs some time, then you need a few separate millis timers. That is normal to collect data from sensor (it is normal for me :wink: ).

See this Issue: Wire.requestFrom() should not be followed by Wire.endTransmission(). · Issue #5 · Schm1tz1/arduino-ms5xxx · GitHub
Those few lines of code that you use can be improved. They are not okay, it's a bug, it's pretty bad, it's terrible. Boooh!
A Wire.requestFrom() should not be followed by Wire.endTransmission(). It causes an extra I2C session on the I2C bus. That might disturb the sensor.

I had seen that bug and was wondering if the extra endTranmission() might have been causing an issue, but where I broke the code up to go from a delay to a timer changes nothing except getting rid of that switch logic that chooses which delay to use. I have tried using a timer, I'll try upping the wait time to 1 or 5 minutes, but I have little hope it will solve the issue.

Can you show a full minimal sketch for only that sensor with a millis timer ?
The chances that such thing will work is 100%. It is how I collect data from sensors which need some time.

I see a lot of problems with the hardware of the I2C bus. Do you have a 5V I2C bus mixed with a 3.3V I2C bus or long wires ? I hope you don't use a flat ribbon cable.

I am not mixing 5v and 3.3v i2c buses, wire lenghts are no more than 1 ft. Not using flat ribbon cable (why would that one be a bad thing, out of curiosity)?

I will try and get minimal sketch written up tonight using the MS56xx library that I'm using. I'll test and see if I can get the timer to work when it is by itself and if so, if there is one specific device that is breaking functionality.

Talking about the wire.endtransmission() issue, when is the correct time to do an end transmission? For example, each time this library sends a command it does:


  _Wire->beginTransmission(i2caddr);
  _Wire->write(aCMD);
  uint8_t ret=_Wire->endTransmission(true);
  return ret;

does it need that endTransmission after each write?

Write data:
The Wire.beginTransmission() clears a buffer and resets variables.
The Wire.write() puts data in that buffer, it can be called more than once.
The Wire.endTransmission() does the I2C bus session and waits until everything has finished.

Read data:
The Wire.requestFrom() does the complete I2C bus session. It stores the received data in a buffer. It waits until everything has finished.

At Github, I have a alternative explanation of the Wire library.

The extra I2C bus session by a Wire.endTranmission() after a Wire.requestFrom() might not do something wrong with the sensor. The real cause of the problem is probably something else.

@abishur, could you take your time to read all of this.

About the flat ribbon cable:

When SDA and SCL are next to each other in a cable, then they are inductive and capacitive coupled.

The 'low' level of SDA and SCL is strong, but the 'high' level is weak because that is created with pullup resistors. If SDA goes low in a fast and strong way, then that can cause a glitch at SCL. The Slave might see that glitch as a extra clock pulse or a START or STOP condition.

The I2C bus can not deal with crosstalk between SDA and SCL. They are digital signals and the I2C bus was not designed to go into a cable. It is not that kind of "bus".
You can also not take a memory module out of your computer and connect the address "bus" and data "bus" with long wires.

Could you read page 60 of the official UM10204 document ?

So everyone knows this ? right ?
Wrong ! You are now part of the very select group of people that know this.
Everyone else puts SDA next to SCL, for example in the Qwiic, Stemma and Gravity I2C bus and also in the UEXT connector.

See also my page: How to make a reliable I2C bus

Thanks for the links and tips. It makes sense that a rapid alternating of SDA/SCL could cause some cross talk issues, kinda interesting that just about every chip I see put them right next to each other when you think about it.

So one thing led to another and I ended up re-writing the whole library and just made my own by the end of it, lol. I'd love any feedback you might have to offer on it: GitHub - abishur/ms5x: Library for digital pressure sensors MS5xxx by Measurement Specialties (MEAS). Tested with Arduino Uno/Nano/Mico, ESP8266 and ESP32. Long-term tests with MS5611 have been performed.

I'm calling it a fork of the arduino-MS5xxx library, but honestly I don't think much of the original code even remains at this point. With the work I did I can now use timers instead of delays and it is working quite smoothly.

You could set "Wire" as default parameter for the object. As far as I know, the "Wire" exists in every board.
Example: https://github.com/adafruit/Adafruit_BNO055/blob/master/Adafruit_BNO055.h#L283.

The 'true' parameter in Wire.endTransmission(true) is often omitted. The default is 'true' for a STOP condition.
The datasheet does not show a 'repeated start' as a option, so there is indeed always a STOP condition.

There are two calls to Wire.requestFrom(), but there is no defined value when the sensor is not connected and the temperature or pressure is still requested or when the I2C bus is bad.
The Wire.read() returns -1 when the sensor did not acknowledge to its I2C address.
You could add a check.

auto n = _Wire->requestFrom(i2caddr, 3);  // request 3 bytes
if( n == 3)       // received 3 bytes ?
{
  r = _Wire->read();
  value = (r<<16);
  r = _Wire->read();
  value += (r<<8);
  r = _Wire->read();
  value += r;
}
else
{
  // return zero, or set a flag that the data is not valid
  ..
}

Have you thought about self-heating ?
Some sensors produce heat when busy. Suppose the sensordata can be requested every 2 seconds, then request it every 20 seconds would prevent too much self-heating.

Good point about the overheating. I haven't seen much overheating while testing, but it is in a controlled environment. I'll separate the checkUpdates function out so that a different one can be used to periodically check if the process has finished.

I'll also add in a return condition (probably tomorrow morning) when the i2c bus is bad or sensor not connected. I actually did get some garbage values for that when I had my sensor disconnected and it still returned "readings" from the math using zeros as sensor values.

I appreciate the feedback on the i2c stuff. It's really helped me understand it.

Added the polling delay option, and a very basic check on the i2c reads (thank you for that tip). Someday I need to get a second sensor and add in an SPI communications option to round out the library, but for now I think it's good to go. I'll see about getting it added to the Arduino library manger, hopefully it will be helpful to someone else too.