Home | pfodApps/pfodDevices | WebStringTemplates | Java/J2EE | Unix | Torches | Superannuation | | About Us
 

Forward Logo (image)      

Reliable Startup for I2C Battery Backed RTC
Why the Arduino Wire library is not enough.

by Matthew Ford 12th Nov 2022 (original 28th September 2014)
© Forward Computing and Control Pty. Ltd. NSW Australia
All rights reserved.

Update 12th Nov 2022 – added link to I2C_ClearBus.zip with I2C_ClearBus.cpp and I2C_ClearBus.h files, modified to work with nRF52832 low power
Update 1
st August 2017 – fix for error 3 ("I2C bus error. Could not clear. SDA data line held low").

Posted on https://github.com/esp8266/Arduino/issues/1025#issuecomment-319199959

Well, it's an old issue, but i had the same problem with a stuck I2C bus on startup (hangs at Wire.endTransmission). The SCL line was high, but SDA was being held low. I tried the solutions here and ended with error 3 ("I2C bus error. Could not clear. SDA data line held low").

Finally found a solution here and want to share it in the first Google result: I connect SDA parallel to a GPIO and check, if the error condition occurs. Then just put the GPIO (and SDA) to low for some time.
It however doesn't work to change the SDA pin directly to output and write LOW on my Feather nRF52832.

//Serial.print(digitalRead(PIN_SCL));    //should be HIGH
//Serial.println(digitalRead(PIN_SDA));   //should be HIGH, is LOW on stuck I2C bus

if(digitalRead(PIN_SCL) == HIGH && digitalRead(PIN_SDA) == LOW) {
      Serial.println("reset");
      pinMode(15, OUTPUT);      // is connected to SDA
      digitalWrite(15, LOW);
      delay(2000);              //maybe too long
      pinMode(15, INPUT);       // reset pin
      delay(50);
//Serial.print(digitalRead(26));    // HIGH
//Serial.println(digitalRead(25));  // HIGH
}

Hopefully, other people find this solution useful!
I've added a soft reset after this piece of code, sometimes it needs to restart up to 4 times, but always recovers the I2C bus.

Update: 17th November 2015 – now ESP8266 compatible

Added #if defined around disable of Atmel 2-wire so will compile and run on non-Atmel micros. Note: as at 4th March 2022, ESP32 already has most of this I2C clear bus code included in it Wire begin(), but ESP8266 does not include any of the I2C clear bus code.

Introduction

I2C based RTC (real time clocks) with battery back up are common. Examples are modules based on DS1307 and DS3231 chips. The DS3231 datasheet draws attention to a possible problem when the connected micro-processors, Arduino, restarts.

The I2C interface is accessible whenever either VCC or VBAT is at a valid level. If a micro-controller connected to the DS3231 resets because of a loss of VCC or other event, it is possible that the micro-controller and DS3231 I2C communications could become unsynchronized, e.g., the micro-controller resets while reading data from the DS3231. When the micro-controller resets, the DS3231 I 2 C interface may be placed into a known state by toggling SCL until SDA is observed to be at a high level. At that point the micro-controller should pull SDA low while SCL is high, generating a START condition.

The DS1307 resets the I2C bus when Vcc is removed which helps to over come this problem.

The Arduino Wire library does not do this recovery.

If a device on the I2C line is holding the SDA line low then this can lead to unexpected results and bad data. The problem with the DS3231 device is that the battery on the RTC keeps the that module running and the RTC continues to hold the last level for the SDA line, waiting for the next clock on the SCL line from the master, the Arduino. If this level is LOW then when the Arduino starts up again the Wire library cannot control the SDA line because it is being held low by the RTC, at least for the first few clocks. This results in bad data transfer.

Noise on the I2C line can also cause problems.

I2C_ClearBus() method

