DS3231 Timing Issues Under Frequent I2C Reads

I have been encountering some odd phenomena using an Adafruit DS3231 RTC. I have the square wave output set to 1024 Hz and have that connected to a pin on the Arduino Due as an interrupt to count pulses and keep track of time. Sounds simple, right?

Doing some testing and benchmarking of the timing, I noticed that the square wave output frequency will change slightly if I try to read from the RTC repeatedly, without any delays. I would expect this output to be rock solid. Note that this code worked fine on the Mega2560 - the Due has proved to be more finicky.

Here's the lines of code that are the culprit. Without printing it all out, I am trying to determine when a new second rolls over on the RTC by repeatedly reading the current second from the RTC and comparing that with the current value in the code:

newSecond = Clock.getSecond();
  
  if (currentSecond != newSecond ) { // RTC second has rolled over
  // Do some counting checks, print results to the serial port, etc.
  }

It's that first line to read the current second from the RTC that causes issues. Without a delay of at least ~10ms between repeating that call, the square wave output frequency will vary. I have confirmed this via oscilloscope and via pulse counts on the arduino itself. Both show a small, but noticeable increase in frequency of about 0.2% or so and it is rather erratic - swaying between 1024 Hz and 1027 Hz or so. Adding in a delay or commenting out the function with this code results in consistent timing over long periods of time, as expected. Interestingly enough, I also see the same behavior on the raw 32khz oscillator pin output as well: its frequency will vary slightly during repeated, high speed calls to the RTC, just like the square wave output pin - so it seems the oscillator itself is the culprit. I've also tried it on a different Due with the same results.

Any ideas on what could be causing this? Chip heating up due to repeated calls? Power supply issue? Why would it occur on the Due but not on the Mega?

There are times where I will want to be able to sync up some counts to the rollover of a new second on the RTC and I'd rather not have to impose a delay as large as 10ms to do so.

The nuclear option is to count from the 32khz pin and then use the alarm function on the square wave pin to set an alarm every second and use a second interrupt from that to determine a rollover, but that sounds painful and would involve a large amount of code changes.

Why would it occur on the Due but not on the Mega?

Different voltage, maybe faster I2C. You failed to provide the complete sketch as well as a wiring diagram, so we have to guess what you might have done.

Try it with a SAMD21 (Arduino Zero, M0, MKR, and so on) or with the SDA1 and SCL1 pins of the Due. I would not only like to see a complete sketch and a wiring diagram, but also a sharp photo of the Arduino Due that you have.

The Due has a problem with the pullup resistors for SDA and SCL. They are too low, and sometimes other resistors are used that are even lower (1k). That could be outside the specifications of the I2C bus and perhaps also outside the specifications of the DS3231.

You could have found a serious problem in the DS3231, but I assume that someone else would already have reported it. The Due with its I2C bus has already enough problems on its own, so I would start to look there.

I'll try and get a schematic in the next day or two and strip down my code (code currently has bits from the main project that aren't relevant). But it's pretty dead simple right now:

Benchtop power supply feeding the Due, 3.3V from Due going to the RTC, SDA and SCL going to pin 20 and 21 on the Due, and the square wave output going to pin 2 on the Due with an interrupt on that pin. An external 10k pullup is on the square wave output pin (although I had the same problem when using the internal pullup resistor as well). Wires are about 4" long and the RTC is the only device connected.

