Does using i2c affect millis() accuracy?

I’m trying to track down where I am seeing larger than expected drift with millis(). I know it’s not an accurate timekeeping source, but I just need it to be accurate within a few seconds for up to tens of minutes. I’m seeing it’s drifting (compared to my phone) by 1 second or so every 10-20 seconds if I am using an I2C LCD (Adafruit’s RGBLCDShield specifically)

Here’s a test sketch:

#define SECONDS_MS 1000
#define RED 0x1
#define YELLOW 0x3
#define GREEN 0x2
#define TEAL 0x6
#define BLUE 0x4
#define VIOLET 0x5
#define WHITE 0x7

#define LCD_COLUMNS 16
#define LCD_ROWS 2
#include <Adafruit_RGBLCDShield.h>

Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();

long timePassed;
unsigned long previousMillis;

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

  /* Print Title and Version */
  lcd.setBacklight(YELLOW);
  lcd.setCursor(0,0);
  lcd.print(F("DevDawg"));
  lcd.setCursor(0,1);
  timePassed = 0;
  previousMillis = 0;
}

void loop()
{
  // Update once a second
  if(millis() - previousMillis >= SECONDS_MS)
  {
    //Serial.print("Time Elapsed: ");
    //Serial.println(timePassed);
    updateDisplay(int(timePassed), 40);
    ++timePassed;
    previousMillis = millis();
  }
}

void updateDisplay(int totalSeconds, int temperature)
{
  char buffer[32];
  sprintf(buffer, "%02d:%02d / %02dC", int(totalSeconds / 60), int(totalSeconds % 60), temperature);
  lcd.setCursor(0,1);
  lcd.print(buffer);
}

I noticed if I remove all the LCD code (including the include, object, etc.) and just look at the Serial output, it’s better but not great - it lost about 2 seconds over 5 minutes which I think is good enough for now. If I comment out the Serial.print’s but comment in the LCD bits, it lost 6 seconds in 60 seconds which is really bad.

Is there anything I can do to help with that? The application is for a film rotary processor and while I can just use the microcontroller to turn the rotary and keep track of the water bath, it would be nice to have it track development time as well.

I was planning on switching to a “normal” LCD and wiring it direct (and using a resistor ladder for buttons on an analog input). Might that help keep millis() on track if/until I can look at implementing an RTC? I feel like an RTC might be overkill - the maximum time I would expect to develop for would be 20 minutes. That would be for black and white film and it tends to be less regimented than color films. For color, I just need typically six minutes or less (3:00 or 3:15 when developing normally) but it should be reasonably accurate - a few seconds is ok but not really any more than that.

I read that replacing the oscillator with something better might help, but since I’m only seeing the drift with I2C devices, I suspect something is awry there? In the full application (found here) I am also using a Dallas Temperature sensor as well as the PID library. I haven’t evaluated them directly yet with clock drift since the LCD was easier to test (and seems to be causing a rather large drift by itself).

Thoughts?

You're chasing an x-y problem

The problem is the way you’re timing what you think is a second, it’s not. In addition, your method of checking the actual time is quite error prone. The Uno clock is far more accurate over 15 seconds than your reaction time and a phone timer.

void loop()
{
  // Update once a second
  if(millis() - previousMillis >= SECONDS_MS)
  {
    //Serial.print("Time Elapsed: ");
    //Serial.println(timePassed);
    updateDisplay(int(timePassed), 40);
    ++timePassed;
    previousMillis = millis();
  }
}

This is flawed - it assumes that updateDisplay() is instantaneous. It isn't - but millis() should stay accurate, because it doesn't keep interrupts disabled while it's doing it's thing. What you're doing is checking the time, and if it's a second after the last timestamp, you do stuff, then update the timestamp. This doesn't do it "every second", this does each iteration one second after the previous one completed.

Imagine you were eating a cookie every half hour (I'm home for the holidays, and there are cookies to eat, so this example is on my mind). The way you're doing it, you set a half hour timer on your phone. It goes off. You get a cookie, eat it, and then reset the timer. This doesn't quite get you a cookie every half hour - but if you set the timer again before you get and eat the cookie, then you get the cookie every half hour.

Try this instead:

void loop()
{
  // Update once a second
  if(millis() - previousMillis >= SECONDS_MS)
  {
    //Serial.print("Time Elapsed: ");
    //Serial.println(timePassed);
    updateDisplay(int(timePassed), 40); 
    timePassed++;
    previousMillis +=1000; //don't assume all the above code ran instantly.
  }
}

(I would do this a bit differently if it were me writing this code, but the point is to demonstrate how this simple adjustment rectifies the problem you're seeing)

Note: I wouldn't call this an x-y problem - he knows what he wants to do, it's a perfectly reasonable thing to do, and has an idea about how to achieve it, but just implemented it wrong, didn't see the logical flaw in his program, and started grasping at straws. Unlike a typical x-y problem, he posted the information we need to set him straight.

Aha!!! Yes that makes a ton of sense - thanks for clarifying! I was wracking my brain trying to figure out why my readings were off by so much.

With the LCD is very noticeable very early which now makes sense if it's taking a long time to update (over Serial). Collecting temperatures and things also takes time if I'm not mistaken. Thank you very much for the insight!

It's been an interesting road having to handle timings like this since there's quite a few things based on doing things periodically (check the temperature every so often, change state based on the PID, change direction on the motor including a short coast, etc.). Of all those the only one that's really important is keeping track of elapsed time - the others can drift by some amount and it's ok.

I suspect I may want/need to consider using the Timer library - that seems a lot cleaner than the way I'm doing it I think. I'm trying to avoid using interrupts since I know I'll need one for handling a Triac/SSR (to hopefully improve the accuracy of heating a waterbath) and as I recall a buzzer may also use an interrupt?

EDIT: As an updated, I tested this against my full application and it was accurate to within 1 second after 10 minutes which I would say is within the margin of error for how I'm comparing the time. That's fantastic! Thank you again DrAzzy!