This I2C_ClearBus() method performs the following recovery steps:-

  1. Wait 2.5 secs. This is strictly only necessary on the first power up of the DS3231 module to allow it to initialize properly, but is also assists in reliable programming of FioV3 boards as it gives the IDE a chance to start uploaded the program before existing sketch confuses the IDE by sending Serial data.

  2. Check the SCL (clock) line is not being held low. If it is held low Arduno cannot become the I2C master. So return error code 1 if it is.

  3. Check that the SDA (data) is not being held low. If SDA is Low, then clock SCL Low for >5us then High>5us and check SDA input again. The >5us is so that even the slowest I2C devices are handled. Do this at most 20 times. If SDA is still Low return error code 3. Note: While clocking the SCL line a slave device may hold the line low “clock stretching” in order to preform some internal operation. If the SCL line is being held low for more then 2sec the return error Code 2.

  4. When SDA becomes High make SDA LOW while the SCL is High i.e. send an I2C Start or Repeated start control. When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus. A Repeat Start is a Start occurring after a Start with no intervening Stop.

  5. wait >5us and make SDA high i.e. send I2C STOP control.

  6. Wait >5us and restore SDA and SCL to tri-state inputs which is the default state on reset.

The above sequence will clear the I2C bus of any partial reads or writes that where in progress when the Arduino was reset or powered down.

An example sketch, I2C_ClearBus.ino, is shown below. This can also be used to clear the bus for other battery powered I2C devices.

A note about the code.

The I2C bus is an open collector bus held high by pullup resistors and pulled down by turning on transistors to ground. The Arduino's Atmel microprocessor implements an open collector output when in I2C (two-wire) mode, but in normal digital output mode (the mode used in this recovery method), the output is switched between +Vcc and GND. If the Arduino output is made High (+Vcc) while some other device is pulling the line to ground then excessive current can damage the Arduino microprocessor and perhaps the device also.

To avoid connecting the SCL and SDA Arduino pins to +Vcc, a somewhat unusual series of code is needed to switch between an Input with pullups and Output LOW. Atmel uses the same bit in the pin control register to control both the pullup resistors for Inputs and the output state for outputs. Another register controls whether the pin is an input or output. If the pin is currenly an input with pullups and you make it an output. The pin will become output HIGH. To avoid this you need to first remove the pullups from the input and then make the pin an output. It will then go directly from an Input (no pullups) to output LOW. This is what the code below does.

However one down side if this code is that there is a short period when the pin is an input without pullups, i.e. high impedence. This means I2C bus will float unless there are external pullup resistors on the lines. Floating buses are very susceptible to noise. So for best do NOT rely on the Arduino's pullup resistors but add external pullup resistors on the SCL and SDA lines. Many I2C devices already have these pullup resistors or have prevision to add them.

One final point. Some RTC libraries call Wire.begin() as part of their initialization code. However the I2C_ClearBus() method disables the I2C function so you need to call Wire.begin() after this to re-enable the I2C.

/**
 * I2C_ClearBus
 * (http://www.forward.com.au/pfod/ArduinoProgramming/I2C_ClearBus/index.html)
 * (c)2014 Forward Computing and Control Pty. Ltd.
 * NSW Australia, www.forward.com.au
 * This code may be freely used for both private and commerical use
 */

#include <Wire.h>

/**
 * This routine turns off the I2C bus and clears it
 * on return SCA and SCL pins are tri-state inputs.
 * You need to call Wire.begin() after this to re-enable I2C
 * This routine does NOT use the Wire library at all.
 *
 * returns 0 if bus cleared
 *         1 if SCL held low.
 *         2 if SDA held low by slave clock stretch for > 2sec
 *         3 if SDA held low after 20 clocks.
 */
