9

Going through an old project, I had code on two Arduino Due that looked like this

void loop()
{
  foo();
  delay(time);
}

taking to heart the majority of literature on using delay(); I recoded this as

void loop()
{
  static unsigned long PrevTime;
  if(millis()-PrevTime>time)
  {
    foo();
    PrevTime=millis();
  }
}

However, this seems to have created a situation where the two devices drift over a period of time when they didn't previously

My question is twofold:

  1. Why would if(millis()-PrevTime>time) cause more drift than delay(time)?
  2. Is there a way to prevent this drift without going back to delay(time)?
4
  • 1
    What is the order of magnitude of "period of time" over which you notice the drift? Are the two devices in the same location, thus at the same temperature? Are they run on a crystal oscillator or a ceramic resonator? Dec 4, 2017 at 16:48
  • Personally I prefer Majenko's solution, and always use it (I put the incrementing before the other instructions, but this is just a preference). Note, however, that this timing is PRECISELY 100ms, while the other code (foo; delay;) has a period longer than 100ms (it is 100ms + time of foo). So you will experience drift (but it is the delay-implemented SW which is drifting). In any case, keep in mind that even perfectly equal implementations "drift", because clocks are not equal; if you need complete synchronization use a signal to, well, synchronize the two programs.
    – frarugi87
    Dec 4, 2017 at 17:10
  • The two devices are next to each other, after running from Friday at 17:00 to Monday at 9:00 there was a 4 min drift. I'm deciding that I'm going to use a digital pin to synchronize the inputs as per your suggestion
    – ATE-ENGE
    Dec 4, 2017 at 20:42
  • "The two devices are next to each other, ..." that doesn't mean the timing mechanism is not accurate you are talking about an error rate of ~800ppm, high for two crystal oscillators but reasonable for even one ceramic resonator. you have to compare it vs. a reasonably accurate timing standard to be sure: crystals are typically within 20ppm, and tcxo's can do sub 1ppm. that would be my way of doing it.
    – dannyf
    Dec 4, 2017 at 23:30

3 Answers 3

12

There's one important thing that you need to remember when working with time on an Arudino of any form:

  • Every operation takes time.

Your foo() function will take an amount of time. What that time is, we cannot say.

The most reliable way of dealing with time is to only rely on the time for triggering, not for working out when the next triggering should be.

For instance, take the following:

if (millis() - last > interval) {
    doSomething();
    last = millis();
}

The variable last will be the time the routine triggered *plus the time doSomething took to run. So say interval is 100, and doSomething takes 10ms to run, you will get triggerings at 101ms, 212ms, 323ms, etc. Not the 100ms you were expecting.

So one thing you can do is to always use the same time regardless by remembering it at once specific point (as Juraj suggests):

uint32_t time = millis();

if (time - last > interval) {
    doSomething();
    last = time;
}

Now the time that doSomething() takes will have no effect on anything. So you will get triggerings at 101ms, 202ms, 303ms, etc. Still not quite the 100ms you wanted - because you're looking for more that 100ms having passed - and that means 101ms or more. Instead you should use >=:

uint32_t time = millis();

if (time - last >= interval) {
    doSomething();
    last = time;
}

Now, assuming that nothing else happens in your loop you get triggerings at 100ms, 200ms, 300ms, etc. But note that bit: "as long as nothing else happens in your loop"...

What happens if an operation that takes 5ms happens to occur at 99ms...? Your next triggering will be delayed until 104ms. That's a drift. But it's easy to combat. Instead of saying "The recorded time is now" you say "The recorded time is 100ms later than it was". That means that no matter what delays you get in your code your triggering will always be at 100ms intervals, or drift within a 100ms tick.

if (millis() - last >= interval) {
    doSomething();
    last += interval;
}

Now you will get triggerings at 100ms, 200ms, 300ms, etc. Or if there are delays in other bits of code you may get 100ms, 204ms, 300ms, 408ms, 503ms, 600ms, etc. It always tries to run it as close to the interval as possible regardless of delays. And if you have delays that are greater than the interval it will automatically run your routine enough times to catch up with the current time.

Before you had drift. Now you have jitter.

1

Because you reset the timer after the operation.

static unsigned long PrevTime=millis();

unsigned long t = millis();

if (t - PrevTime > time) {
    foo();
    PrevTime = t;
}
4
  • no. notice that PrevTime is static.
    – dannyf
    Dec 4, 2017 at 16:48
  • 4
    @dannyf, yes, and?
    – Juraj
    Dec 4, 2017 at 18:53
  • if you knew what "static" means, you would know why your answer isn't correct.
    – dannyf
    Dec 4, 2017 at 23:31
  • I know what static does with local variable.. I do not understand why you think my answer has something to do with the static. I just moved the current millis reading before foo() is invoked.
    – Juraj
    Dec 5, 2017 at 7:49
0

For what you are trying to do, delay() is the appropriate way to implement the code. The reason you would want to use the if(millis()) is if you want to allow the main loop to continue to loop so your code or some other code outside of that loop can do other processing.

For example:

long next_trigger_time = 0L;
long interval = DESIRED_INTERVAL;

void loop() {
   do_something(); // check a sensor or value set by an interrupt
   long m = millis();
   if (m >= next_trigger_time) {
       next_trigger_time = m + interval;
       foo();
   }
}

This would run foo() on the specified interval while allowing the loop to continue to run in between. I put the calculation of next_trigger_time before the call to foo() to help minimize the drift, but it is inevitable. If drift is a significant concern, use an interrupt timer or some sort of clock/timer synchronization. Also remember that millis() will wrap around after some period of time and I didn't account for that to keep the code example simple.

8
  • Hate to mention this: rollover problem in 52 days.
    – user31481
    Dec 7, 2017 at 18:04
  • I already mentioned the rollover issue at the end of my answer. Dec 7, 2017 at 18:06
  • Well, solve it.
    – user31481
    Dec 7, 2017 at 18:08
  • My standard consulting fee $100/hr if you want me to write code for you. I think what I have written is a sufficiently relevant. Dec 7, 2017 at 18:13
  • 1
    Are you aware that Majenko posted a more complete and better answer that yours? Are you aware that your code doens't compile? That long m - millis() doesn't do what you intend to do? That's on the house.
    – user31481
    Dec 7, 2017 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.