arduino / ArduinoCore-samd

Arduino Core for SAMD21 CPU
GNU Lesser General Public License v2.1
468 stars 714 forks source link

I2C bus left in incorrect state when going to sleep #414

Open sslupsky opened 5 years ago

sslupsky commented 5 years ago

There is an issue with the I2C bus state when using low power deep sleep. The problem occurs when the sleeping. When the I2C master goes to sleep, the SCL and SDA lines are pulled low while the MCU is sleeping. This causes excessive power consumption during sleep because of the I2C pull up resistors. The next time the master wakes up, the SCL and SDA lines are pulled high and stay that way until the next I2C transaction. This cycle repeats. Here is the sketch. In this example, the rtc alarm event fires every 10 seconds.

void loop() {
  Watchdog.reset();
  if ( rtcTick >= heartBeatTick ) {
    heartBeatTick += HEARTBEAT_PERIOD;
    hostWire.beginTransmission(2);
    hostWire.write("Blink");
    hostWire.endTransmission();
  }
  LowPower.deepSleep();
}

void rtcAlarmEvent() {
// RTC alarm wake interrupt callback
  rtcTick += RTC_ALARM_PERIOD;
  rtc.setAlarmEpoch( rtcTick );  //  Set the time for the next RTC Alarm
}
sslupsky commented 5 years ago

I determined the source of this problem. When the Wire tells the I2C to stop the bus cycle, the command is issued and synced to the peripheral. The command to stop is non-blocking so program execution continues. If you subsequently go to sleep right away before the stop sequence has not been completed by the peripheral, the master still owns the bus and keeps SCL and SDA low. To fix this, you need to insert a short delay (about 20 us) after issuing the STOP command.

I'll submit a PR for this issue.

This issue was also posted on the Arduino forum:
https://forum.arduino.cc/index.php?topic=618735.0

sandeepmistry commented 4 years ago

@sslupsky did you open a pull request for this?

sslupsky commented 4 years ago

Not yet ... I was trying to think of a way to ensure the STOP is complete without using an arbitrary delay. At the moment, it seems to me there would need to be a check in the deepSleep() method. Any thoughts?

sslupsky commented 4 years ago

@sandeepmistry I think I have determined the underlying cause of the issue in the Wire library. When a command written (CTRLB CMD) to send a stop bit, the library sends the command but does not wait for the bus to return to the idle state. This occurs in four places:

https://github.com/arduino/ArduinoCore-samd/blob/2ca7f7531d4e4e54068bc584503b9e02e9033f3b/libraries/Wire/Wire.cpp#L94

https://github.com/arduino/ArduinoCore-samd/blob/2ca7f7531d4e4e54068bc584503b9e02e9033f3b/libraries/Wire/Wire.cpp#L132

https://github.com/arduino/ArduinoCore-samd/blob/2ca7f7531d4e4e54068bc584503b9e02e9033f3b/libraries/Wire/Wire.cpp#L142

https://github.com/arduino/ArduinoCore-samd/blob/2ca7f7531d4e4e54068bc584503b9e02e9033f3b/libraries/Wire/Wire.cpp#L149

I believe the proper way to address this issue is to add the following line after each of the above statements:

while (!sercom->isBusIdleWIRE()) {}

I'll submit a PR for this. Is there any feedback I should consider before I do so?

sslupsky commented 4 years ago

One concern I have is if there is any reason that the the bus would not return to idle after issuing the stop bit, the while loop would block indefinitely.

sslupsky commented 4 years ago

Another approach would be to add a check in the Arduino Low Power library before going to sleep that checks that the I2C bus is idle. To do the check however would require creating an API in the Wire library to do so.

robshep commented 1 year ago

Thanks for the efforts here. Did this get resolved in user code?