int I2C_ClearBus() {
#if defined(TWCR) && defined(TWEN)
  TWCR &= ~(_BV(TWEN)); //Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly
#endif

  pinMode(SDA, INPUT_PULLUP); // Make SDA (data) and SCL (clock) pins Inputs with pullup.
  pinMode(SCL, INPUT_PULLUP);

  delay(2500);  // Wait 2.5 secs. This is strictly only necessary on the first power
  // up of the DS3231 module to allow it to initialize properly,
  // but is also assists in reliable programming of FioV3 boards as it gives the
  // IDE a chance to start uploaded the program
  // before existing sketch confuses the IDE by sending Serial data.

  boolean SCL_LOW = (digitalRead(SCL) == LOW); // Check is SCL is Low.
  if (SCL_LOW) { //If it is held low Arduno cannot become the I2C master. 
    return 1; //I2C bus error. Could not clear SCL clock line held low
  }

  boolean SDA_LOW = (digitalRead(SDA) == LOW);  // vi. Check SDA input.
  int clockCount = 20; // > 2x9 clock

  while (SDA_LOW && (clockCount > 0)) { //  vii. If SDA is Low,
    clockCount--;
  // Note: I2C bus is open collector so do NOT drive SCL or SDA high.
    pinMode(SCL, INPUT); // release SCL pullup so that when made output it will be LOW
    pinMode(SCL, OUTPUT); // then clock SCL Low
    delayMicroseconds(10); //  for >5us
    pinMode(SCL, INPUT); // release SCL LOW
    pinMode(SCL, INPUT_PULLUP); // turn on pullup resistors again
    // do not force high as slave may be holding it low for clock stretching.
    delayMicroseconds(10); //  for >5us
    // The >5us is so that even the slowest I2C devices are handled.
    SCL_LOW = (digitalRead(SCL) == LOW); // Check if SCL is Low.
    int counter = 20;
    while (SCL_LOW && (counter > 0)) {  //  loop waiting for SCL to become High only wait 2sec.
      counter--;
      delay(100);
      SCL_LOW = (digitalRead(SCL) == LOW);
    }
    if (SCL_LOW) { // still low after 2 sec error
      return 2; // I2C bus error. Could not clear. SCL clock line held low by slave clock stretch for >2sec
    }
    SDA_LOW = (digitalRead(SDA) == LOW); //   and check SDA input again and loop
  }
  if (SDA_LOW) { // still low
    return 3; // I2C bus error. Could not clear. SDA data line held low
  }

  // else pull SDA line low for Start or Repeated Start
  pinMode(SDA, INPUT); // remove pullup.
  pinMode(SDA, OUTPUT);  // and then make it LOW i.e. send an I2C Start or Repeated start control.
  // When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus.
  /// A Repeat Start is a Start occurring after a Start with no intervening Stop.
  delayMicroseconds(10); // wait >5us
  pinMode(SDA, INPUT); // remove output low
  pinMode(SDA, INPUT_PULLUP); // and make SDA high i.e. send I2C STOP control.
  delayMicroseconds(10); // x. wait >5us
  pinMode(SDA, INPUT); // and reset pins as tri-state inputs which is the default state on reset
  pinMode(SCL, INPUT);
  return 0; // all ok
}


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  int rtn = I2C_ClearBus(); // clear the I2C bus first before calling Wire.begin()
  if (rtn != 0) {
    Serial.println(F("I2C bus error. Could not clear"));
    if (rtn == 1) {
      Serial.println(F("SCL clock line held low"));
    } else if (rtn == 2) {
      Serial.println(F("SCL clock line held low by slave clock stretch"));
    } else if (rtn == 3) {
      Serial.println(F("SDA data line held low"));
    }
  } else { // bus clear
    // re-enable Wire
    // now can start Wire Arduino master
    Wire.begin();
  }
  Serial.println("setup finished");
}

void loop() {
  // put your main code here, to run repeatedly:
  // do Wire RTC stuff here.
}




The General Purpose Android/Arduino Control App.
pfodDevice™ and pfodApp™ are trade marks of Forward Computing and Control Pty. Ltd.


Forward home page link (image)

Contact Forward Computing and Control by
©Copyright 1996-2020 Forward Computing and Control Pty. Ltd. ACN 003 669 994