So far I've tried two different Due's. One is a legit Arduino branded one (say's "Made in Italy by Arduino.org" on it and the pin headers are labeled and nice and straight) and the other was something more generic - both had the same result.

I've tried it at 100 khz bus speed and 400 khz with the same results (bus speed verified with scope as well)

I've even looked at the I2C bus with my scope and the rise time and noise were much better than I expected. Although the scope itself seems to occasionally lock up the bus and cause issues as well when decoding things.

I think I'll try putting it on SDA1 and SDA2 with external pullup resistors. Is 4k7 a good starting point? Or maybe use some pots for now? I'm also looking at using a TCA9617B to clean things up as I do want to add more devices. I'll probably go the nuclear route in the end as well and use the 32Khz output and then just set alarms for every second as the consistency of this timing is pretty important to my project.

Sad to see that the I2C on the Due has so many issues - the rest of its capabilities are quite nice.

Here’s the code:

/*************************************************************************************************/
#include <Wire.h>                // I2C Library
#include <DS3231.h>              // RTC Library
#include <sdios.h>               // needed for cout serial functionality
/*************************************************************************************************/

// Real time clock and timing variables
DS3231 Clock;
const uint16_t  rtcFreq = 1024, samplePeriod = 2;   // 2 Hz test cycle frequency
volatile uint16_t rtcCount = 0;
volatile uint32_t tElapsed;
uint32_t currentSecond = 0;

// Interrupt & switch variables
const uint16_t rtcInterruptPin = 2;
volatile boolean rtcTrigger = false;

ArduinoOutStream cout(Serial);

/*************************************************************************************************/
void setup() {
  Wire.begin();                   // Start the I2C interface
  Wire.setClock(400000);          // Same issues at 400khz and 100khz bus speed
  Serial.begin(250000);           // Start the serial interface
  
  // RTC Oscillator (Enabled, Run off battery, Freq - 0 = 1 Hz)
  Clock.enableOscillator(true, false, 1);   // rtcSqWv[] = {1, 1024, 4096, 8192}
  Clock.enable32kHz(false);
  Clock.setClockMode(false);          // set to 24h clock

  pinMode(rtcInterruptPin, INPUT); // RTC (external 10k pullup on square wave out from RTC)
  attachInterrupt(digitalPinToInterrupt(rtcInterruptPin), rtcISR, FALLING);

  waitForNextSecond();
}

/*************************************************************************************************/
void loop() {
  rtcFreqCheck();
  //checkSecondTiming();        // REPEATED CALLS OF THIS FUNCTION CAUSES ISSUES WITH SQUARE WAVE FREQ!
                                // ALSO CAUSES ISSUES WITH 32KHZ OUTPUT
}

/*************************************************************************************************/
void rtcISR() {
  ++rtcCount;
  
  if (rtcCount == rtcFreq) {               // Increment elapsed time in seconds
    ++tElapsed;
    rtcCount = 0;
  }

  rtcTrigger = true;
}

/*************************************************************************************************/
void rtcFreqCheck() {
  static uint32_t prevTime = 0, currentTime = 0, i = 0, rtcTime[rtcFreq*samplePeriod];
  uint32_t sum;
  uint16_t j = 0;
  float rtcCalcFreq;
  
  if (rtcTrigger == true) {
    rtcTrigger = false;
    currentTime = micros();
    rtcTime[i] = currentTime - prevTime;
    i++;
    prevTime = currentTime;
    
    if (i == rtcFreq) {
      i = 0;
      sum = 0;
      for (j = 0; j < rtcFreq; j++) {
        sum = sum + rtcTime[j];
      }
      rtcCalcFreq = float(rtcFreq)/sum * 1.0e6;
      memset(rtcTime, 0, rtcFreq*sizeof(rtcTime[0]));
      Serial.print(sum);                  Serial.print("   ");
      Serial.print(rtcCalcFreq);          Serial.print("   ");
      Serial.print(tElapsed);             Serial.print("   ");
      Serial.println(Clock.getSecond());
      // Look for consistent pulse counts and timing frequency
      // Ideal output: 1000000   1024.00   x   y   
      // First number (sum of counts): will be off by a few tenths of perct. and vary by ~4-5 counts
      // due to innacurate internal timing on Arduino and innacuracy of micros().
      // Calc. of pulse freq. will also be off slightly due to the above.
      // Last two numbers wont be identical, but should maintain the same offset (e.g. both odd, both even, etc)
    }
  }
}

/*************************************************************************************************/
void checkSecondTiming() { // Determines if a second has passed using the RTC
  uint16_t newSecond, rtc1, rtc2;
  uint32_t ti, tf;
  rtc1 = rtcCount;
  ti = micros();

  newSecond = Clock.getSecond();    // <-- Repeated calls to this function are the problem
  
  if (currentSecond != newSecond ) {
    currentSecond = newSecond;
    rtc2 = rtcCount;
    tf = micros();
    cout << currentSecond << "   " << rtc1 << "   " << rtc2 << "   " << tf-ti <<'\n'; 
    // look for consisent timing between rtc1, rtc2, and tf-ti
  }
}

/*************************************************************************************************/
void waitForNextSecond() { // Wait for a second to pass on the RTC before starting timers
  uint16_t nextSecond;
  // On start/resume, wait until next second has passed to sync timing
  detachInterrupt(digitalPinToInterrupt(rtcInterruptPin));
  nextSecond = Clock.getSecond();
  while (1) {
    if (nextSecond != Clock.getSecond()) {
      currentSecond = Clock.getSecond();
      // reset timing to zero
      rtcCount = 0;
      attachInterrupt(digitalPinToInterrupt(rtcInterruptPin), rtcISR, RISING);
      break;
    }
  }
}

After doing the setup, I wait for a second to rollover on the RTC and then enter the main loop. I have two functions in the main loop: rtcFreqCheck() and checkSecondTiming().

rtcFreqCheck() logs the time between rtc pulses using micros() and stores them in an array for one full second cycle. When 1024 pulses is reached, it sums up those times, calculates the equiv. freq, and displays some values to the serial port. This function shows consistent values - although it isn’t perfectly accurate due to errors in the arduino clock itself and variability in the micros() function. But it’s close enough and gives consistent results for 12+ hours so long as the next function is not running…

The second function is checkSecondTiming(). It retrieves the current second from the RTC using Clock.getSecond() and then compares that to the previously known value. If they are different, a second rollover has occurred and it prints out some timing numbers on what count and total time it took to process. Those timing numbers are consistent. The problem is that when this function is active in the main loop, the frequency of the square wave output and 32khz output vary by a few tenths of a percent. The rtcFreqCheck() function shows this variation as well as the scope.

There is a third function, waitForNextSecond(), but it is only used in setup to start things up immediately after a second has rolled over on the RTC. It too probably causes these issues, but you don’t know it because no timing is being performed yet.

Can you look (with a magnifier) at the pullup resistors: https://forum.arduino.cc/index.php?action=dlattach;topic=223513.0;attach=72003. Those are four resistors and only two are used. Officially they are 1k5, which is already stupidly low. But sometimes they are 1k0.

If your DS3231 module also has pullup resistors, and the internal pullup resistors of the SAM3X8E are also enabled, then it might be too much pullup.

I'm not saying that this could be the cause of the problem. I give it 1% chance, but it is the only thing that I know that might be wrong.

Normal pullup resistors can be 10k (with short wires) or 4k7. If you have the DS3231 on a module, then there are probably already pullup resistors on the module.

Can you write your code in a better way ? The I2C bus is slow. Reading the seconds so often with a slow I2C bus makes no sense. I think millis() is more accurate, or use one of the internal Timers.

Can you give a link to the library that you use ? The Arduino Wire library is often used in the wrong way. This one: https://github.com/NorthernWidget/DS3231 ? That one is okay.

The Due is fast and the Wire library for the Due had some bugs in the past that had to do with the fast processor. Could you get the source of the getSecond() into your sketch and add a delay of 1ms after the Wire.endTransmission() and 1ms after reading the byte.

The AVR microcontrollers (also the ATmega2560) has a slew rate limiter and a filter for the I2C bus. Newer processors don't have that, causing steep flanks and more crosstalk and more noise. Do you use seperate wires for SDA and SCL ? They are not in a flat ribbon cable or other cable ?